diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 5d789df..abe63a8 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,54 +1,54 @@
-name: Deploy to GitHub Pages
-
-on:
- push:
- branches:
- - main
- workflow_dispatch:
-
-permissions:
- contents: read
- pages: write
- id-token: write
-
-concurrency:
- group: "pages"
- cancel-in-progress: true
-
-jobs:
- build-and-deploy:
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: 'npm'
-
- - name: Install Dependencies
- run: npm ci
-
- - name: Build
- run: npm run build
- env:
- VITE_BASE_URL: '/${{ github.event.repository.name }}'
- - name: Create 404.html
- run: cp dist/index.html dist/404.html
-
- - name: Setup Pages
- uses: actions/configure-pages@v4
-
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: './dist'
-
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
+name: Deploy to GitHub Pages
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+jobs:
+ build-and-deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+
+ - name: Install Dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+ env:
+ VITE_BASE_URL: '/${{ github.event.repository.name }}'
+ - name: Create 404.html
+ run: cp dist/index.html dist/404.html
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: './dist'
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index a547bf3..4108b33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,24 +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?
+# 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/Dockerfile b/Dockerfile
index ee47d5c..8ddc99b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,15 @@
-# Build stage
-FROM node:20-alpine as build
-
-WORKDIR /app
-COPY package*.json ./
-RUN npm ci
-COPY . .
-RUN npm run build
-
-# Production stage
-FROM nginx:alpine
-COPY --from=build /app/dist /usr/share/nginx/html
-COPY nginx.conf /etc/nginx/conf.d/default.conf
-EXPOSE 80
-CMD ["nginx", "-g", "daemon off;"]
+# Build stage
+FROM node:20-alpine as build
+
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci
+COPY . .
+RUN npm run build
+
+# Production stage
+FROM nginx:alpine
+COPY --from=build /app/dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/LICENSE b/LICENSE
index ee16d29..fedd654 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,21 @@
-MIT License
-
-Copyright (c) 2024 Chrissy8283 (aka Tomato6966)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+MIT License
+
+Copyright (c) 2024 Chrissy8283 (aka Tomato6966)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 8a09415..d05baf5 100644
--- a/README.md
+++ b/README.md
@@ -1,84 +1,84 @@
-# Investment Portfolio Simulator
-
-A modern web application for simulating and tracking investment portfolios with real-time data. Built with React, TypeScript, and Tailwind CSS.
-
-Why this Project?
-- I wanted to see how my portfolio would perform if I had invested in something else, and thus with savings plan(s)
-- The main issue with other saving-plan calculators is, that they calculate based on the p.a. performance, i wanted further insights however, and thus this projected was created.
-- It allows you to see how single-investments and savings plans would have performed, and how they would have affected your portfolio.
-- There are multiple indicators and design choices made:
- - TTWOR (Time Travel Without Risk) calculations
- - Average Portfolio Performance
- - Portfolio Performance & Value
- - All assets (except the TTWOR and Portfolio-Value) are scaled by percentage of their price. Thus their referenced, scale is on the right. The referenced scale on the left is only for the portfolio-value
-
-https://github.com/user-attachments/assets/4507e102-8dfb-4614-b2ba-938e20e3d97b
-
-## Features
-
-- π Real-time stock data from Yahoo Finance
-- π° Track multiple assets and investments
-- π Interactive charts with performance visualization
-- π Dark/Light mode support
-- π± Responsive design
- - *Mobile friendly*
-- π
Historical data analysis
- - *The portfolio is fully based on real-historical data, with configurable timeranges*
-- πΉ TTWOR (Time Travel Without Risk) calculations
- - *Including metrics for TTWOR*
-- π Support for one-time and periodic investments
- - *You can config your dream-portfolio by one time and periodic investments easily*
-- π Detailed performance metrics
- - *See all needed performance metrics in one place*
-- π
Future Projection with Withdrawal Analysis and Sustainability Analysis
- - *Generate a future projection based on the current portfolio performance, with a withdrawal analysis and sustainability analysis + calculator*
- - *Including with best, worst and average case scenarios*
-- π Savings Plan Performance Overview Tables
- - *See the performance of your savings plans if you have multiple assets to compare them*
-- π Export to PDF
- - *Export the entire portfolio Overview to a PDF, including Future Projections of 10, 15, 20, 30 and 40 years*
-- π Export to CSV Tables
- - *Export all available tables to CSV*
-- See the asset performance p.a. as well as of the portfolio
-
-## Tech Stack
-
-- React 19
-- TypeScript
-- Tailwind CSS
-- Vite@6
-- Recharts
-- date-fns
-- Lucide Icons
-
-## Self Hosting
-
-### Prerequisites
-
-- Node.js & npm 20 or higher
-
-### Local Development
-
-1. Clone the repository
-2. Run `npm install`
-3. Run `npm run dev` -> developer preview
- - Run `npm run build` -> build for production (dist folder) (you can then launch it with dockerfile or with a static file server like nginx)
- - Run `npm run preview` -> preview the production build (dist folder)
-
-
-
-
-
-
-
-
-
-
-
-
-### Credits:
-
-> Thanks to [yahoofinance](https://finance.yahoo.com/) for the stock data.
-
-
-- **15.01.2025:** Increased Performance of entire Site by utilizing Maps
+# Investment Portfolio Simulator
+
+A modern web application for simulating and tracking investment portfolios with real-time data. Built with React, TypeScript, and Tailwind CSS.
+
+Why this Project?
+- I wanted to see how my portfolio would perform if I had invested in something else, and thus with savings plan(s)
+- The main issue with other saving-plan calculators is, that they calculate based on the p.a. performance, i wanted further insights however, and thus this projected was created.
+- It allows you to see how single-investments and savings plans would have performed, and how they would have affected your portfolio.
+- There are multiple indicators and design choices made:
+ - TTWOR (Time Travel Without Risk) calculations
+ - Average Portfolio Performance
+ - Portfolio Performance & Value
+ - All assets (except the TTWOR and Portfolio-Value) are scaled by percentage of their price. Thus their referenced, scale is on the right. The referenced scale on the left is only for the portfolio-value
+
+https://github.com/user-attachments/assets/4507e102-8dfb-4614-b2ba-938e20e3d97b
+
+## Features
+
+- π Real-time stock data from Yahoo Finance
+- π° Track multiple assets and investments
+- π Interactive charts with performance visualization
+- π Dark/Light mode support
+- π± Responsive design
+ - *Mobile friendly*
+- π
Historical data analysis
+ - *The portfolio is fully based on real-historical data, with configurable timeranges*
+- πΉ TTWOR (Time Travel Without Risk) calculations
+ - *Including metrics for TTWOR*
+- π Support for one-time and periodic investments
+ - *You can config your dream-portfolio by one time and periodic investments easily*
+- π Detailed performance metrics
+ - *See all needed performance metrics in one place*
+- π
Future Projection with Withdrawal Analysis and Sustainability Analysis
+ - *Generate a future projection based on the current portfolio performance, with a withdrawal analysis and sustainability analysis + calculator*
+ - *Including with best, worst and average case scenarios*
+- π Savings Plan Performance Overview Tables
+ - *See the performance of your savings plans if you have multiple assets to compare them*
+- π Export to PDF
+ - *Export the entire portfolio Overview to a PDF, including Future Projections of 10, 15, 20, 30 and 40 years*
+- π Export to CSV Tables
+ - *Export all available tables to CSV*
+- See the asset performance p.a. as well as of the portfolio
+
+## Tech Stack
+
+- React 19
+- TypeScript
+- Tailwind CSS
+- Vite@6
+- Recharts
+- date-fns
+- Lucide Icons
+
+## Self Hosting
+
+### Prerequisites
+
+- Node.js & npm 20 or higher
+
+### Local Development
+
+1. Clone the repository
+2. Run `npm install`
+3. Run `npm run dev` -> developer preview
+ - Run `npm run build` -> build for production (dist folder) (you can then launch it with dockerfile or with a static file server like nginx)
+ - Run `npm run preview` -> preview the production build (dist folder)
+
+
+
+
+
+
+
+
+
+
+
+
+### Credits:
+
+> Thanks to [yahoofinance](https://finance.yahoo.com/) for the stock data.
+
+
+- **15.01.2025:** Increased Performance of entire Site by utilizing Maps
diff --git a/browserconfig.xml b/browserconfig.xml
index c554148..856ab2c 100644
--- a/browserconfig.xml
+++ b/browserconfig.xml
@@ -1,2 +1,2 @@
-
+
#ffffff
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
index 25300b0..96d3ece 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,29 +1,29 @@
-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,
- '@typescript-eslint/no-explicit-any': 'off',
- 'react-refresh/only-export-components': [
- 'warn',
- { allowConstantExport: true },
- ],
- },
- }
-);
+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,
+ '@typescript-eslint/no-explicit-any': 'off',
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+ }
+);
diff --git a/index.html b/index.html
index a0b1574..28a7196 100644
--- a/index.html
+++ b/index.html
@@ -1,45 +1,45 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Investment Portfolio Simulator
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Investment Portfolio Simulator
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/manifest.json b/manifest.json
index 013d4a6..7cfcc91 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,41 +1,41 @@
-{
- "name": "App",
- "icons": [
- {
- "src": "\/android-icon-36x36.png",
- "sizes": "36x36",
- "type": "image\/png",
- "density": "0.75"
- },
- {
- "src": "\/android-icon-48x48.png",
- "sizes": "48x48",
- "type": "image\/png",
- "density": "1.0"
- },
- {
- "src": "\/android-icon-72x72.png",
- "sizes": "72x72",
- "type": "image\/png",
- "density": "1.5"
- },
- {
- "src": "\/android-icon-96x96.png",
- "sizes": "96x96",
- "type": "image\/png",
- "density": "2.0"
- },
- {
- "src": "\/android-icon-144x144.png",
- "sizes": "144x144",
- "type": "image\/png",
- "density": "3.0"
- },
- {
- "src": "\/android-icon-192x192.png",
- "sizes": "192x192",
- "type": "image\/png",
- "density": "4.0"
- }
- ]
+{
+ "name": "App",
+ "icons": [
+ {
+ "src": "\/android-icon-36x36.png",
+ "sizes": "36x36",
+ "type": "image\/png",
+ "density": "0.75"
+ },
+ {
+ "src": "\/android-icon-48x48.png",
+ "sizes": "48x48",
+ "type": "image\/png",
+ "density": "1.0"
+ },
+ {
+ "src": "\/android-icon-72x72.png",
+ "sizes": "72x72",
+ "type": "image\/png",
+ "density": "1.5"
+ },
+ {
+ "src": "\/android-icon-96x96.png",
+ "sizes": "96x96",
+ "type": "image\/png",
+ "density": "2.0"
+ },
+ {
+ "src": "\/android-icon-144x144.png",
+ "sizes": "144x144",
+ "type": "image\/png",
+ "density": "3.0"
+ },
+ {
+ "src": "\/android-icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image\/png",
+ "density": "4.0"
+ }
+ ]
}
\ No newline at end of file
diff --git a/nginx.conf b/nginx.conf
index 0ff48c7..6828742 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -1,17 +1,17 @@
-server {
- listen 80;
- server_name localhost;
- root /usr/share/nginx/html;
- index index.html;
-
- # Support for SPA routing
- location / {
- try_files $uri $uri/ /index.html;
- }
-
- # Cache static assets
- location /assets/ {
- expires 1y;
- add_header Cache-Control "public, no-transform";
- }
-}
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Support for SPA routing
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Cache static assets
+ location /assets/ {
+ expires 1y;
+ add_header Cache-Control "public, no-transform";
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index f6632b1..2f76808 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,4742 +1,4742 @@
-{
- "name": "investment-portfolio-tracker",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "investment-portfolio-tracker",
- "version": "1.0.0",
- "dependencies": {
- "date-fns": "^4.1.0",
- "jspdf": "^2.5.2",
- "jspdf-autotable": "^3.8.4",
- "lucide-react": "^0.469.0",
- "react": "^19.0.0",
- "react-dom": "^19.0.0",
- "react-hot-toast": "^2.4.1",
- "react-router-dom": "^7.1.0",
- "recharts": "^2.15.0",
- "use-debounce": "^10.0.4"
- },
- "devDependencies": {
- "@eslint/js": "^9.17.0",
- "@types/node": "^22.10.2",
- "@types/react": "^19.0.2",
- "@types/react-dom": "^19.0.2",
- "@vitejs/plugin-react": "^4.3.4",
- "autoprefixer": "^10.4.20",
- "eslint": "^9.17.0",
- "eslint-plugin-react-hooks": "^5.1.0",
- "eslint-plugin-react-refresh": "^0.4.16",
- "globals": "^15.14.0",
- "postcss": "^8.4.49",
- "tailwindcss": "^3.4.17",
- "typescript": "^5.7.2",
- "typescript-eslint": "^8.18.1",
- "vite": "^6.0.5"
- }
- },
- "node_modules/@alloc/quick-lru": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
- "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
- "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.26.3",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz",
- "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
- "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.26.0",
- "@babel/generator": "^7.26.0",
- "@babel/helper-compilation-targets": "^7.25.9",
- "@babel/helper-module-transforms": "^7.26.0",
- "@babel/helpers": "^7.26.0",
- "@babel/parser": "^7.26.0",
- "@babel/template": "^7.25.9",
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.26.0",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.26.3",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
- "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.26.3",
- "@babel/types": "^7.26.3",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
- "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.25.9",
- "@babel/helper-validator-option": "^7.25.9",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
- "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
- "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9",
- "@babel/traverse": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz",
- "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
- "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
- "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
- "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
- "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.26.3",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
- "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.26.3"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
- "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
- "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/runtime": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
- "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.26.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
- "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.26.3",
- "@babel/parser": "^7.26.3",
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.3",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.26.3",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
- "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz",
- "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz",
- "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz",
- "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz",
- "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz",
- "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz",
- "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz",
- "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz",
- "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz",
- "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz",
- "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz",
- "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz",
- "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz",
- "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz",
- "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz",
- "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz",
- "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz",
- "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz",
- "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz",
- "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz",
- "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz",
- "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz",
- "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz",
- "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz",
- "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
- "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
- "dev": true,
- "dependencies": {
- "eslint-visitor-keys": "^3.3.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
- }
- },
- "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint-community/regexpp": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
- }
- },
- "node_modules/@eslint/config-array": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
- "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/object-schema": "^2.1.5",
- "debug": "^4.3.1",
- "minimatch": "^3.1.2"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/core": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz",
- "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/eslintrc": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
- "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "^6.12.4",
- "debug": "^4.3.2",
- "espree": "^10.0.1",
- "globals": "^14.0.0",
- "ignore": "^5.2.0",
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
- "minimatch": "^3.1.2",
- "strip-json-comments": "^3.1.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint/eslintrc/node_modules/globals": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@eslint/js": {
- "version": "9.17.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
- "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/object-schema": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
- "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/plugin-kit": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz",
- "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@humanfs/core": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
- "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node": {
- "version": "0.16.6",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
- "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@humanfs/core": "^0.19.1",
- "@humanwhocodes/retry": "^0.3.0"
- },
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
- "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/module-importer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
- "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
- "dev": true,
- "engines": {
- "node": ">=12.22"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/retry": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
- "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "dev": true,
- "dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
- "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
- "dev": true,
- "dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
- "dev": true
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
- "dev": true,
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
- "optional": true,
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
- "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
- "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
- "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
- "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
- "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
- "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
- "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
- "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
- "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
- "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
- "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
- "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
- "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
- "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
- "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
- "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@types/babel__core": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
- "dev": true,
- "dependencies": {
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7",
- "@types/babel__generator": "*",
- "@types/babel__template": "*",
- "@types/babel__traverse": "*"
- }
- },
- "node_modules/@types/babel__generator": {
- "version": "7.6.8",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
- "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
- "dev": true,
- "dependencies": {
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
- "dev": true,
- "dependencies": {
- "@babel/parser": "^7.1.0",
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__traverse": {
- "version": "7.20.6",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
- "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
- "dev": true,
- "dependencies": {
- "@babel/types": "^7.20.7"
- }
- },
- "node_modules/@types/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
- "license": "MIT"
- },
- "node_modules/@types/d3-array": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
- "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
- },
- "node_modules/@types/d3-color": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
- "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
- },
- "node_modules/@types/d3-ease": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
- "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
- },
- "node_modules/@types/d3-interpolate": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
- "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
- "dependencies": {
- "@types/d3-color": "*"
- }
- },
- "node_modules/@types/d3-path": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
- "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="
- },
- "node_modules/@types/d3-scale": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
- "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
- "dependencies": {
- "@types/d3-time": "*"
- }
- },
- "node_modules/@types/d3-shape": {
- "version": "3.1.6",
- "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
- "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==",
- "dependencies": {
- "@types/d3-path": "*"
- }
- },
- "node_modules/@types/d3-time": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
- "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
- },
- "node_modules/@types/d3-timer": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
- "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
- },
- "node_modules/@types/estree": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
- "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
- "dev": true
- },
- "node_modules/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true
- },
- "node_modules/@types/node": {
- "version": "22.10.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
- "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.20.0"
- }
- },
- "node_modules/@types/raf": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
- "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/@types/react": {
- "version": "19.0.2",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.2.tgz",
- "integrity": "sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "19.0.2",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz",
- "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "^19.0.0"
- }
- },
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz",
- "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.18.1",
- "@typescript-eslint/type-utils": "8.18.1",
- "@typescript-eslint/utils": "8.18.1",
- "@typescript-eslint/visitor-keys": "8.18.1",
- "graphemer": "^1.4.0",
- "ignore": "^5.3.1",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "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"
- }
- },
- "node_modules/@typescript-eslint/parser": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz",
- "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/scope-manager": "8.18.1",
- "@typescript-eslint/types": "8.18.1",
- "@typescript-eslint/typescript-estree": "8.18.1",
- "@typescript-eslint/visitor-keys": "8.18.1",
- "debug": "^4.3.4"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
- }
- },
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz",
- "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.18.1",
- "@typescript-eslint/visitor-keys": "8.18.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/type-utils": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz",
- "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/typescript-estree": "8.18.1",
- "@typescript-eslint/utils": "8.18.1",
- "debug": "^4.3.4",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
- }
- },
- "node_modules/@typescript-eslint/types": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz",
- "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz",
- "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.18.1",
- "@typescript-eslint/visitor-keys": "8.18.1",
- "debug": "^4.3.4",
- "fast-glob": "^3.3.2",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <5.8.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.6.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
- "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@typescript-eslint/utils": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz",
- "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "8.18.1",
- "@typescript-eslint/types": "8.18.1",
- "@typescript-eslint/typescript-estree": "8.18.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
- }
- },
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz",
- "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.18.1",
- "eslint-visitor-keys": "^4.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@vitejs/plugin-react": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
- "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.26.0",
- "@babel/plugin-transform-react-jsx-self": "^7.25.9",
- "@babel/plugin-transform-react-jsx-source": "^7.25.9",
- "@types/babel__core": "^7.20.5",
- "react-refresh": "^0.14.2"
- },
- "engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
- }
- },
- "node_modules/acorn": {
- "version": "8.14.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
- "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/any-promise": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
- "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true
- },
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/arg": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
- "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "dev": true
- },
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
- "license": "Python-2.0"
- },
- "node_modules/atob": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
- "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
- "license": "(MIT OR Apache-2.0)",
- "bin": {
- "atob": "bin/atob.js"
- },
- "engines": {
- "node": ">= 4.5.0"
- }
- },
- "node_modules/autoprefixer": {
- "version": "10.4.20",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
- "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/autoprefixer"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "browserslist": "^4.23.3",
- "caniuse-lite": "^1.0.30001646",
- "fraction.js": "^4.3.7",
- "normalize-range": "^0.1.2",
- "picocolors": "^1.0.1",
- "postcss-value-parser": "^4.2.0"
- },
- "bin": {
- "autoprefixer": "bin/autoprefixer"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- },
- "peerDependencies": {
- "postcss": "^8.1.0"
- }
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
- },
- "node_modules/base64-arraybuffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
- "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 0.6.0"
- }
- },
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/browserslist": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
- "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "caniuse-lite": "^1.0.30001663",
- "electron-to-chromium": "^1.5.28",
- "node-releases": "^2.0.18",
- "update-browserslist-db": "^1.1.0"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/btoa": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
- "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
- "license": "(MIT OR Apache-2.0)",
- "bin": {
- "btoa": "bin/btoa.js"
- },
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/camelcase-css": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
- "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "dev": true,
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001667",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
- "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ]
- },
- "node_modules/canvg": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
- "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@babel/runtime": "^7.12.5",
- "@types/raf": "^3.4.0",
- "core-js": "^3.8.3",
- "raf": "^3.4.1",
- "regenerator-runtime": "^0.13.7",
- "rgbcolor": "^1.0.1",
- "stackblur-canvas": "^2.0.0",
- "svg-pathdata": "^6.0.3"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/canvg/node_modules/regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/chokidar/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/clsx": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/commander": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
- "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
- "dev": true,
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cookie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
- "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/core-js": {
- "version": "3.39.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
- "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/css-line-break": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
- "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "utrie": "^1.0.2"
- }
- },
- "node_modules/cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
- "bin": {
- "cssesc": "bin/cssesc"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
- },
- "node_modules/d3-array": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
- "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
- "dependencies": {
- "internmap": "1 - 2"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-color": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
- "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-ease": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
- "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-format": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
- "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-interpolate": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
- "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
- "dependencies": {
- "d3-color": "1 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-path": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
- "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-scale": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
- "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
- "dependencies": {
- "d3-array": "2.10.0 - 3",
- "d3-format": "1 - 3",
- "d3-interpolate": "1.2.0 - 3",
- "d3-time": "2.1.1 - 3",
- "d3-time-format": "2 - 4"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-shape": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
- "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
- "dependencies": {
- "d3-path": "^3.1.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-time": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
- "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
- "dependencies": {
- "d3-array": "2 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-time-format": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
- "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
- "dependencies": {
- "d3-time": "1 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-timer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
- "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/date-fns": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
- "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/kossnocorp"
- }
- },
- "node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dev": true,
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/decimal.js-light": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
- "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
- },
- "node_modules/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true
- },
- "node_modules/didyoumean": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
- "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "dev": true
- },
- "node_modules/dlv": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
- "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "dev": true
- },
- "node_modules/dom-helpers": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
- "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.8.7",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/dompurify": {
- "version": "2.5.8",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
- "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
- "license": "(MPL-2.0 OR Apache-2.0)",
- "optional": true
- },
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.33",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz",
- "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==",
- "dev": true
- },
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true
- },
- "node_modules/esbuild": {
- "version": "0.24.0",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz",
- "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.24.0",
- "@esbuild/android-arm": "0.24.0",
- "@esbuild/android-arm64": "0.24.0",
- "@esbuild/android-x64": "0.24.0",
- "@esbuild/darwin-arm64": "0.24.0",
- "@esbuild/darwin-x64": "0.24.0",
- "@esbuild/freebsd-arm64": "0.24.0",
- "@esbuild/freebsd-x64": "0.24.0",
- "@esbuild/linux-arm": "0.24.0",
- "@esbuild/linux-arm64": "0.24.0",
- "@esbuild/linux-ia32": "0.24.0",
- "@esbuild/linux-loong64": "0.24.0",
- "@esbuild/linux-mips64el": "0.24.0",
- "@esbuild/linux-ppc64": "0.24.0",
- "@esbuild/linux-riscv64": "0.24.0",
- "@esbuild/linux-s390x": "0.24.0",
- "@esbuild/linux-x64": "0.24.0",
- "@esbuild/netbsd-x64": "0.24.0",
- "@esbuild/openbsd-arm64": "0.24.0",
- "@esbuild/openbsd-x64": "0.24.0",
- "@esbuild/sunos-x64": "0.24.0",
- "@esbuild/win32-arm64": "0.24.0",
- "@esbuild/win32-ia32": "0.24.0",
- "@esbuild/win32-x64": "0.24.0"
- }
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/eslint": {
- "version": "9.17.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz",
- "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.19.0",
- "@eslint/core": "^0.9.0",
- "@eslint/eslintrc": "^3.2.0",
- "@eslint/js": "9.17.0",
- "@eslint/plugin-kit": "^0.2.3",
- "@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.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.2.0",
- "eslint-visitor-keys": "^4.2.0",
- "espree": "^10.3.0",
- "esquery": "^1.5.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "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.3"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "jiti": "*"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-plugin-react-hooks": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz",
- "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==",
- "dev": true,
- "license": "MIT",
- "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"
- }
- },
- "node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.16",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz",
- "integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "eslint": ">=8.40"
- }
- },
- "node_modules/eslint-scope": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
- "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-visitor-keys": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/eslint/node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/eslint/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/eslint/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
- },
- "node_modules/eslint/node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint/node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/eslint/node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/espree": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
- "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "acorn": "^8.14.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/esquery": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
- "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
- "dev": true,
- "dependencies": {
- "estraverse": "^5.1.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/eventemitter3": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
- "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-equals": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
- "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/fast-glob": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
- "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
- "dev": true,
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.4"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true
- },
- "node_modules/fastq": {
- "version": "1.17.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
- "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
- "dev": true,
- "dependencies": {
- "reusify": "^1.0.4"
- }
- },
- "node_modules/fflate": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
- "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
- "license": "MIT"
- },
- "node_modules/file-entry-cache": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
- "dev": true,
- "dependencies": {
- "flat-cache": "^4.0.0"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
- "dev": true,
- "dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
- },
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/flatted": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
- "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
- "dev": true
- },
- "node_modules/foreground-child": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
- "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
- "dev": true,
- "dependencies": {
- "cross-spawn": "^7.0.0",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/fraction.js": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
- "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
- "dev": true,
- "engines": {
- "node": "*"
- },
- "funding": {
- "type": "patreon",
- "url": "https://github.com/sponsors/rawify"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
- "dev": true,
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/glob/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/glob/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/globals": {
- "version": "15.14.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
- "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/goober": {
- "version": "2.1.16",
- "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
- "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
- "license": "MIT",
- "peerDependencies": {
- "csstype": "^3.0.10"
- }
- },
- "node_modules/graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/html2canvas": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
- "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "css-line-break": "^2.1.0",
- "text-segmentation": "^1.0.3"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/ignore": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
- "dev": true,
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/import-fresh": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
- "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/internmap": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
- "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-core-module": {
- "version": "2.15.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
- "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
- "dev": true,
- "dependencies": {
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true
- },
- "node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "dev": true,
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
- "node_modules/jiti": {
- "version": "1.21.6",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
- "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
- "dev": true,
- "bin": {
- "jiti": "bin/jiti.js"
- }
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
- },
- "node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/jspdf": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
- "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.23.2",
- "atob": "^2.1.2",
- "btoa": "^1.2.1",
- "fflate": "^0.8.1"
- },
- "optionalDependencies": {
- "canvg": "^3.0.6",
- "core-js": "^3.6.0",
- "dompurify": "^2.5.4",
- "html2canvas": "^1.0.0-rc.5"
- }
- },
- "node_modules/jspdf-autotable": {
- "version": "3.8.4",
- "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.8.4.tgz",
- "integrity": "sha512-rSffGoBsJYX83iTRv8Ft7FhqfgEL2nLpGAIiqruEQQ3e4r0qdLFbPUB7N9HAle0I3XgpisvyW751VHCqKUVOgQ==",
- "license": "MIT",
- "peerDependencies": {
- "jspdf": "^2.5.1"
- }
- },
- "node_modules/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
- "dependencies": {
- "json-buffer": "3.0.1"
- }
- },
- "node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/lilconfig": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
- "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/antonk52"
- }
- },
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true
- },
- "node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/lucide-react": {
- "version": "0.469.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz",
- "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==",
- "license": "ISC",
- "peerDependencies": {
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/micromatch": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "dev": true,
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true
- },
- "node_modules/mz": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
- "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "dev": true,
- "dependencies": {
- "any-promise": "^1.0.0",
- "object-assign": "^4.0.1",
- "thenify-all": "^1.0.0"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true
- },
- "node_modules/node-releases": {
- "version": "2.0.18",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
- "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
- "dev": true
- },
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/normalize-range": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
- "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-hash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
- "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "dev": true,
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/optionator": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
- "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "dev": true,
- "dependencies": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.5"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "dev": true
- },
- "node_modules/parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
- },
- "node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "dev": true,
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/path-scurry/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true
- },
- "node_modules/performance-now": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
- "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/pirates": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
- "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
- "dev": true,
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/postcss": {
- "version": "8.4.49",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
- "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/postcss-import": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
- "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
- "dev": true,
- "dependencies": {
- "postcss-value-parser": "^4.0.0",
- "read-cache": "^1.0.0",
- "resolve": "^1.1.7"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "postcss": "^8.0.0"
- }
- },
- "node_modules/postcss-js": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
- "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
- "dev": true,
- "dependencies": {
- "camelcase-css": "^2.0.1"
- },
- "engines": {
- "node": "^12 || ^14 || >= 16"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- "peerDependencies": {
- "postcss": "^8.4.21"
- }
- },
- "node_modules/postcss-load-config": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
- "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "lilconfig": "^3.0.0",
- "yaml": "^2.3.4"
- },
- "engines": {
- "node": ">= 14"
- },
- "peerDependencies": {
- "postcss": ">=8.0.9",
- "ts-node": ">=9.0.0"
- },
- "peerDependenciesMeta": {
- "postcss": {
- "optional": true
- },
- "ts-node": {
- "optional": true
- }
- }
- },
- "node_modules/postcss-nested": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
- "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "postcss-selector-parser": "^6.1.1"
- },
- "engines": {
- "node": ">=12.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.14"
- }
- },
- "node_modules/postcss-selector-parser": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
- "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "dev": true,
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postcss-value-parser": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true
- },
- "node_modules/prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true,
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
- "node_modules/prop-types/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/raf": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
- "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "performance-now": "^2.1.0"
- }
- },
- "node_modules/react": {
- "version": "19.0.0",
- "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
- "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "19.0.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
- "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
- "license": "MIT",
- "dependencies": {
- "scheduler": "^0.25.0"
- },
- "peerDependencies": {
- "react": "^19.0.0"
- }
- },
- "node_modules/react-hot-toast": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
- "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
- "license": "MIT",
- "dependencies": {
- "goober": "^2.1.10"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "react": ">=16",
- "react-dom": ">=16"
- }
- },
- "node_modules/react-is": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
- },
- "node_modules/react-refresh": {
- "version": "0.14.2",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
- "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-router": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.0.tgz",
- "integrity": "sha512-VcFhWqkNIcojDRYaUO8qV0Jib52s9ULpCp3nkBbmrvtoCVFRp6tmk3tJ2w9BZauVctA1YRnJlFYDn9iJRuCpGA==",
- "license": "MIT",
- "dependencies": {
- "@types/cookie": "^0.6.0",
- "cookie": "^1.0.1",
- "set-cookie-parser": "^2.6.0",
- "turbo-stream": "2.4.0"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/react-router-dom": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.0.tgz",
- "integrity": "sha512-F4/nYBC9e4s0/ZjxM8GkZ9a68DpX76LN1a9W9mfPl2GfbDJ9/vzJro6MThNR5qGBH6KkgcK1BziyEzXhHV46Xw==",
- "license": "MIT",
- "dependencies": {
- "react-router": "7.1.0"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- }
- },
- "node_modules/react-smooth": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
- "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
- "license": "MIT",
- "dependencies": {
- "fast-equals": "^5.0.1",
- "prop-types": "^15.8.1",
- "react-transition-group": "^4.4.5"
- },
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/react-transition-group": {
- "version": "4.4.5",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
- "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/runtime": "^7.5.5",
- "dom-helpers": "^5.0.1",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.6.2"
- },
- "peerDependencies": {
- "react": ">=16.6.0",
- "react-dom": ">=16.6.0"
- }
- },
- "node_modules/read-cache": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
- "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
- "dev": true,
- "dependencies": {
- "pify": "^2.3.0"
- }
- },
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/recharts": {
- "version": "2.15.0",
- "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
- "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
- "dependencies": {
- "clsx": "^2.0.0",
- "eventemitter3": "^4.0.1",
- "lodash": "^4.17.21",
- "react-is": "^18.3.1",
- "react-smooth": "^4.0.0",
- "recharts-scale": "^0.4.4",
- "tiny-invariant": "^1.3.1",
- "victory-vendor": "^36.6.8"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/recharts-scale": {
- "version": "0.4.5",
- "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
- "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
- "dependencies": {
- "decimal.js-light": "^2.4.1"
- }
- },
- "node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
- },
- "node_modules/resolve": {
- "version": "1.22.8",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
- "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
- "dev": true,
- "dependencies": {
- "is-core-module": "^2.13.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true,
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
- "node_modules/rgbcolor": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
- "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
- "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
- "optional": true,
- "engines": {
- "node": ">= 0.8.15"
- }
- },
- "node_modules/rollup": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
- "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
- "dev": true,
- "dependencies": {
- "@types/estree": "1.0.6"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.24.0",
- "@rollup/rollup-android-arm64": "4.24.0",
- "@rollup/rollup-darwin-arm64": "4.24.0",
- "@rollup/rollup-darwin-x64": "4.24.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.24.0",
- "@rollup/rollup-linux-arm64-gnu": "4.24.0",
- "@rollup/rollup-linux-arm64-musl": "4.24.0",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.24.0",
- "@rollup/rollup-linux-s390x-gnu": "4.24.0",
- "@rollup/rollup-linux-x64-gnu": "4.24.0",
- "@rollup/rollup-linux-x64-musl": "4.24.0",
- "@rollup/rollup-win32-arm64-msvc": "4.24.0",
- "@rollup/rollup-win32-ia32-msvc": "4.24.0",
- "@rollup/rollup-win32-x64-msvc": "4.24.0",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "dependencies": {
- "queue-microtask": "^1.2.2"
- }
- },
- "node_modules/scheduler": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
- "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
- "license": "MIT"
- },
- "node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/set-cookie-parser": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
- "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
- "license": "MIT"
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/stackblur-canvas": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
- "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=0.1.14"
- }
- },
- "node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "node_modules/string-width-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/sucrase": {
- "version": "3.35.0",
- "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
- "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
- "dev": true,
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.2",
- "commander": "^4.0.0",
- "glob": "^10.3.10",
- "lines-and-columns": "^1.1.6",
- "mz": "^2.7.0",
- "pirates": "^4.0.1",
- "ts-interface-checker": "^0.1.9"
- },
- "bin": {
- "sucrase": "bin/sucrase",
- "sucrase-node": "bin/sucrase-node"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/svg-pathdata": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
- "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/tailwindcss": {
- "version": "3.4.17",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
- "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@alloc/quick-lru": "^5.2.0",
- "arg": "^5.0.2",
- "chokidar": "^3.6.0",
- "didyoumean": "^1.2.2",
- "dlv": "^1.1.3",
- "fast-glob": "^3.3.2",
- "glob-parent": "^6.0.2",
- "is-glob": "^4.0.3",
- "jiti": "^1.21.6",
- "lilconfig": "^3.1.3",
- "micromatch": "^4.0.8",
- "normalize-path": "^3.0.0",
- "object-hash": "^3.0.0",
- "picocolors": "^1.1.1",
- "postcss": "^8.4.47",
- "postcss-import": "^15.1.0",
- "postcss-js": "^4.0.1",
- "postcss-load-config": "^4.0.2",
- "postcss-nested": "^6.2.0",
- "postcss-selector-parser": "^6.1.2",
- "resolve": "^1.22.8",
- "sucrase": "^3.35.0"
- },
- "bin": {
- "tailwind": "lib/cli.js",
- "tailwindcss": "lib/cli.js"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/text-segmentation": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
- "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "utrie": "^1.0.2"
- }
- },
- "node_modules/thenify": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
- "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "dev": true,
- "dependencies": {
- "any-promise": "^1.0.0"
- }
- },
- "node_modules/thenify-all": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
- "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
- "dev": true,
- "dependencies": {
- "thenify": ">= 3.1.0 < 4"
- },
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/tiny-invariant": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
- "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/ts-api-utils": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
- "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=16"
- },
- "peerDependencies": {
- "typescript": ">=4.2.0"
- }
- },
- "node_modules/ts-interface-checker": {
- "version": "0.1.13",
- "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
- "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true
- },
- "node_modules/turbo-stream": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
- "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
- "license": "ISC"
- },
- "node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/typescript": {
- "version": "5.7.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
- "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/typescript-eslint": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.1.tgz",
- "integrity": "sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/eslint-plugin": "8.18.1",
- "@typescript-eslint/parser": "8.18.1",
- "@typescript-eslint/utils": "8.18.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.8.0"
- }
- },
- "node_modules/undici-types": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
- "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/update-browserslist-db": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
- "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.0"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
- "node_modules/use-debounce": {
- "version": "10.0.4",
- "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz",
- "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==",
- "license": "MIT",
- "engines": {
- "node": ">= 16.0.0"
- },
- "peerDependencies": {
- "react": "*"
- }
- },
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true
- },
- "node_modules/utrie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
- "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "base64-arraybuffer": "^1.0.2"
- }
- },
- "node_modules/victory-vendor": {
- "version": "36.9.2",
- "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
- "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
- "dependencies": {
- "@types/d3-array": "^3.0.3",
- "@types/d3-ease": "^3.0.0",
- "@types/d3-interpolate": "^3.0.1",
- "@types/d3-scale": "^4.0.2",
- "@types/d3-shape": "^3.1.0",
- "@types/d3-time": "^3.0.0",
- "@types/d3-timer": "^3.0.0",
- "d3-array": "^3.1.6",
- "d3-ease": "^3.0.1",
- "d3-interpolate": "^3.0.1",
- "d3-scale": "^4.0.2",
- "d3-shape": "^3.1.0",
- "d3-time": "^3.0.0",
- "d3-timer": "^3.0.1"
- }
- },
- "node_modules/vite": {
- "version": "6.0.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.5.tgz",
- "integrity": "sha512-akD5IAH/ID5imgue2DYhzsEwCi0/4VKY31uhMLEYJwPP4TiUp8pL5PIK+Wo7H8qT8JY9i+pVfPydcFPYD1EL7g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "0.24.0",
- "postcss": "^8.4.49",
- "rollup": "^4.23.0"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || ^20.0.0 || >=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
- }
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/word-wrap": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
- "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
- },
- "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "node_modules/wrap-ansi-cjs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi/node_modules/ansi-styles": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
- "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/yaml": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
- "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
- "dev": true,
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/yocto-queue": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- }
- }
-}
+{
+ "name": "investment-portfolio-tracker",
+ "version": "1.2.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "investment-portfolio-tracker",
+ "version": "1.2.0",
+ "dependencies": {
+ "date-fns": "^4.1.0",
+ "jspdf": "^2.5.2",
+ "jspdf-autotable": "^3.8.4",
+ "lucide-react": "^0.469.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-hot-toast": "^2.4.1",
+ "react-router-dom": "^7.1.0",
+ "recharts": "^2.15.0",
+ "use-debounce": "^10.0.4"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.17.0",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.2",
+ "@types/react-dom": "^19.0.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.17.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.16",
+ "globals": "^15.14.0",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2",
+ "typescript-eslint": "^8.18.1",
+ "vite": "^6.0.5"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+ "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz",
+ "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
+ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.26.0",
+ "@babel/generator": "^7.26.0",
+ "@babel/helper-compilation-targets": "^7.25.9",
+ "@babel/helper-module-transforms": "^7.26.0",
+ "@babel/helpers": "^7.26.0",
+ "@babel/parser": "^7.26.0",
+ "@babel/template": "^7.25.9",
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.26.0",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
+ "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.26.3",
+ "@babel/types": "^7.26.3",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
+ "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.25.9",
+ "@babel/helper-validator-option": "^7.25.9",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+ "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
+ "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "@babel/traverse": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz",
+ "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+ "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
+ "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
+ "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.25.9",
+ "@babel/types": "^7.26.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
+ "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.3"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
+ "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
+ "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
+ "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
+ "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.25.9",
+ "@babel/parser": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.26.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
+ "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.26.3",
+ "@babel/parser": "^7.26.3",
+ "@babel/template": "^7.25.9",
+ "@babel/types": "^7.26.3",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
+ "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz",
+ "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz",
+ "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz",
+ "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz",
+ "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz",
+ "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz",
+ "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz",
+ "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz",
+ "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz",
+ "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz",
+ "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz",
+ "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz",
+ "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz",
+ "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz",
+ "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz",
+ "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz",
+ "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz",
+ "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz",
+ "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz",
+ "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz",
+ "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz",
+ "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz",
+ "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz",
+ "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz",
+ "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
+ "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.5",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz",
+ "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
+ "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
+ "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
+ "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz",
+ "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
+ "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+ "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
+ "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
+ "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
+ "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
+ "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
+ "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
+ "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
+ "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
+ "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
+ "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
+ "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
+ "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
+ "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
+ "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
+ "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
+ "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
+ "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.8",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
+ "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.6",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
+ "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
+ "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
+ "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "22.10.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
+ "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
+ },
+ "node_modules/@types/raf": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
+ "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/react": {
+ "version": "19.0.2",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.2.tgz",
+ "integrity": "sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.0.2",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz",
+ "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz",
+ "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.18.1",
+ "@typescript-eslint/type-utils": "8.18.1",
+ "@typescript-eslint/utils": "8.18.1",
+ "@typescript-eslint/visitor-keys": "8.18.1",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "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"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz",
+ "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.18.1",
+ "@typescript-eslint/types": "8.18.1",
+ "@typescript-eslint/typescript-estree": "8.18.1",
+ "@typescript-eslint/visitor-keys": "8.18.1",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz",
+ "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.18.1",
+ "@typescript-eslint/visitor-keys": "8.18.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz",
+ "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.18.1",
+ "@typescript-eslint/utils": "8.18.1",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz",
+ "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz",
+ "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.18.1",
+ "@typescript-eslint/visitor-keys": "8.18.1",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz",
+ "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "8.18.1",
+ "@typescript-eslint/types": "8.18.1",
+ "@typescript-eslint/typescript-estree": "8.18.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz",
+ "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.18.1",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
+ "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.26.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.25.9",
+ "@babel/plugin-transform-react-jsx-source": "^7.25.9",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.14.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "atob": "bin/atob.js"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.20",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+ "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "browserslist": "^4.23.3",
+ "caniuse-lite": "^1.0.30001646",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
+ "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001663",
+ "electron-to-chromium": "^1.5.28",
+ "node-releases": "^2.0.18",
+ "update-browserslist-db": "^1.1.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/btoa": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
+ "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "btoa": "bin/btoa.js"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001667",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
+ "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/canvg": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
+ "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@types/raf": "^3.4.0",
+ "core-js": "^3.8.3",
+ "raf": "^3.4.1",
+ "regenerator-runtime": "^0.13.7",
+ "rgbcolor": "^1.0.1",
+ "stackblur-canvas": "^2.0.0",
+ "svg-pathdata": "^6.0.3"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/canvg/node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/core-js": {
+ "version": "3.39.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
+ "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-line-break": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true
+ },
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/dompurify": {
+ "version": "2.5.8",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
+ "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optional": true
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.33",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz",
+ "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==",
+ "dev": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz",
+ "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.24.0",
+ "@esbuild/android-arm": "0.24.0",
+ "@esbuild/android-arm64": "0.24.0",
+ "@esbuild/android-x64": "0.24.0",
+ "@esbuild/darwin-arm64": "0.24.0",
+ "@esbuild/darwin-x64": "0.24.0",
+ "@esbuild/freebsd-arm64": "0.24.0",
+ "@esbuild/freebsd-x64": "0.24.0",
+ "@esbuild/linux-arm": "0.24.0",
+ "@esbuild/linux-arm64": "0.24.0",
+ "@esbuild/linux-ia32": "0.24.0",
+ "@esbuild/linux-loong64": "0.24.0",
+ "@esbuild/linux-mips64el": "0.24.0",
+ "@esbuild/linux-ppc64": "0.24.0",
+ "@esbuild/linux-riscv64": "0.24.0",
+ "@esbuild/linux-s390x": "0.24.0",
+ "@esbuild/linux-x64": "0.24.0",
+ "@esbuild/netbsd-x64": "0.24.0",
+ "@esbuild/openbsd-arm64": "0.24.0",
+ "@esbuild/openbsd-x64": "0.24.0",
+ "@esbuild/sunos-x64": "0.24.0",
+ "@esbuild/win32-arm64": "0.24.0",
+ "@esbuild/win32-ia32": "0.24.0",
+ "@esbuild/win32-x64": "0.24.0"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.17.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz",
+ "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.19.0",
+ "@eslint/core": "^0.9.0",
+ "@eslint/eslintrc": "^3.2.0",
+ "@eslint/js": "9.17.0",
+ "@eslint/plugin-kit": "^0.2.3",
+ "@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.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.2.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "espree": "^10.3.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "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.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz",
+ "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.16",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz",
+ "integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+ "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+ "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-equals": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
+ "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "dev": true
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
+ "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globals": {
+ "version": "15.14.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
+ "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/goober": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
+ "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
+ "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.6",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
+ "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
+ "dev": true,
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jspdf": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
+ "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "atob": "^2.1.2",
+ "btoa": "^1.2.1",
+ "fflate": "^0.8.1"
+ },
+ "optionalDependencies": {
+ "canvg": "^3.0.6",
+ "core-js": "^3.6.0",
+ "dompurify": "^2.5.4",
+ "html2canvas": "^1.0.0-rc.5"
+ }
+ },
+ "node_modules/jspdf-autotable": {
+ "version": "3.8.4",
+ "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.8.4.tgz",
+ "integrity": "sha512-rSffGoBsJYX83iTRv8Ft7FhqfgEL2nLpGAIiqruEQQ3e4r0qdLFbPUB7N9HAle0I3XgpisvyW751VHCqKUVOgQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "jspdf": "^2.5.1"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.469.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz",
+ "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+ "dev": true
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "node_modules/performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "dev": true,
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/raf": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "performance-now": "^2.1.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
+ "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
+ "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.25.0"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/react-hot-toast": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
+ "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
+ "license": "MIT",
+ "dependencies": {
+ "goober": "^2.1.10"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
+ },
+ "node_modules/react-refresh": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.0.tgz",
+ "integrity": "sha512-VcFhWqkNIcojDRYaUO8qV0Jib52s9ULpCp3nkBbmrvtoCVFRp6tmk3tJ2w9BZauVctA1YRnJlFYDn9iJRuCpGA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cookie": "^0.6.0",
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0",
+ "turbo-stream": "2.4.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.0.tgz",
+ "integrity": "sha512-F4/nYBC9e4s0/ZjxM8GkZ9a68DpX76LN1a9W9mfPl2GfbDJ9/vzJro6MThNR5qGBH6KkgcK1BziyEzXhHV46Xw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-smooth": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
+ "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-equals": "^5.0.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/recharts": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
+ "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
+ "dependencies": {
+ "clsx": "^2.0.0",
+ "eventemitter3": "^4.0.1",
+ "lodash": "^4.17.21",
+ "react-is": "^18.3.1",
+ "react-smooth": "^4.0.0",
+ "recharts-scale": "^0.4.4",
+ "tiny-invariant": "^1.3.1",
+ "victory-vendor": "^36.6.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/recharts-scale": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
+ "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
+ "dependencies": {
+ "decimal.js-light": "^2.4.1"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
+ },
+ "node_modules/resolve": {
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rgbcolor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+ "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+ "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.8.15"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
+ "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.24.0",
+ "@rollup/rollup-android-arm64": "4.24.0",
+ "@rollup/rollup-darwin-arm64": "4.24.0",
+ "@rollup/rollup-darwin-x64": "4.24.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.24.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.24.0",
+ "@rollup/rollup-linux-arm64-musl": "4.24.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.24.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.24.0",
+ "@rollup/rollup-linux-x64-gnu": "4.24.0",
+ "@rollup/rollup-linux-x64-musl": "4.24.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.24.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.24.0",
+ "@rollup/rollup-win32-x64-msvc": "4.24.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
+ "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackblur-canvas": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+ "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.1.14"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "^10.3.10",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/svg-pathdata": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+ "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.6",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/text-segmentation": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true
+ },
+ "node_modules/turbo-stream": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
+ "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
+ "license": "ISC"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.1.tgz",
+ "integrity": "sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.18.1",
+ "@typescript-eslint/parser": "8.18.1",
+ "@typescript-eslint/utils": "8.18.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.8.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+ "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-debounce": {
+ "version": "10.0.4",
+ "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz",
+ "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "node_modules/utrie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "36.9.2",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
+ "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.5.tgz",
+ "integrity": "sha512-akD5IAH/ID5imgue2DYhzsEwCi0/4VKY31uhMLEYJwPP4TiUp8pL5PIK+Wo7H8qT8JY9i+pVfPydcFPYD1EL7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "0.24.0",
+ "postcss": "^8.4.49",
+ "rollup": "^4.23.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=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
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
+ "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
+ "dev": true,
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 26b406c..bb788d0 100644
--- a/package.json
+++ b/package.json
@@ -1,41 +1,41 @@
-{
- "name": "investment-portfolio-tracker",
- "private": true,
- "version": "1.2.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "lint": "eslint .",
- "preview": "vite preview"
- },
- "dependencies": {
- "date-fns": "^4.1.0",
- "jspdf": "^2.5.2",
- "jspdf-autotable": "^3.8.4",
- "lucide-react": "^0.469.0",
- "react": "^19.0.0",
- "react-dom": "^19.0.0",
- "react-hot-toast": "^2.4.1",
- "react-router-dom": "^7.1.0",
- "recharts": "^2.15.0",
- "use-debounce": "^10.0.4"
- },
- "devDependencies": {
- "@eslint/js": "^9.17.0",
- "@types/node": "^22.10.2",
- "@types/react": "^19.0.2",
- "@types/react-dom": "^19.0.2",
- "@vitejs/plugin-react": "^4.3.4",
- "autoprefixer": "^10.4.20",
- "eslint": "^9.17.0",
- "eslint-plugin-react-hooks": "^5.1.0",
- "eslint-plugin-react-refresh": "^0.4.16",
- "globals": "^15.14.0",
- "postcss": "^8.4.49",
- "tailwindcss": "^3.4.17",
- "typescript": "^5.7.2",
- "typescript-eslint": "^8.18.1",
- "vite": "^6.0.5"
- }
-}
+{
+ "name": "investment-portfolio-tracker",
+ "private": true,
+ "version": "1.2.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "date-fns": "^4.1.0",
+ "jspdf": "^2.5.2",
+ "jspdf-autotable": "^3.8.4",
+ "lucide-react": "^0.469.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-hot-toast": "^2.4.1",
+ "react-router-dom": "^7.1.0",
+ "recharts": "^2.15.0",
+ "use-debounce": "^10.0.4"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.17.0",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.2",
+ "@types/react-dom": "^19.0.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.17.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.16",
+ "globals": "^15.14.0",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.17",
+ "typescript": "^5.7.2",
+ "typescript-eslint": "^8.18.1",
+ "vite": "^6.0.5"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
index 2aa7205..daedffd 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,6 +1,6 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-};
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/src/App.tsx b/src/App.tsx
index 2c124ce..26237e8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,26 +1,39 @@
-import { lazy, Suspense, useState } from "react";
-import { Toaster } from "react-hot-toast";
-
-import { AppShell } from "./components/Landing/AppShell";
-import { LoadingPlaceholder } from "./components/utils/LoadingPlaceholder";
-import { PortfolioProvider } from "./providers/PortfolioProvider";
-
-const MainContent = lazy(() => import("./components/Landing/MainContent"));
-
-export default function App() {
- const [isAddingAsset, setIsAddingAsset] = useState(false);
-
- return (
-
- setIsAddingAsset(true)}>
- }>
-
-
-
-
-
- );
-}
+import { lazy, Suspense, useState } from "react";
+import { Toaster } from "react-hot-toast";
+
+import { AppShell } from "./components/Landing/AppShell";
+import { LoadingPlaceholder } from "./components/utils/LoadingPlaceholder";
+import StockExplorer from "./pages/StockExplorer";
+import { PortfolioProvider } from "./providers/PortfolioProvider";
+
+const MainContent = lazy(() => import("./components/Landing/MainContent"));
+
+function Root() {
+ const [isAddingAsset, setIsAddingAsset] = useState(false);
+
+ return (
+
+ setIsAddingAsset(true)}>
+ }>
+
+
+
+
+
+ );
+}
+
+// Export the routes configuration that will be used in main.tsx
+export default [
+ {
+ path: '/',
+ element:
+ },
+ {
+ path: '/explore',
+ element:
+ }
+];
diff --git a/src/components/Chart/AssetPerformanceModal.tsx b/src/components/Chart/AssetPerformanceModal.tsx
index f6a93f8..26df374 100644
--- a/src/components/Chart/AssetPerformanceModal.tsx
+++ b/src/components/Chart/AssetPerformanceModal.tsx
@@ -1,129 +1,129 @@
-import { X } from "lucide-react";
-import { memo, useEffect } from "react";
-import {
- CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
-} from "recharts";
-
-interface AssetPerformanceModalProps {
- assetName: string;
- performances: { year: number; percentage: number; price?: number }[];
- onClose: () => void;
-}
-
-export const AssetPerformanceModal = memo(({ assetName, performances, onClose }: AssetPerformanceModalProps) => {
- const sortedPerformances = [...performances].sort((a, b) => a.year - b.year);
-
- // Prevent body scroll when modal is open
- useEffect(() => {
- document.body.style.overflow = 'hidden';
- return () => {
- document.body.style.overflow = 'unset';
- };
- }, []);
-
- const CustomizedDot = (props: any) => {
- const { cx, cy, payload } = props;
- return (
- = 0 ? '#22c55e' : '#ef4444'}
- />
- );
- };
-
- return (
-
-
- {/* Header - Fixed */}
-
-
{assetName} - Yearly Performance
-
-
-
-
-
- {/* Content - Scrollable */}
-
- {/* Chart */}
-
-
-
-
-
- `${value.toFixed(2)}%`}
- />
- `β¬${value.toFixed(2)}`}
- />
- {
- if (name === 'Performance') return [`${value.toFixed(2)}%`, name];
- return [`β¬${value.toFixed(2)}`, name];
- }}
- labelFormatter={(year) => `Year ${year}`}
- />
- }
- strokeWidth={2}
- yAxisId="left"
- />
-
-
-
-
-
-
-
-
-
-
-
- {/* Performance Cards */}
-
- {sortedPerformances.map(({ year, percentage, price }) => (
-
= 0 ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'
- }`}
- >
-
{year}
-
= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
- }`}>
- {percentage.toFixed(2)}%
-
- {price && (
-
- β¬{price.toFixed(2)}
-
- )}
-
- ))}
-
-
-
-
- );
-});
+import { X } from "lucide-react";
+import { memo, useEffect } from "react";
+import {
+ CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
+} from "recharts";
+
+interface AssetPerformanceModalProps {
+ assetName: string;
+ performances: { year: number; percentage: number; price?: number }[];
+ onClose: () => void;
+}
+
+export const AssetPerformanceModal = memo(({ assetName, performances, onClose }: AssetPerformanceModalProps) => {
+ const sortedPerformances = [...performances].sort((a, b) => a.year - b.year);
+
+ // Prevent body scroll when modal is open
+ useEffect(() => {
+ document.body.style.overflow = 'hidden';
+ return () => {
+ document.body.style.overflow = 'unset';
+ };
+ }, []);
+
+ const CustomizedDot = (props: any) => {
+ const { cx, cy, payload } = props;
+ return (
+ = 0 ? '#22c55e' : '#ef4444'}
+ />
+ );
+ };
+
+ return (
+
+
+ {/* Header - Fixed */}
+
+
{assetName} - Yearly Performance
+
+
+
+
+
+ {/* Content - Scrollable */}
+
+ {/* Chart */}
+
+
+
+
+
+ `${value.toFixed(2)}%`}
+ />
+ `β¬${value.toFixed(2)}`}
+ />
+ {
+ if (name === 'Performance') return [`${value.toFixed(2)}%`, name];
+ return [`β¬${value.toFixed(2)}`, name];
+ }}
+ labelFormatter={(year) => `Year ${year}`}
+ />
+ }
+ strokeWidth={2}
+ yAxisId="left"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {/* Performance Cards */}
+
+ {sortedPerformances.map(({ year, percentage, price }) => (
+
= 0 ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'
+ }`}
+ >
+
{year}
+
= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
+ }`}>
+ {percentage.toFixed(2)}%
+
+ {price && (
+
+ β¬{price.toFixed(2)}
+
+ )}
+
+ ))}
+
+
+
+
+ );
+});
diff --git a/src/components/Chart/ChartContent.tsx b/src/components/Chart/ChartContent.tsx
index 8b3677b..dfdae2b 100644
--- a/src/components/Chart/ChartContent.tsx
+++ b/src/components/Chart/ChartContent.tsx
@@ -1,184 +1,187 @@
-import { format } from "date-fns";
-import { Maximize2, RefreshCcw } from "lucide-react";
-import { memo } from "react";
-import {
- CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
-} from "recharts";
-
-import { Asset, DateRange } from "../../types";
-import { DateRangePicker } from "../utils/DateRangePicker";
-import { ChartLegend } from "./ChartLegend";
-
-interface ChartContentProps {
- dateRange: DateRange;
- handleUpdateDateRange: (range: DateRange) => void;
- handleReRender: () => void;
- isFullscreen: boolean;
- setIsFullscreen: (value: boolean) => void;
- renderKey: number;
- isDarkMode: boolean;
- hideAssets: boolean;
- hiddenAssets: Set;
- processedData: any[];
- assets: Asset[];
- assetColors: Record;
- toggleAsset: (assetId: string) => void;
- toggleAllAssets: () => void;
-}
-
-export const ChartContent = memo(({
- dateRange,
- handleUpdateDateRange,
- handleReRender,
- isFullscreen,
- setIsFullscreen,
- renderKey,
- isDarkMode,
- hideAssets,
- hiddenAssets,
- processedData,
- assets,
- assetColors,
- toggleAsset,
- toggleAllAssets
-}: ChartContentProps) => (
- <>
-
-
handleUpdateDateRange({ ...dateRange, startDate: date })}
- onEndDateChange={(date) => handleUpdateDateRange({ ...dateRange, endDate: date })}
- />
-
-
-
-
- setIsFullscreen(!isFullscreen)}
- className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded hover:text-blue-500"
- >
-
-
-
-
-
-
-
-
- format(new Date(date), 'dd.MM.yyyy')}
- />
- `${value.toFixed(2)}β¬`}
- />
- `${value.toFixed(2)}%`}
- />
- {
- const assetKey = name.split('_')[0] as keyof typeof assets;
- const processedKey = `${assets.find(a => a.name === name.replace(" (%)", ""))?.id}_price`;
-
- if (name === "avg. Portfolio % gain")
- return [`${value.toFixed(2)}%`, name];
-
- if (name === "TTWOR")
- return [`${value.toLocaleString()}β¬ (${item.payload["ttwor_percent"].toFixed(2)}%)`, name];
-
- if (name === "Portfolio-Value" || name === "Invested Capital")
- return [`${value.toLocaleString()}β¬`, name];
-
- if (name.includes("(%)"))
- return [`${Number(item.payload[processedKey]).toFixed(2)}β¬ ${value.toFixed(2)}%`, name.replace(" (%)", "")];
-
- return [`${value.toLocaleString()}β¬ (${((value - Number(assets[assetKey])) / Number(assets[assetKey]) * 100).toFixed(2)}%)`, name];
- }}
- labelFormatter={(date) => format(new Date(date), 'dd.MM.yyyy')}
- />
- } />
-
-
-
- {assets.map((asset) => (
-
- ))}
-
-
-
-
-
- *Note: The YAxis on the left shows the value of your portfolio (black line) and invested capital (dotted line),
- all other assets are scaled by their % gain/loss and thus scaled to the right YAxis.
-
-
- **Note: The % is based on daily weighted average data, thus the percentages might alter slightly.
-
- >
-));
+import { format } from "date-fns";
+import { Maximize2, RefreshCcw } from "lucide-react";
+import {
+ CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
+} from "recharts";
+
+import { Asset, DateRange } from "../../types";
+import { DateRangePicker } from "../utils/DateRangePicker";
+import { ChartLegend } from "./ChartLegend";
+
+interface ChartContentProps {
+ dateRange: DateRange;
+ handleUpdateDateRange: (range: DateRange) => void;
+ handleReRender: () => void;
+ isFullscreen: boolean;
+ setIsFullscreen: (value: boolean) => void;
+ renderKey: number;
+ isDarkMode: boolean;
+ hideAssets: boolean;
+ hiddenAssets: Set;
+ processedData: any[];
+ assets: Asset[];
+ assetColors: Record;
+ toggleAsset: (assetId: string) => void;
+ toggleAllAssets: () => void;
+ removeAsset?: (assetId: string) => void;
+}
+
+export const ChartContent = ({
+ dateRange,
+ handleUpdateDateRange,
+ handleReRender,
+ isFullscreen,
+ setIsFullscreen,
+ renderKey,
+ isDarkMode,
+ hideAssets,
+ hiddenAssets,
+ processedData,
+ assets,
+ assetColors,
+ toggleAsset,
+ toggleAllAssets,
+ removeAsset
+}: ChartContentProps) => (
+ <>
+
+
handleUpdateDateRange({ ...dateRange, startDate: date })}
+ onEndDateChange={(date) => handleUpdateDateRange({ ...dateRange, endDate: date })}
+ />
+
+
+
+
+ setIsFullscreen(!isFullscreen)}
+ className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded hover:text-blue-500"
+ >
+
+
+
+
+
+
+
+
+ format(new Date(date), 'dd.MM.yyyy')}
+ />
+ `${value.toFixed(2)}β¬`}
+ />
+ `${value.toFixed(2)}%`}
+ />
+ {
+ const assetKey = name.split('_')[0] as keyof typeof assets;
+ const processedKey = `${assets.find(a => a.name === name.replace(" (%)", ""))?.id}_price`;
+
+ if (name === "avg. Portfolio % gain")
+ return [`${value.toFixed(2)}%`, name];
+
+ if (name === "TTWOR")
+ return [`${value.toLocaleString()}β¬ (${item.payload["ttwor_percent"].toFixed(2)}%)`, name];
+
+ if (name === "Portfolio-Value" || name === "Invested Capital")
+ return [`${value.toLocaleString()}β¬`, name];
+
+ if (name.includes("(%)"))
+ return [`${Number(item.payload[processedKey]).toFixed(2)}β¬ ${value.toFixed(2)}%`, name.replace(" (%)", "")];
+
+ return [`${value.toLocaleString()}β¬ (${((value - Number(assets[assetKey])) / Number(assets[assetKey]) * 100).toFixed(2)}%)`, name];
+ }}
+ labelFormatter={(date) => format(new Date(date), 'dd.MM.yyyy')}
+ />
+ } />
+
+
+
+ {assets.map((asset) => (
+
+ ))}
+
+
+
+
+
+ *Note: The YAxis on the left shows the value of your portfolio (black line) and invested capital (dotted line),
+ all other assets are scaled by their % gain/loss and thus scaled to the right YAxis.
+
+
+ **Note: The % is based on daily weighted average data, thus the percentages might alter slightly.
+
+ >
+);
diff --git a/src/components/Chart/ChartLegend.tsx b/src/components/Chart/ChartLegend.tsx
index cb282d1..ef1f330 100644
--- a/src/components/Chart/ChartLegend.tsx
+++ b/src/components/Chart/ChartLegend.tsx
@@ -1,66 +1,83 @@
-import { BarChart2, Eye, EyeOff } from "lucide-react";
-import { memo } from "react";
-
-interface ChartLegendProps {
- payload: any[];
- hideAssets: boolean;
- hiddenAssets: Set;
- toggleAsset: (assetId: string) => void;
- toggleAllAssets: () => void;
-}
-
-export const ChartLegend = memo(({ payload, hideAssets, hiddenAssets, toggleAsset, toggleAllAssets }: ChartLegendProps) => {
- return (
-
-
-
-
- Chart Legend
-
-
- {hideAssets ? (
- <>
-
- Show All
- >
- ) : (
- <>
-
- Hide All
- >
- )}
-
-
-
- {payload.map((entry: any, index: number) => {
- const assetId = entry.dataKey.split('_')[0];
- const isHidden = hideAssets || hiddenAssets.has(assetId);
- return (
-
toggleAsset(assetId)}
- className={`flex items-center gap-2 px-2 py-1 rounded transition-opacity duration-200 ${isHidden ? 'opacity-40' : ''
- } hover:bg-gray-100 dark:hover:bg-gray-800`}
- >
-
-
-
{entry.value.replace(' (%)', '')}
- {isHidden ? (
-
- ) : (
-
- )}
-
-
- );
- })}
-
-
- );
-});
+import { BarChart2, Eye, EyeOff, Trash2 } from "lucide-react";
+import { memo } from "react";
+
+interface ChartLegendProps {
+ payload: any[];
+ hideAssets: boolean;
+ hiddenAssets: Set;
+ toggleAsset: (assetId: string) => void;
+ toggleAllAssets: () => void;
+ removeAsset?: (assetId: string) => void;
+}
+
+export const ChartLegend = memo(({ payload, hideAssets, hiddenAssets, toggleAsset, toggleAllAssets, removeAsset }: ChartLegendProps) => {
+ return (
+
+
+
+
+ Chart Legend
+
+
+ {hideAssets ? (
+ <>
+
+ Show All
+ >
+ ) : (
+ <>
+
+ Hide All
+ >
+ )}
+
+
+
+ {payload.map((entry: any, index: number) => {
+ const assetId = entry.dataKey.split('_')[0];
+ const isHidden = hideAssets || hiddenAssets.has(assetId);
+ return (
+
+
toggleAsset(assetId)}
+ className={`flex items-center gap-2 px-2 py-1 rounded transition-opacity duration-200 ${
+ isHidden ? 'opacity-40' : ''
+ } hover:bg-gray-100 dark:hover:bg-gray-800`}
+ >
+
+
+
{entry.value.replace(' (%)', '')}
+ {isHidden ? (
+
+ ) : (
+
+ )}
+
+
+
+ {removeAsset && !['total', 'invested', 'percentageChange', 'ttwor'].includes(assetId) && (
+
{
+ if (confirm(`Are you sure you want to remove ${entry.value.replace(' (%)', '')}?`)) {
+ removeAsset(assetId);
+ }
+ }}
+ className="p-1 ml-1 text-red-500 hover:bg-red-100 dark:hover:bg-red-900/30 rounded"
+ title="Remove asset"
+ >
+
+
+ )}
+
+ );
+ })}
+
+
+ );
+});
diff --git a/src/components/Chart/PortfolioPerformanceModal.tsx b/src/components/Chart/PortfolioPerformanceModal.tsx
index 1cabfeb..5a00778 100644
--- a/src/components/Chart/PortfolioPerformanceModal.tsx
+++ b/src/components/Chart/PortfolioPerformanceModal.tsx
@@ -1,114 +1,114 @@
-import { X } from "lucide-react";
-import { memo, useEffect } from "react";
-import {
- CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
-} from "recharts";
-
-interface PortfolioPerformanceModalProps {
- performances: { year: number; percentage: number; }[];
- onClose: () => void;
-}
-
-export const PortfolioPerformanceModal = memo(({ performances, onClose }: PortfolioPerformanceModalProps) => {
- const sortedPerformances = [...performances].sort((a, b) => a.year - b.year);
-
- // Prevent body scroll when modal is open
- useEffect(() => {
- document.body.style.overflow = 'hidden';
- return () => {
- document.body.style.overflow = 'unset';
- };
- }, []);
-
- const CustomizedDot = (props: any) => {
- const { cx, cy, payload } = props;
- return (
- = 0 ? '#22c55e' : '#ef4444'}
- />
- );
- };
-
- return (
-
-
- {/* Header - Fixed */}
-
-
Portfolio Performance History
-
-
-
-
-
- {/* Content - Scrollable */}
-
- {/* Chart */}
-
-
-
-
-
- `${value.toFixed(2)}%`}
- />
- `β¬${value.toLocaleString()}`}
- />
- {
- if (name === 'Performance') return [`${value.toFixed(2)}%`, name];
- return [`β¬${value.toLocaleString()}`, 'Portfolio Value'];
- }}
- labelFormatter={(year) => `Year ${year}`}
- />
- }
- strokeWidth={2}
- yAxisId="left"
- />
-
-
-
-
-
-
-
-
-
-
- {/* Performance Cards */}
-
- {sortedPerformances.map(({ year, percentage }) => (
-
= 0 ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'
- }`}
- >
-
{year}
-
= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
- }`}>
- {percentage.toFixed(2)}%
-
-
- ))}
-
-
-
-
- );
-});
+import { X } from "lucide-react";
+import { memo, useEffect } from "react";
+import {
+ CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
+} from "recharts";
+
+interface PortfolioPerformanceModalProps {
+ performances: { year: number; percentage: number; }[];
+ onClose: () => void;
+}
+
+export const PortfolioPerformanceModal = memo(({ performances, onClose }: PortfolioPerformanceModalProps) => {
+ const sortedPerformances = [...performances].sort((a, b) => a.year - b.year);
+
+ // Prevent body scroll when modal is open
+ useEffect(() => {
+ document.body.style.overflow = 'hidden';
+ return () => {
+ document.body.style.overflow = 'unset';
+ };
+ }, []);
+
+ const CustomizedDot = (props: any) => {
+ const { cx, cy, payload } = props;
+ return (
+ = 0 ? '#22c55e' : '#ef4444'}
+ />
+ );
+ };
+
+ return (
+
+
+ {/* Header - Fixed */}
+
+
Portfolio Performance History
+
+
+
+
+
+ {/* Content - Scrollable */}
+
+ {/* Chart */}
+
+
+
+
+
+ `${value.toFixed(2)}%`}
+ />
+ `β¬${value.toLocaleString()}`}
+ />
+ {
+ if (name === 'Performance') return [`${value.toFixed(2)}%`, name];
+ return [`β¬${value.toLocaleString()}`, 'Portfolio Value'];
+ }}
+ labelFormatter={(year) => `Year ${year}`}
+ />
+ }
+ strokeWidth={2}
+ yAxisId="left"
+ />
+
+
+
+
+
+
+
+
+
+
+ {/* Performance Cards */}
+
+ {sortedPerformances.map(({ year, percentage }) => (
+
= 0 ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'
+ }`}
+ >
+
{year}
+
= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
+ }`}>
+ {percentage.toFixed(2)}%
+
+
+ ))}
+
+
+
+
+ );
+});
diff --git a/src/components/InvestmentForm.tsx b/src/components/InvestmentForm.tsx
index 5c803cf..f73c9b6 100644
--- a/src/components/InvestmentForm.tsx
+++ b/src/components/InvestmentForm.tsx
@@ -1,331 +1,331 @@
-import { Loader2 } from "lucide-react";
-import React, { memo, useState } from "react";
-import toast from "react-hot-toast";
-
-import { useLocaleDateFormat } from "../hooks/useLocalDateFormat";
-import { usePortfolioSelector } from "../hooks/usePortfolio";
-import { generatePeriodicInvestments } from "../utils/calculations/assetValue";
-
-export default function InvestmentFormWrapper() {
- const { assets, clearAssets } = usePortfolioSelector((state) => ({
- assets: state.assets,
- clearAssets: state.clearAssets,
- }));
- const [selectedAsset, setSelectedAsset] = useState(null);
-
- const handleClearAssets = () => {
- if (window.confirm('Are you sure you want to delete all assets? This action cannot be undone.')) {
- clearAssets();
- setSelectedAsset(null);
- }
- };
-
- return (
-
-
-
-
Add Investment
- {assets.length > 0 && (
-
- Clear Assets
-
- )}
-
-
- setSelectedAsset(e.target.value)}
- className={`w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300 ${assets.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
- >
- Select Asset
- {assets.map((asset) => (
-
- {asset.name}
-
- ))}
-
-
-
- {
- selectedAsset && (
-
- )
- }
-
- );
-}
-
-interface IntervalConfig {
- value: number;
- unit: 'days' | 'months' | 'years';
-}
-
-const InvestmentForm = memo(({ assetId }: { assetId: string }) => {
- const [type, setType] = useState<'single' | 'periodic'>('single');
- const [amount, setAmount] = useState('');
- const [date, setDate] = useState('');
- const [dayOfMonth, setDayOfMonth] = useState('1');
- const [isDynamic, setIsDynamic] = useState(false);
- const [dynamicType, setDynamicType] = useState<'percentage' | 'fixed'>('percentage');
- const [dynamicValue, setDynamicValue] = useState('');
- const [yearInterval, setYearInterval] = useState('1');
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [intervalConfig, setIntervalConfig] = useState({
- value: 1,
- unit: 'months'
- });
- const [showIntervalWarning, setShowIntervalWarning] = useState(false);
-
- const localeDateFormat = useLocaleDateFormat();
-
- const { dateRange, addInvestment } = usePortfolioSelector((state) => ({
- dateRange: state.dateRange,
- addInvestment: state.addInvestment,
- }));
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- e.stopPropagation();
-
- setIsSubmitting(true);
-
- setTimeout(() => {
- try {
- if (type === "single") {
- const investment = {
- id: crypto.randomUUID(),
- assetId,
- type,
- amount: parseFloat(amount),
- date: new Date(date),
- };
- addInvestment(assetId, investment);
- toast.success('Investment added successfully');
- } else {
- const periodicSettings = {
- startDate: new Date(date),
- dayOfMonth: parseInt(dayOfMonth),
- interval: intervalConfig.value,
- amount: parseFloat(amount),
- intervalUnit: intervalConfig.unit,
- ...(isDynamic ? {
- dynamic: {
- type: dynamicType,
- value: parseFloat(dynamicValue),
- yearInterval: parseInt(yearInterval),
- },
- } : undefined),
- };
-
- const investments = generatePeriodicInvestments(
- periodicSettings,
- new Date(dateRange.endDate),
- assetId
- );
- addInvestment(assetId, investments);
-
- toast.success('Sparplan erfolgreich erstellt');
- }
- } catch (error:any) {
- toast.error('Fehler beim Erstellen des Investments: ' + String(error?.message || error));
- } finally {
- setIsSubmitting(false);
- setAmount('');
- }
- }, 10);
- };
-
- const handleIntervalUnitChange = (unit: IntervalConfig['unit']) => {
- setIntervalConfig(prev => ({
- ...prev,
- unit
- }));
-
- setShowIntervalWarning(['days', 'weeks'].includes(unit));
- };
-
- return (
-
- );
-});
+import { Loader2 } from "lucide-react";
+import React, { memo, useState } from "react";
+import toast from "react-hot-toast";
+
+import { useLocaleDateFormat } from "../hooks/useLocalDateFormat";
+import { usePortfolioSelector } from "../hooks/usePortfolio";
+import { generatePeriodicInvestments } from "../utils/calculations/assetValue";
+
+export default function InvestmentFormWrapper() {
+ const { assets, clearAssets } = usePortfolioSelector((state) => ({
+ assets: state.assets,
+ clearAssets: state.clearAssets,
+ }));
+ const [selectedAsset, setSelectedAsset] = useState(null);
+
+ const handleClearAssets = () => {
+ if (window.confirm('Are you sure you want to delete all assets? This action cannot be undone.')) {
+ clearAssets();
+ setSelectedAsset(null);
+ }
+ };
+
+ return (
+
+
+
+
Add Investment
+ {assets.length > 0 && (
+
+ Clear Assets
+
+ )}
+
+
+ setSelectedAsset(e.target.value)}
+ className={`w-full p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300 ${assets.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
+ >
+ Select Asset
+ {assets.map((asset) => (
+
+ {asset.name}
+
+ ))}
+
+
+
+ {
+ selectedAsset && (
+
+ )
+ }
+
+ );
+}
+
+interface IntervalConfig {
+ value: number;
+ unit: 'days' | 'months' | 'years';
+}
+
+const InvestmentForm = memo(({ assetId }: { assetId: string }) => {
+ const [type, setType] = useState<'single' | 'periodic'>('single');
+ const [amount, setAmount] = useState('');
+ const [date, setDate] = useState('');
+ const [dayOfMonth, setDayOfMonth] = useState('1');
+ const [isDynamic, setIsDynamic] = useState(false);
+ const [dynamicType, setDynamicType] = useState<'percentage' | 'fixed'>('percentage');
+ const [dynamicValue, setDynamicValue] = useState('');
+ const [yearInterval, setYearInterval] = useState('1');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [intervalConfig, setIntervalConfig] = useState({
+ value: 1,
+ unit: 'months'
+ });
+ const [showIntervalWarning, setShowIntervalWarning] = useState(false);
+
+ const localeDateFormat = useLocaleDateFormat();
+
+ const { dateRange, addInvestment } = usePortfolioSelector((state) => ({
+ dateRange: state.dateRange,
+ addInvestment: state.addInvestment,
+ }));
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ setIsSubmitting(true);
+
+ setTimeout(() => {
+ try {
+ if (type === "single") {
+ const investment = {
+ id: crypto.randomUUID(),
+ assetId,
+ type,
+ amount: parseFloat(amount),
+ date: new Date(date),
+ };
+ addInvestment(assetId, investment);
+ toast.success('Investment added successfully');
+ } else {
+ const periodicSettings = {
+ startDate: new Date(date),
+ dayOfMonth: parseInt(dayOfMonth),
+ interval: intervalConfig.value,
+ amount: parseFloat(amount),
+ intervalUnit: intervalConfig.unit,
+ ...(isDynamic ? {
+ dynamic: {
+ type: dynamicType,
+ value: parseFloat(dynamicValue),
+ yearInterval: parseInt(yearInterval),
+ },
+ } : undefined),
+ };
+
+ const investments = generatePeriodicInvestments(
+ periodicSettings,
+ new Date(dateRange.endDate),
+ assetId
+ );
+ addInvestment(assetId, investments);
+
+ toast.success('Sparplan erfolgreich erstellt');
+ }
+ } catch (error:any) {
+ toast.error('Fehler beim Erstellen des Investments: ' + String(error?.message || error));
+ } finally {
+ setIsSubmitting(false);
+ setAmount('');
+ }
+ }, 10);
+ };
+
+ const handleIntervalUnitChange = (unit: IntervalConfig['unit']) => {
+ setIntervalConfig(prev => ({
+ ...prev,
+ unit
+ }));
+
+ setShowIntervalWarning(['days', 'weeks'].includes(unit));
+ };
+
+ return (
+
+ );
+});
diff --git a/src/components/Landing/AppShell.tsx b/src/components/Landing/AppShell.tsx
index 6992a7b..85c7090 100644
--- a/src/components/Landing/AppShell.tsx
+++ b/src/components/Landing/AppShell.tsx
@@ -1,55 +1,63 @@
-import { Heart, Moon, Plus, Sun } from "lucide-react";
-import React from "react";
-
-import { useDarkMode } from "../../hooks/useDarkMode";
-
-interface AppShellProps {
- children: React.ReactNode;
- onAddAsset: () => void;
-}
-
-export const AppShell = ({ children, onAddAsset }: AppShellProps) => {
- const { isDarkMode, toggleDarkMode } = useDarkMode();
-
- return (
-
- );
-};
+import { BarChart2, Heart, Moon, Plus, Sun } from "lucide-react";
+import React from "react";
+import { Link } from "react-router-dom";
+
+import { useDarkMode } from "../../hooks/useDarkMode";
+
+interface AppShellProps {
+ children: React.ReactNode;
+ onAddAsset: () => void;
+}
+
+export const AppShell = ({ children, onAddAsset }: AppShellProps) => {
+ const { isDarkMode, toggleDarkMode } = useDarkMode();
+
+ return (
+
+
+
+
+
Portfolio Simulator
+
+
+ {isDarkMode ? (
+
+ ) : (
+
+ )}
+
+
+
+ Add Asset
+
+
+
+ Stock Explorer
+
+
+
+ {children}
+
+
+
+ Built with by Tomato6966
+
+
+
+ );
+};
diff --git a/src/components/Landing/MainContent.tsx b/src/components/Landing/MainContent.tsx
index 79fca00..bea0d8a 100644
--- a/src/components/Landing/MainContent.tsx
+++ b/src/components/Landing/MainContent.tsx
@@ -1,37 +1,37 @@
-import { lazy, Suspense } from "react";
-
-import { LoadingPlaceholder } from "../utils/LoadingPlaceholder";
-
-const AddAssetModal = lazy(() => import("../Modals/AddAssetModal"));
-const InvestmentFormWrapper = lazy(() => import("../InvestmentForm"));
-const PortfolioChart = lazy(() => import("../PortfolioChart"));
-const PortfolioTable = lazy(() => import("../PortfolioTable"));
-
-
-export default function MainContent({ isAddingAsset, setIsAddingAsset }: { isAddingAsset: boolean, setIsAddingAsset: (value: boolean) => void }) {
- return (
- <>
-
- }>
-
-
-
- {isAddingAsset && (
-
- setIsAddingAsset(false)} />
-
- )}
- >
- );
-};
+import { lazy, Suspense } from "react";
+
+import { LoadingPlaceholder } from "../utils/LoadingPlaceholder";
+
+const AddAssetModal = lazy(() => import("../Modals/AddAssetModal"));
+const InvestmentFormWrapper = lazy(() => import("../InvestmentForm"));
+const PortfolioChart = lazy(() => import("../PortfolioChart"));
+const PortfolioTable = lazy(() => import("../PortfolioTable"));
+
+
+export default function MainContent({ isAddingAsset, setIsAddingAsset }: { isAddingAsset: boolean, setIsAddingAsset: (value: boolean) => void }) {
+ return (
+ <>
+
+ }>
+
+
+
+ {isAddingAsset && (
+
+ setIsAddingAsset(false)} />
+
+ )}
+ >
+ );
+};
diff --git a/src/components/Modals/AddAssetModal.tsx b/src/components/Modals/AddAssetModal.tsx
index b762a7a..cf79431 100644
--- a/src/components/Modals/AddAssetModal.tsx
+++ b/src/components/Modals/AddAssetModal.tsx
@@ -1,136 +1,139 @@
-import { Loader2, Search, X } from "lucide-react";
-import { useState } from "react";
-import toast from "react-hot-toast";
-import { useDebouncedCallback } from "use-debounce";
-
-import { usePortfolioSelector } from "../../hooks/usePortfolio";
-import { EQUITY_TYPES, getHistoricalData, searchAssets } from "../../services/yahooFinanceService";
-import { Asset } from "../../types";
-
-export default function AddAssetModal({ onClose }: { onClose: () => void }) {
- const [ search, setSearch ] = useState('');
- const [ searchResults, setSearchResults ] = useState([]);
- const [ loading, setLoading ] = useState(null);
- const [ equityType, setEquityType ] = useState(EQUITY_TYPES.all);
- const { addAsset, dateRange, assets } = usePortfolioSelector((state) => ({
- addAsset: state.addAsset,
- dateRange: state.dateRange,
- assets: state.assets,
- }));
-
- const handleSearch = (query: string) => {
- if (query.length < 2) return;
- setLoading("searching");
- setTimeout(async () => {
- try {
- const results = await searchAssets(query, equityType);
- setSearchResults(results.filter((result) => !assets.some((asset) => asset.symbol === result.symbol)));
- } catch (error) {
- console.error('Error searching assets:', error);
- } finally {
- setLoading(null);
- }
- }, 10);
- };
-
- const debouncedSearch = useDebouncedCallback(handleSearch, 750);
-
- const handleAssetSelect = (asset: Asset) => {
- setLoading("adding");
- setTimeout(async () => {
- try {
- const { historicalData, longName } = await getHistoricalData(
- asset.symbol,
- dateRange.startDate,
- dateRange.endDate
- );
-
- if (historicalData.size === 0) {
- toast.error(`No historical data available for ${asset.name}`);
- return;
- }
-
- const assetWithHistory = {
- ...asset,
- name: longName || asset.name,
- historicalData,
- };
-
- addAsset(assetWithHistory);
- toast.success(`Successfully added ${assetWithHistory.name}`);
- onClose();
- } catch (error) {
- console.error('Error fetching historical data:', error);
- toast.error(`Failed to add ${asset.name}. Please try again.`);
- } finally {
- setLoading(null);
- }
- }, 10);
- };
-
- return (
-
-
-
-
Add Asset
-
- Asset Type:
- setEquityType(e.target.value)} className="w-[30%] p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300">
- {Object.entries(EQUITY_TYPES).map(([key, value]) => (
- {key.charAt(0).toUpperCase() + key.slice(1)}
- ))}
-
-
-
-
-
-
-
-
- {
- setSearch(e.target.value);
- debouncedSearch(e.target.value);
- }}
- />
-
-
-
-
- {loading ? (
-
-
- {loading === "searching" ? "Searching Assets..." : "Fetching Details & Adding..."}
-
- ) : (
- searchResults.map((result) => (
-
handleAssetSelect(result)}
- >
-
-
{result.name}
-
-
- {!result.priceChangePercent?.includes("-") && "+"}{result.priceChangePercent}
-
- {result.price}
-
-
-
- Ticker-Symbol: {result.symbol} | Type: {result.quoteType?.toUpperCase() || "Unknown"} | Rank: #{result.rank || "-"}
-
-
- ))
- )}
-
-
-
- );
-};
+import { Loader2, Search, X } from "lucide-react";
+import { useState } from "react";
+import toast from "react-hot-toast";
+import { useDebouncedCallback } from "use-debounce";
+
+import { usePortfolioSelector } from "../../hooks/usePortfolio";
+import { EQUITY_TYPES, getHistoricalData, searchAssets } from "../../services/yahooFinanceService";
+import { Asset } from "../../types";
+
+export default function AddAssetModal({ onClose }: { onClose: () => void }) {
+ const [ search, setSearch ] = useState('');
+ const [ searchResults, setSearchResults ] = useState([]);
+ const [ loading, setLoading ] = useState(null);
+ const [ equityType, setEquityType ] = useState(EQUITY_TYPES.all);
+ const { addAsset, dateRange, assets } = usePortfolioSelector((state) => ({
+ addAsset: state.addAsset,
+ dateRange: state.dateRange,
+ assets: state.assets,
+ }));
+
+ const handleSearch = (query: string) => {
+ if (query.length < 2) return;
+ setLoading("searching");
+ setTimeout(async () => {
+ try {
+ const results = await searchAssets(query, equityType);
+ setSearchResults(results.filter((result) => !assets.some((asset) => asset.symbol === result.symbol)));
+ } catch (error) {
+ console.error('Error searching assets:', error);
+ } finally {
+ setLoading(null);
+ }
+ }, 10);
+ };
+
+ const debouncedSearch = useDebouncedCallback(handleSearch, 750);
+
+ const handleAssetSelect = (asset: Asset) => {
+ setLoading("adding");
+ setTimeout(async () => {
+ try {
+ const { historicalData, longName } = await getHistoricalData(
+ asset.symbol,
+ dateRange.startDate,
+ dateRange.endDate
+ );
+
+ if (historicalData.size === 0) {
+ toast.error(`No historical data available for ${asset.name}`);
+ return;
+ }
+
+ const assetWithHistory = {
+ ...asset,
+ name: longName || asset.name,
+ historicalData,
+ };
+
+ addAsset(assetWithHistory);
+ toast.success(`Successfully added ${assetWithHistory.name}`);
+ onClose();
+ } catch (error) {
+ console.error('Error fetching historical data:', error);
+ toast.error(`Failed to add ${asset.name}. Please try again.`);
+ } finally {
+ setLoading(null);
+ }
+ }, 10);
+ };
+
+ return (
+
+
+
+
Add Asset
+
+ Asset Type:
+ {
+ setEquityType(e.target.value);
+ debouncedSearch(search);
+ }} className="w-[30%] p-2 border rounded dark:bg-slate-800 dark:border-slate-700 dark:outline-none dark:text-gray-300">
+ {Object.entries(EQUITY_TYPES).map(([key, value]) => (
+ {key.charAt(0).toUpperCase() + key.slice(1)}
+ ))}
+
+
+
+
+
+
+
+
+ {
+ setSearch(e.target.value);
+ debouncedSearch(e.target.value);
+ }}
+ />
+
+
+
+
+ {loading ? (
+
+
+ {loading === "searching" ? "Searching Assets..." : "Fetching Details & Adding..."}
+
+ ) : (
+ searchResults.map((result) => (
+
handleAssetSelect(result)}
+ >
+
+
{result.name}
+
+
+ {!result.priceChangePercent?.includes("-") && "+"}{result.priceChangePercent}
+
+ {result.price}
+
+
+
+ Ticker-Symbol: {result.symbol} | Type: {result.quoteType?.toUpperCase() || "Unknown"} | Rank: #{result.rank || "-"}
+
+
+ ))
+ )}
+
+
+
+ );
+};
diff --git a/src/components/Modals/EditInvestmentModal.tsx b/src/components/Modals/EditInvestmentModal.tsx
index a871f99..a6b01db 100644
--- a/src/components/Modals/EditInvestmentModal.tsx
+++ b/src/components/Modals/EditInvestmentModal.tsx
@@ -1,87 +1,87 @@
-import { X } from "lucide-react";
-import { useState } from "react";
-import toast from "react-hot-toast";
-
-import { usePortfolioSelector } from "../../hooks/usePortfolio";
-import { Investment } from "../../types";
-
-interface EditInvestmentModalProps {
- investment: Investment;
- assetId: string;
- onClose: () => void;
-}
-
-export const EditInvestmentModal = ({ investment, assetId, onClose }: EditInvestmentModalProps) => {
- const { updateInvestment } = usePortfolioSelector((state) => ({
- updateInvestment: state.updateInvestment,
- }));
- const [amount, setAmount] = useState(investment.amount.toString());
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- try {
- updateInvestment(assetId, investment.id, {
- ...investment,
- amount: parseFloat(amount),
- });
- toast.success('Investment updated successfully');
- onClose();
- } catch (error:any) {
- toast.error('Failed to update investment' + String(error?.message || error));
- }
- };
-
- return (
-
-
-
-
Edit Investment
-
-
-
-
-
-
-
-
- );
-};
+import { X } from "lucide-react";
+import { useState } from "react";
+import toast from "react-hot-toast";
+
+import { usePortfolioSelector } from "../../hooks/usePortfolio";
+import { Investment } from "../../types";
+
+interface EditInvestmentModalProps {
+ investment: Investment;
+ assetId: string;
+ onClose: () => void;
+}
+
+export const EditInvestmentModal = ({ investment, assetId, onClose }: EditInvestmentModalProps) => {
+ const { updateInvestment } = usePortfolioSelector((state) => ({
+ updateInvestment: state.updateInvestment,
+ }));
+ const [amount, setAmount] = useState(investment.amount.toString());
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ updateInvestment(assetId, investment.id, {
+ ...investment,
+ amount: parseFloat(amount),
+ });
+ toast.success('Investment updated successfully');
+ onClose();
+ } catch (error:any) {
+ toast.error('Failed to update investment' + String(error?.message || error));
+ }
+ };
+
+ return (
+
+
+
+
Edit Investment
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/Modals/EditSavingsPlanModal.tsx b/src/components/Modals/EditSavingsPlanModal.tsx
index 3b34f1c..042fb4b 100644
--- a/src/components/Modals/EditSavingsPlanModal.tsx
+++ b/src/components/Modals/EditSavingsPlanModal.tsx
@@ -1,295 +1,295 @@
-import { format } from "date-fns";
-import { Loader2, X } from "lucide-react";
-import { useEffect, useState } from "react";
-import toast from "react-hot-toast";
-
-import { useLocaleDateFormat } from "../../hooks/useLocalDateFormat";
-import { usePortfolioSelector } from "../../hooks/usePortfolio";
-import { PeriodicSettings } from "../../types";
-import { generatePeriodicInvestments } from "../../utils/calculations/assetValue";
-import { Tooltip } from "../utils/ToolTip";
-
-interface EditSavingsPlanModalProps {
- assetId: string;
- groupId: string;
- amount: number;
- dayOfMonth: number;
- interval: number;
- dynamic?: {
- type: 'percentage' | 'fixed';
- value: number;
- yearInterval: number;
- };
- onClose: () => void;
-}
-
-interface IntervalConfig {
- value: number;
- unit: 'days' | 'months' | 'years';
-}
-
-export const EditSavingsPlanModal = ({
- assetId,
- groupId,
- amount: initialAmount,
- dayOfMonth: initialDayOfMonth,
- interval: initialInterval,
- dynamic: initialDynamic,
- onClose
-}: EditSavingsPlanModalProps) => {
- const [amount, setAmount] = useState(initialAmount.toString());
- const [dayOfMonth, setDayOfMonth] = useState(initialDayOfMonth.toString());
- const [interval, setInterval] = useState(initialInterval.toString());
- const [intervalUnit, setIntervalUnit] = useState<'days' | 'weeks' | 'months' | 'quarters' | 'years'>('months');
- const [isDynamic, setIsDynamic] = useState(!!initialDynamic);
- const [dynamicType, setDynamicType] = useState<'percentage' | 'fixed'>(initialDynamic?.type || 'percentage');
- const [dynamicValue, setDynamicValue] = useState(initialDynamic?.value.toString() || '');
- const [yearInterval, setYearInterval] = useState(initialDynamic?.yearInterval.toString() || '1');
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [showIntervalWarning, setShowIntervalWarning] = useState(false);
- const [startDate, setStartDate] = useState('');
- const localeDateFormat = useLocaleDateFormat();
-
- const { dateRange, addInvestment, removeInvestment, assets } = usePortfolioSelector((state) => ({
- dateRange: state.dateRange,
- addInvestment: state.addInvestment,
- removeInvestment: state.removeInvestment,
- assets: state.assets,
- }));
-
- useEffect(() => {
- const asset = assets.find(a => a.id === assetId)!;
- const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
- const firstInvestmentDate = investments[0].date!;
- setStartDate(format(firstInvestmentDate, 'yyyy-MM-dd'));
- }, [assetId, groupId, assets]);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- e.stopPropagation();
- setIsSubmitting(true);
-
- setTimeout(async () => {
- try {
- // First, remove all existing investments for this savings plan
- const asset = assets.find(a => a.id === assetId)!;
- const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
-
- investments.forEach(inv => {
- removeInvestment(assetId, inv.id);
- });
-
- // Generate and add new investments with the new start date
- const periodicSettings: PeriodicSettings = {
- startDate: new Date(startDate), // Use the new start date
- dayOfMonth: parseInt(dayOfMonth),
- interval: parseInt(interval),
- intervalUnit: intervalUnit,
- amount: parseFloat(amount),
- ...(isDynamic ? {
- dynamic: {
- type: dynamicType,
- value: parseFloat(dynamicValue),
- yearInterval: parseInt(yearInterval),
- },
- } : undefined),
- };
-
- const newInvestments = generatePeriodicInvestments(
- periodicSettings,
- dateRange.endDate,
- assetId
- );
-
- addInvestment(assetId, newInvestments);
- toast.success('Savings plan updated successfully');
- onClose();
- } catch (error:any) {
- toast.error('Failed to update savings plan: ' + String(error?.message || error));
- } finally {
- setIsSubmitting(false);
- }
- }, 10);
- };
-
- const handleIntervalUnitChange = (unit: IntervalConfig['unit']) => {
- setIntervalUnit(unit);
- setShowIntervalWarning(['days', 'weeks'].includes(unit));
- };
-
- return (
-
-
-
-
Edit Savings Plan
-
-
-
-
-
-
-
-
- );
-};
+import { format } from "date-fns";
+import { Loader2, X } from "lucide-react";
+import { useEffect, useState } from "react";
+import toast from "react-hot-toast";
+
+import { useLocaleDateFormat } from "../../hooks/useLocalDateFormat";
+import { usePortfolioSelector } from "../../hooks/usePortfolio";
+import { PeriodicSettings } from "../../types";
+import { generatePeriodicInvestments } from "../../utils/calculations/assetValue";
+import { Tooltip } from "../utils/ToolTip";
+
+interface EditSavingsPlanModalProps {
+ assetId: string;
+ groupId: string;
+ amount: number;
+ dayOfMonth: number;
+ interval: number;
+ dynamic?: {
+ type: 'percentage' | 'fixed';
+ value: number;
+ yearInterval: number;
+ };
+ onClose: () => void;
+}
+
+interface IntervalConfig {
+ value: number;
+ unit: 'days' | 'months' | 'years';
+}
+
+export const EditSavingsPlanModal = ({
+ assetId,
+ groupId,
+ amount: initialAmount,
+ dayOfMonth: initialDayOfMonth,
+ interval: initialInterval,
+ dynamic: initialDynamic,
+ onClose
+}: EditSavingsPlanModalProps) => {
+ const [amount, setAmount] = useState(initialAmount.toString());
+ const [dayOfMonth, setDayOfMonth] = useState(initialDayOfMonth.toString());
+ const [interval, setInterval] = useState(initialInterval.toString());
+ const [intervalUnit, setIntervalUnit] = useState<'days' | 'weeks' | 'months' | 'quarters' | 'years'>('months');
+ const [isDynamic, setIsDynamic] = useState(!!initialDynamic);
+ const [dynamicType, setDynamicType] = useState<'percentage' | 'fixed'>(initialDynamic?.type || 'percentage');
+ const [dynamicValue, setDynamicValue] = useState(initialDynamic?.value.toString() || '');
+ const [yearInterval, setYearInterval] = useState(initialDynamic?.yearInterval.toString() || '1');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [showIntervalWarning, setShowIntervalWarning] = useState(false);
+ const [startDate, setStartDate] = useState('');
+ const localeDateFormat = useLocaleDateFormat();
+
+ const { dateRange, addInvestment, removeInvestment, assets } = usePortfolioSelector((state) => ({
+ dateRange: state.dateRange,
+ addInvestment: state.addInvestment,
+ removeInvestment: state.removeInvestment,
+ assets: state.assets,
+ }));
+
+ useEffect(() => {
+ const asset = assets.find(a => a.id === assetId)!;
+ const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
+ const firstInvestmentDate = investments[0].date!;
+ setStartDate(format(firstInvestmentDate, 'yyyy-MM-dd'));
+ }, [assetId, groupId, assets]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsSubmitting(true);
+
+ setTimeout(async () => {
+ try {
+ // First, remove all existing investments for this savings plan
+ const asset = assets.find(a => a.id === assetId)!;
+ const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
+
+ investments.forEach(inv => {
+ removeInvestment(assetId, inv.id);
+ });
+
+ // Generate and add new investments with the new start date
+ const periodicSettings: PeriodicSettings = {
+ startDate: new Date(startDate), // Use the new start date
+ dayOfMonth: parseInt(dayOfMonth),
+ interval: parseInt(interval),
+ intervalUnit: intervalUnit,
+ amount: parseFloat(amount),
+ ...(isDynamic ? {
+ dynamic: {
+ type: dynamicType,
+ value: parseFloat(dynamicValue),
+ yearInterval: parseInt(yearInterval),
+ },
+ } : undefined),
+ };
+
+ const newInvestments = generatePeriodicInvestments(
+ periodicSettings,
+ dateRange.endDate,
+ assetId
+ );
+
+ addInvestment(assetId, newInvestments);
+ toast.success('Savings plan updated successfully');
+ onClose();
+ } catch (error:any) {
+ toast.error('Failed to update savings plan: ' + String(error?.message || error));
+ } finally {
+ setIsSubmitting(false);
+ }
+ }, 10);
+ };
+
+ const handleIntervalUnitChange = (unit: IntervalConfig['unit']) => {
+ setIntervalUnit(unit);
+ setShowIntervalWarning(['days', 'weeks'].includes(unit));
+ };
+
+ return (
+
+
+
+
Edit Savings Plan
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/Modals/FutureProjectionModal.tsx b/src/components/Modals/FutureProjectionModal.tsx
index e1a1114..bde08fa 100644
--- a/src/components/Modals/FutureProjectionModal.tsx
+++ b/src/components/Modals/FutureProjectionModal.tsx
@@ -1,741 +1,741 @@
-import { isSameDay } from "date-fns";
-import { BarChart as BarChartIcon, LineChart as LineChartIcon, Loader2, X } from "lucide-react";
-import { useCallback, useState } from "react";
-import {
- Bar, BarChart, CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
-} from "recharts";
-
-import { usePortfolioSelector } from "../../hooks/usePortfolio";
-import { calculateFutureProjection } from "../../utils/calculations/futureProjection";
-import { formatCurrency } from "../../utils/formatters";
-import { Tooltip as InfoTooltip } from "../utils/ToolTip";
-
-import type { ProjectionData, SustainabilityAnalysis, WithdrawalPlan } from "../../types";
-interface FutureProjectionModalProps {
- performancePerAnno: number;
- bestPerformancePerAnno: { percentage: number, year: number }[];
- worstPerformancePerAnno: { percentage: number, year: number }[];
- onClose: () => void;
-}
-
-export type ChartType = 'line' | 'bar';
-
-type ScenarioCalc = { projection: ProjectionData[], sustainability: SustainabilityAnalysis | null, avaragedAmount: number, percentage: number, percentageAveraged: number };
-
-export const FutureProjectionModal = ({
- performancePerAnno,
- bestPerformancePerAnno,
- worstPerformancePerAnno,
- onClose
-}: FutureProjectionModalProps) => {
- const [years, setYears] = useState('10');
- const [isCalculating, setIsCalculating] = useState(false);
- const [chartType, setChartType] = useState('line');
- const [projectionData, setProjectionData] = useState([]);
- const [scenarios, setScenarios] = useState<{ best: ScenarioCalc, worst: ScenarioCalc }>({
- best: { projection: [], sustainability: null, avaragedAmount: 0, percentage: 0, percentageAveraged: 0 },
- worst: { projection: [], sustainability: null, avaragedAmount: 0, percentage: 0, percentageAveraged: 0 },
- });
- const [withdrawalPlan, setWithdrawalPlan] = useState({
- amount: 0,
- interval: 'monthly',
- startTrigger: 'auto',
- startDate: new Date(),
- startPortfolioValue: 0,
- enabled: false,
- autoStrategy: {
- type: 'maintain',
- targetYears: 30,
- targetGrowth: 2,
- },
- });
- const [sustainabilityAnalysis, setSustainabilityAnalysis] = useState(null);
- const [startFromZero, setStartFromZero] = useState(false);
-
- const { assets } = usePortfolioSelector((state) => ({
- assets: state.assets,
- }));
-
- const calculateProjection = useCallback(async () => {
- setIsCalculating(true);
- try {
- const { projection, sustainability } = await calculateFutureProjection(
- assets,
- parseInt(years),
- performancePerAnno,
- withdrawalPlan.enabled ? withdrawalPlan : undefined,
- startFromZero
- );
- setProjectionData(projection);
- setSustainabilityAnalysis(sustainability);
- const slicedBestCase = bestPerformancePerAnno.slice(0, bestPerformancePerAnno.length > 1 ? Math.floor(bestPerformancePerAnno.length / 2) : 1);
- const slicedWorstCase = worstPerformancePerAnno.slice(0, worstPerformancePerAnno.length > 1 ? Math.floor(worstPerformancePerAnno.length / 2) : 1);
- const bestCase = slicedBestCase.reduce((acc, curr) => acc + curr.percentage, 0) / slicedBestCase.length || 0;
- const worstCase = slicedWorstCase.reduce((acc, curr) => acc + curr.percentage, 0) / slicedWorstCase.length || 0;
-
- const bestCaseAvaraged = (bestCase + performancePerAnno) / 2;
- const worstCaseAvaraged = (worstCase + performancePerAnno) / 2;
- setScenarios({
- best: {
- ...await calculateFutureProjection(
- assets,
- parseInt(years),
- bestCaseAvaraged,
- withdrawalPlan.enabled ? withdrawalPlan : undefined
- ),
- avaragedAmount: slicedBestCase.length,
- percentageAveraged: bestCaseAvaraged,
- percentage: bestCase
- },
- worst: {
- ...await calculateFutureProjection(
- assets,
- parseInt(years),
- worstCaseAvaraged,
- withdrawalPlan.enabled ? withdrawalPlan : undefined
- ),
- avaragedAmount: slicedWorstCase.length,
- percentage: worstCase,
- percentageAveraged: worstCaseAvaraged
- }
- });
- } catch (error) {
- console.error('Error calculating projection:', error);
- } finally {
- setIsCalculating(false);
- }
- }, [assets, years, withdrawalPlan, performancePerAnno, bestPerformancePerAnno, worstPerformancePerAnno, startFromZero]);
-
- const CustomTooltip = ({ active, payload, label }: any) => {
- if (active && payload && payload.length) {
- const value = payload[0].value;
- const invested = payload[1].value;
- const withdrawn = payload[2]?.value || 0;
- const totalWithdrawn = payload[3]?.value || 0;
- const percentageGain = ((value - invested) / invested) * 100;
-
- return (
-
-
- {new Date(label).toLocaleDateString('de-DE')}
-
-
- Value: {formatCurrency(value)}
-
-
- Invested: {formatCurrency(invested)}
-
- {withdrawn > 0 && (
- <>
-
- Monthly Withdrawal: {formatCurrency(withdrawn)}
-
-
- Total Withdrawn: {formatCurrency(totalWithdrawn)}
-
- >
- )}
-
= 0 ? 'text-green-500' : 'text-red-500'}`}>
- Return: {percentageGain.toFixed(2)}%
-
-
- );
- }
- return null;
- };
-
-
- const CustomScenarioTooltip = ({ active, payload, label }: any) => {
- if (active && payload && payload.length) {
- const bestCase = payload.find((p: any) => p.dataKey === 'bestCase')?.value || 0;
- const baseCase = payload.find((p: any) => p.dataKey === 'baseCase')?.value || 0;
- const worstCase = payload.find((p: any) => p.dataKey === 'worstCase')?.value || 0;
- const invested = payload.find((p: any) => p.dataKey === 'invested')?.value || 0;
-
- return (
-
-
- {new Date(label).toLocaleDateString('de-DE')}
-
-
- Best-Case: {formatCurrency(bestCase)} {((bestCase - invested) / invested * 100).toFixed(2)}%
-
-
- Avg. Base-Case: {formatCurrency(baseCase)} {((baseCase - invested) / invested * 100).toFixed(2)}%
-
-
- Worst-Case: {formatCurrency(worstCase)} {((worstCase - invested) / invested * 100).toFixed(2)}%
-
-
- );
- }
- return null;
- };
-
-
-
- const renderChart = () => {
- if (isCalculating) {
- return (
-
-
-
- );
- }
-
- if (!projectionData.length) {
- return (
-
- Click calculate to see the projection
-
- );
- }
-
- return (
-
- {chartType === 'line' ? (
-
-
- new Date(date).toLocaleDateString('de-DE', {
- year: 'numeric',
- month: 'numeric'
- })}
- />
-
- } />
-
-
- {withdrawalPlan.enabled && (
- <>
-
-
- >
- )}
-
- ) : (
-
-
- new Date(date).toLocaleDateString('de-DE', {
- year: 'numeric',
- month: 'numeric'
- })}
- />
-
- } />
-
-
- {withdrawalPlan.enabled && (
- <>
-
-
- >
- )}
-
- )}
-
- );
- };
-
- const renderScenarioDescription = () => {
- if (!scenarios.best.projection.length) return null;
-
- const getLastValue = (projection: ProjectionData[]) => {
- const lastPoint = projection[projection.length - 1];
- return {
- value: lastPoint.value,
- invested: lastPoint.invested,
- returnPercentage: ((lastPoint.value - lastPoint.invested) / lastPoint.invested) * 100
- };
- };
-
- const baseCase = getLastValue(projectionData);
- const bestCase = getLastValue(scenarios.best.projection);
- const worstCase = getLastValue(scenarios.worst.projection);
-
- return (
-
-
Scenario Calculations
-
-
- Avg. Base Case: Using historical average return of{' '}
- {performancePerAnno.toFixed(2)}%
-
- After {years} years you'd have{' '}
- {formatCurrency(baseCase.value)} from {formatCurrency(baseCase.invested)} invested,{' '}
- that's a total return of {baseCase.returnPercentage.toFixed(2)}%
-
-
-
- Best Case: Average of top 50% performing years ({scenarios.best.avaragedAmount} years) at {scenarios.best.percentage.toFixed(2)}%,
- averaged with base case to {scenarios.best.percentageAveraged.toFixed(2)}% .{' '}
-
- After {years} years you'd have {formatCurrency(bestCase.value)} from {formatCurrency(bestCase.invested)} invested,{' '}
- that's a total return of {bestCase.returnPercentage.toFixed(2)}%
-
-
-
- Worst Case: Average of bottom 50% performing years ({scenarios.worst.avaragedAmount} years) at {scenarios.worst.percentage.toFixed(2)}%,
- averaged with base case to {scenarios.worst.percentageAveraged.toFixed(2)}% .{' '}
-
- After {years} years you'd have {formatCurrency(worstCase.value)} from {formatCurrency(worstCase.invested)} invested,{' '}
- that's a total return of {worstCase.returnPercentage.toFixed(2)}%
-
-
-
-
- );
- };
-
- const renderScenarioChart = () => {
- if (!scenarios.best.projection.length) return null;
-
- // Create a merged and sorted dataset for consistent x-axis
- const mergedData = projectionData.map(basePoint => {
- const date = basePoint.date;
- const bestPoint = scenarios.best.projection.find(p => isSameDay(p.date, date));
- const worstPoint = scenarios.worst.projection.find(p => isSameDay(p.date, date));
-
- return {
- date,
- bestCase: bestPoint?.value || 0,
- baseCase: basePoint.value,
- worstCase: worstPoint?.value || 0,
- invested: basePoint.invested
- };
- }).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
-
- return (
-
-
Scenario Comparison
-
-
-
-
- new Date(date).toLocaleDateString('de-DE', {
- year: 'numeric',
- month: 'numeric'
- })}
- />
-
- }/>
-
-
-
-
-
-
-
-
- );
- };
-
- return (
-
-
-
-
-
Future Portfolio Projection
-
-
-
-
-
-
-
-
-
-
Projection Settings
-
-
-
setYears(e.target.value)}
- min="1"
- max="50"
- className="w-24 p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- />
-
- {isCalculating ? (
-
- ) : (
- 'Calculate'
- )}
-
-
- setChartType('line')}
- className={`p-2 rounded ${chartType === 'line' ? 'bg-blue-100 dark:bg-blue-900' : 'hover:bg-gray-100 dark:hover:bg-slate-700'}`}
- title="Line Chart"
- >
-
-
- setChartType('bar')}
- className={`p-2 rounded ${chartType === 'bar' ? 'bg-blue-100 dark:bg-blue-900' : 'hover:bg-gray-100 dark:hover:bg-slate-700'}`}
- title="Bar Chart"
- >
-
-
-
-
-
-
-
- setStartFromZero(e.target.checked)}
- />
-
-
-
- Start from 0β¬
-
-
-
-
-
-
-
- Future projections are calculated with your portfolio's average annual return rate of{' '}
- {performancePerAnno.toFixed(2)}% .
-
-
- Strategy explanations:
-
- Maintain: Portfolio value stays constant, withdrawing only the returns
- Deplete: Portfolio depletes to zero over specified years
- Grow: Portfolio continues to grow at target rate while withdrawing
-
-
-
- {renderScenarioDescription()}
-
-
-
-
-
Withdrawal Plan
-
- setWithdrawalPlan(prev => ({
- ...prev,
- enabled: e.target.checked
- }))}
- />
-
-
-
-
-
-
-
- Withdrawal Amount (β¬)
-
- setWithdrawalPlan(prev => ({
- ...prev,
- amount: parseFloat(e.target.value)
- }))}
- min="0"
- step="100"
- className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- />
-
-
-
-
- Withdrawal Interval
-
- setWithdrawalPlan(prev => ({
- ...prev,
- interval: e.target.value as 'monthly' | 'yearly'
- }))}
- className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- >
- Monthly
- Yearly
-
-
-
-
-
- Start Trigger
-
- setWithdrawalPlan(prev => ({
- ...prev,
- startTrigger: e.target.value as 'date' | 'portfolioValue' | 'auto'
- }))}
- className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- >
- Specific Date
- Portfolio Value Threshold
- Auto-Finder
-
-
-
- {withdrawalPlan.startTrigger === 'date' ? (
-
-
- Start Date
-
- setWithdrawalPlan(prev => ({
- ...prev,
- startDate: new Date(e.target.value)
- }))}
- min={new Date().toISOString().split('T')[0]}
- className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- />
-
- ) : withdrawalPlan.startTrigger === 'portfolioValue' ? (
-
-
- Start at Portfolio Value (β¬)
-
- setWithdrawalPlan(prev => ({
- ...prev,
- startPortfolioValue: parseFloat(e.target.value)
- }))}
- min="0"
- step="1000"
- className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- />
-
- ) : null}
-
- {withdrawalPlan.startTrigger === 'auto' && (
-
-
-
- Desired {withdrawalPlan.interval} Withdrawal (β¬)
-
- setWithdrawalPlan(prev => ({
- ...prev,
- amount: parseFloat(e.target.value)
- }))}
- min="0"
- step="100"
- className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- />
-
-
-
-
- Strategy
-
- setWithdrawalPlan(prev => ({
- ...prev,
- autoStrategy: {
- ...prev.autoStrategy!,
- type: e.target.value as 'maintain' | 'deplete' | 'grow'
- }
- }))}
- className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- >
- Maintain Portfolio Value
- Planned Depletion
- Sustainable Growth
-
-
-
- {withdrawalPlan.autoStrategy?.type === 'deplete' && (
-
-
- Years to Deplete After Starting
-
- setWithdrawalPlan(prev => ({
- ...prev,
- autoStrategy: {
- ...prev.autoStrategy!,
- targetYears: parseInt(e.target.value)
- }
- }))}
- min="1"
- max="100"
- className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- />
-
- )}
-
- {withdrawalPlan.autoStrategy?.type === 'grow' && (
-
-
- Annual Growth After Starting (%)
-
- setWithdrawalPlan(prev => ({
- ...prev,
- autoStrategy: {
- ...prev.autoStrategy!,
- targetGrowth: parseFloat(e.target.value)
- }
- }))}
- min="0.1"
- max="10"
- step="0.1"
- className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
- />
-
- )}
-
-
-
- {withdrawalPlan.autoStrategy?.type === 'maintain' && (
- "The calculator will determine when your portfolio can sustain this withdrawal amount while maintaining its value."
- )}
- {withdrawalPlan.autoStrategy?.type === 'deplete' && (
- "The calculator will determine when you can start withdrawing this amount to deplete the portfolio over your specified timeframe."
- )}
- {withdrawalPlan.autoStrategy?.type === 'grow' && (
- "The calculator will determine when you can start withdrawing this amount while maintaining the target growth rate."
- )}
-
-
-
- )}
-
-
-
-
- {sustainabilityAnalysis && withdrawalPlan.enabled && (
-
-
Withdrawal Analysis
-
- To withdraw {formatCurrency(withdrawalPlan.amount)} {withdrawalPlan.interval}, you need to invest for{' '}
- {sustainabilityAnalysis.yearsToReachTarget} years until your portfolio reaches{' '}
- {formatCurrency(sustainabilityAnalysis.targetValue)} .
-
-
- With this withdrawal plan, your portfolio will{' '}
- {sustainabilityAnalysis.sustainableYears === 'infinite' ? (
- remain sustainable indefinitely
- ) : (
- <>
- last for{' '}
-
- {sustainabilityAnalysis.sustainableYears} years
- {' '}
- {sustainabilityAnalysis.sustainableYears > parseInt(years) && (
-
- (extends beyond the current chart view of {years} years)
-
- )}
- >
- )}
- .
-
-
- )}
-
-
-
- {renderChart()}
-
- {renderScenarioChart()}
-
-
-
-
- );
-};
+import { isSameDay } from "date-fns";
+import { BarChart as BarChartIcon, LineChart as LineChartIcon, Loader2, X } from "lucide-react";
+import { useCallback, useState } from "react";
+import {
+ Bar, BarChart, CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
+} from "recharts";
+
+import { usePortfolioSelector } from "../../hooks/usePortfolio";
+import { calculateFutureProjection } from "../../utils/calculations/futureProjection";
+import { formatCurrency } from "../../utils/formatters";
+import { Tooltip as InfoTooltip } from "../utils/ToolTip";
+
+import type { ProjectionData, SustainabilityAnalysis, WithdrawalPlan } from "../../types";
+interface FutureProjectionModalProps {
+ performancePerAnno: number;
+ bestPerformancePerAnno: { percentage: number, year: number }[];
+ worstPerformancePerAnno: { percentage: number, year: number }[];
+ onClose: () => void;
+}
+
+export type ChartType = 'line' | 'bar';
+
+type ScenarioCalc = { projection: ProjectionData[], sustainability: SustainabilityAnalysis | null, avaragedAmount: number, percentage: number, percentageAveraged: number };
+
+export const FutureProjectionModal = ({
+ performancePerAnno,
+ bestPerformancePerAnno,
+ worstPerformancePerAnno,
+ onClose
+}: FutureProjectionModalProps) => {
+ const [years, setYears] = useState('10');
+ const [isCalculating, setIsCalculating] = useState(false);
+ const [chartType, setChartType] = useState('line');
+ const [projectionData, setProjectionData] = useState([]);
+ const [scenarios, setScenarios] = useState<{ best: ScenarioCalc, worst: ScenarioCalc }>({
+ best: { projection: [], sustainability: null, avaragedAmount: 0, percentage: 0, percentageAveraged: 0 },
+ worst: { projection: [], sustainability: null, avaragedAmount: 0, percentage: 0, percentageAveraged: 0 },
+ });
+ const [withdrawalPlan, setWithdrawalPlan] = useState({
+ amount: 0,
+ interval: 'monthly',
+ startTrigger: 'auto',
+ startDate: new Date(),
+ startPortfolioValue: 0,
+ enabled: false,
+ autoStrategy: {
+ type: 'maintain',
+ targetYears: 30,
+ targetGrowth: 2,
+ },
+ });
+ const [sustainabilityAnalysis, setSustainabilityAnalysis] = useState(null);
+ const [startFromZero, setStartFromZero] = useState(false);
+
+ const { assets } = usePortfolioSelector((state) => ({
+ assets: state.assets,
+ }));
+
+ const calculateProjection = useCallback(async () => {
+ setIsCalculating(true);
+ try {
+ const { projection, sustainability } = await calculateFutureProjection(
+ assets,
+ parseInt(years),
+ performancePerAnno,
+ withdrawalPlan.enabled ? withdrawalPlan : undefined,
+ startFromZero
+ );
+ setProjectionData(projection);
+ setSustainabilityAnalysis(sustainability);
+ const slicedBestCase = bestPerformancePerAnno.slice(0, bestPerformancePerAnno.length > 1 ? Math.floor(bestPerformancePerAnno.length / 2) : 1);
+ const slicedWorstCase = worstPerformancePerAnno.slice(0, worstPerformancePerAnno.length > 1 ? Math.floor(worstPerformancePerAnno.length / 2) : 1);
+ const bestCase = slicedBestCase.reduce((acc, curr) => acc + curr.percentage, 0) / slicedBestCase.length || 0;
+ const worstCase = slicedWorstCase.reduce((acc, curr) => acc + curr.percentage, 0) / slicedWorstCase.length || 0;
+
+ const bestCaseAvaraged = (bestCase + performancePerAnno) / 2;
+ const worstCaseAvaraged = (worstCase + performancePerAnno) / 2;
+ setScenarios({
+ best: {
+ ...await calculateFutureProjection(
+ assets,
+ parseInt(years),
+ bestCaseAvaraged,
+ withdrawalPlan.enabled ? withdrawalPlan : undefined
+ ),
+ avaragedAmount: slicedBestCase.length,
+ percentageAveraged: bestCaseAvaraged,
+ percentage: bestCase
+ },
+ worst: {
+ ...await calculateFutureProjection(
+ assets,
+ parseInt(years),
+ worstCaseAvaraged,
+ withdrawalPlan.enabled ? withdrawalPlan : undefined
+ ),
+ avaragedAmount: slicedWorstCase.length,
+ percentage: worstCase,
+ percentageAveraged: worstCaseAvaraged
+ }
+ });
+ } catch (error) {
+ console.error('Error calculating projection:', error);
+ } finally {
+ setIsCalculating(false);
+ }
+ }, [assets, years, withdrawalPlan, performancePerAnno, bestPerformancePerAnno, worstPerformancePerAnno, startFromZero]);
+
+ const CustomTooltip = ({ active, payload, label }: any) => {
+ if (active && payload && payload.length) {
+ const value = payload[0].value;
+ const invested = payload[1].value;
+ const withdrawn = payload[2]?.value || 0;
+ const totalWithdrawn = payload[3]?.value || 0;
+ const percentageGain = ((value - invested) / invested) * 100;
+
+ return (
+
+
+ {new Date(label).toLocaleDateString('de-DE')}
+
+
+ Value: {formatCurrency(value)}
+
+
+ Invested: {formatCurrency(invested)}
+
+ {withdrawn > 0 && (
+ <>
+
+ Monthly Withdrawal: {formatCurrency(withdrawn)}
+
+
+ Total Withdrawn: {formatCurrency(totalWithdrawn)}
+
+ >
+ )}
+
= 0 ? 'text-green-500' : 'text-red-500'}`}>
+ Return: {percentageGain.toFixed(2)}%
+
+
+ );
+ }
+ return null;
+ };
+
+
+ const CustomScenarioTooltip = ({ active, payload, label }: any) => {
+ if (active && payload && payload.length) {
+ const bestCase = payload.find((p: any) => p.dataKey === 'bestCase')?.value || 0;
+ const baseCase = payload.find((p: any) => p.dataKey === 'baseCase')?.value || 0;
+ const worstCase = payload.find((p: any) => p.dataKey === 'worstCase')?.value || 0;
+ const invested = payload.find((p: any) => p.dataKey === 'invested')?.value || 0;
+
+ return (
+
+
+ {new Date(label).toLocaleDateString('de-DE')}
+
+
+ Best-Case: {formatCurrency(bestCase)} {((bestCase - invested) / invested * 100).toFixed(2)}%
+
+
+ Avg. Base-Case: {formatCurrency(baseCase)} {((baseCase - invested) / invested * 100).toFixed(2)}%
+
+
+ Worst-Case: {formatCurrency(worstCase)} {((worstCase - invested) / invested * 100).toFixed(2)}%
+
+
+ );
+ }
+ return null;
+ };
+
+
+
+ const renderChart = () => {
+ if (isCalculating) {
+ return (
+
+
+
+ );
+ }
+
+ if (!projectionData.length) {
+ return (
+
+ Click calculate to see the projection
+
+ );
+ }
+
+ return (
+
+ {chartType === 'line' ? (
+
+
+ new Date(date).toLocaleDateString('de-DE', {
+ year: 'numeric',
+ month: 'numeric'
+ })}
+ />
+
+ } />
+
+
+ {withdrawalPlan.enabled && (
+ <>
+
+
+ >
+ )}
+
+ ) : (
+
+
+ new Date(date).toLocaleDateString('de-DE', {
+ year: 'numeric',
+ month: 'numeric'
+ })}
+ />
+
+ } />
+
+
+ {withdrawalPlan.enabled && (
+ <>
+
+
+ >
+ )}
+
+ )}
+
+ );
+ };
+
+ const renderScenarioDescription = () => {
+ if (!scenarios.best.projection.length) return null;
+
+ const getLastValue = (projection: ProjectionData[]) => {
+ const lastPoint = projection[projection.length - 1];
+ return {
+ value: lastPoint.value,
+ invested: lastPoint.invested,
+ returnPercentage: ((lastPoint.value - lastPoint.invested) / lastPoint.invested) * 100
+ };
+ };
+
+ const baseCase = getLastValue(projectionData);
+ const bestCase = getLastValue(scenarios.best.projection);
+ const worstCase = getLastValue(scenarios.worst.projection);
+
+ return (
+
+
Scenario Calculations
+
+
+ Avg. Base Case: Using historical average return of{' '}
+ {performancePerAnno.toFixed(2)}%
+
+ After {years} years you'd have{' '}
+ {formatCurrency(baseCase.value)} from {formatCurrency(baseCase.invested)} invested,{' '}
+ that's a total return of {baseCase.returnPercentage.toFixed(2)}%
+
+
+
+ Best Case: Average of top 50% performing years ({scenarios.best.avaragedAmount} years) at {scenarios.best.percentage.toFixed(2)}%,
+ averaged with base case to {scenarios.best.percentageAveraged.toFixed(2)}% .{' '}
+
+ After {years} years you'd have {formatCurrency(bestCase.value)} from {formatCurrency(bestCase.invested)} invested,{' '}
+ that's a total return of {bestCase.returnPercentage.toFixed(2)}%
+
+
+
+ Worst Case: Average of bottom 50% performing years ({scenarios.worst.avaragedAmount} years) at {scenarios.worst.percentage.toFixed(2)}%,
+ averaged with base case to {scenarios.worst.percentageAveraged.toFixed(2)}% .{' '}
+
+ After {years} years you'd have {formatCurrency(worstCase.value)} from {formatCurrency(worstCase.invested)} invested,{' '}
+ that's a total return of {worstCase.returnPercentage.toFixed(2)}%
+
+
+
+
+ );
+ };
+
+ const renderScenarioChart = () => {
+ if (!scenarios.best.projection.length) return null;
+
+ // Create a merged and sorted dataset for consistent x-axis
+ const mergedData = projectionData.map(basePoint => {
+ const date = basePoint.date;
+ const bestPoint = scenarios.best.projection.find(p => isSameDay(p.date, date));
+ const worstPoint = scenarios.worst.projection.find(p => isSameDay(p.date, date));
+
+ return {
+ date,
+ bestCase: bestPoint?.value || 0,
+ baseCase: basePoint.value,
+ worstCase: worstPoint?.value || 0,
+ invested: basePoint.invested
+ };
+ }).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
+
+ return (
+
+
Scenario Comparison
+
+
+
+
+ new Date(date).toLocaleDateString('de-DE', {
+ year: 'numeric',
+ month: 'numeric'
+ })}
+ />
+
+ }/>
+
+
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
Future Portfolio Projection
+
+
+
+
+
+
+
+
+
+
Projection Settings
+
+
+
setYears(e.target.value)}
+ min="1"
+ max="50"
+ className="w-24 p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ />
+
+ {isCalculating ? (
+
+ ) : (
+ 'Calculate'
+ )}
+
+
+ setChartType('line')}
+ className={`p-2 rounded ${chartType === 'line' ? 'bg-blue-100 dark:bg-blue-900' : 'hover:bg-gray-100 dark:hover:bg-slate-700'}`}
+ title="Line Chart"
+ >
+
+
+ setChartType('bar')}
+ className={`p-2 rounded ${chartType === 'bar' ? 'bg-blue-100 dark:bg-blue-900' : 'hover:bg-gray-100 dark:hover:bg-slate-700'}`}
+ title="Bar Chart"
+ >
+
+
+
+
+
+
+
+ setStartFromZero(e.target.checked)}
+ />
+
+
+
+ Start from 0β¬
+
+
+
+
+
+
+
+ Future projections are calculated with your portfolio's average annual return rate of{' '}
+ {performancePerAnno.toFixed(2)}% .
+
+
+ Strategy explanations:
+
+ Maintain: Portfolio value stays constant, withdrawing only the returns
+ Deplete: Portfolio depletes to zero over specified years
+ Grow: Portfolio continues to grow at target rate while withdrawing
+
+
+
+ {renderScenarioDescription()}
+
+
+
+
+
Withdrawal Plan
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ enabled: e.target.checked
+ }))}
+ />
+
+
+
+
+
+
+
+ Withdrawal Amount (β¬)
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ amount: parseFloat(e.target.value)
+ }))}
+ min="0"
+ step="100"
+ className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ />
+
+
+
+
+ Withdrawal Interval
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ interval: e.target.value as 'monthly' | 'yearly'
+ }))}
+ className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ >
+ Monthly
+ Yearly
+
+
+
+
+
+ Start Trigger
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ startTrigger: e.target.value as 'date' | 'portfolioValue' | 'auto'
+ }))}
+ className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ >
+ Specific Date
+ Portfolio Value Threshold
+ Auto-Finder
+
+
+
+ {withdrawalPlan.startTrigger === 'date' ? (
+
+
+ Start Date
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ startDate: new Date(e.target.value)
+ }))}
+ min={new Date().toISOString().split('T')[0]}
+ className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ />
+
+ ) : withdrawalPlan.startTrigger === 'portfolioValue' ? (
+
+
+ Start at Portfolio Value (β¬)
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ startPortfolioValue: parseFloat(e.target.value)
+ }))}
+ min="0"
+ step="1000"
+ className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ />
+
+ ) : null}
+
+ {withdrawalPlan.startTrigger === 'auto' && (
+
+
+
+ Desired {withdrawalPlan.interval} Withdrawal (β¬)
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ amount: parseFloat(e.target.value)
+ }))}
+ min="0"
+ step="100"
+ className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ />
+
+
+
+
+ Strategy
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ autoStrategy: {
+ ...prev.autoStrategy!,
+ type: e.target.value as 'maintain' | 'deplete' | 'grow'
+ }
+ }))}
+ className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ >
+ Maintain Portfolio Value
+ Planned Depletion
+ Sustainable Growth
+
+
+
+ {withdrawalPlan.autoStrategy?.type === 'deplete' && (
+
+
+ Years to Deplete After Starting
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ autoStrategy: {
+ ...prev.autoStrategy!,
+ targetYears: parseInt(e.target.value)
+ }
+ }))}
+ min="1"
+ max="100"
+ className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ />
+
+ )}
+
+ {withdrawalPlan.autoStrategy?.type === 'grow' && (
+
+
+ Annual Growth After Starting (%)
+
+ setWithdrawalPlan(prev => ({
+ ...prev,
+ autoStrategy: {
+ ...prev.autoStrategy!,
+ targetGrowth: parseFloat(e.target.value)
+ }
+ }))}
+ min="0.1"
+ max="10"
+ step="0.1"
+ className="w-full p-2 border rounded dark:bg-slate-700 dark:border-slate-600 dark:text-gray-200"
+ />
+
+ )}
+
+
+
+ {withdrawalPlan.autoStrategy?.type === 'maintain' && (
+ "The calculator will determine when your portfolio can sustain this withdrawal amount while maintaining its value."
+ )}
+ {withdrawalPlan.autoStrategy?.type === 'deplete' && (
+ "The calculator will determine when you can start withdrawing this amount to deplete the portfolio over your specified timeframe."
+ )}
+ {withdrawalPlan.autoStrategy?.type === 'grow' && (
+ "The calculator will determine when you can start withdrawing this amount while maintaining the target growth rate."
+ )}
+
+
+
+ )}
+
+
+
+
+ {sustainabilityAnalysis && withdrawalPlan.enabled && (
+
+
Withdrawal Analysis
+
+ To withdraw {formatCurrency(withdrawalPlan.amount)} {withdrawalPlan.interval}, you need to invest for{' '}
+ {sustainabilityAnalysis.yearsToReachTarget} years until your portfolio reaches{' '}
+ {formatCurrency(sustainabilityAnalysis.targetValue)} .
+
+
+ With this withdrawal plan, your portfolio will{' '}
+ {sustainabilityAnalysis.sustainableYears === 'infinite' ? (
+ remain sustainable indefinitely
+ ) : (
+ <>
+ last for{' '}
+
+ {sustainabilityAnalysis.sustainableYears} years
+ {' '}
+ {sustainabilityAnalysis.sustainableYears > parseInt(years) && (
+
+ (extends beyond the current chart view of {years} years)
+
+ )}
+ >
+ )}
+ .
+
+
+ )}
+
+
+
+ {renderChart()}
+
+ {renderScenarioChart()}
+
+
+
+
+ );
+};
diff --git a/src/components/PortfolioChart.tsx b/src/components/PortfolioChart.tsx
index c865969..9cb7ec7 100644
--- a/src/components/PortfolioChart.tsx
+++ b/src/components/PortfolioChart.tsx
@@ -1,172 +1,189 @@
-import { format } from "date-fns";
-import { X } from "lucide-react";
-import { useCallback, useMemo, useState } from "react";
-import { useDebouncedCallback } from "use-debounce";
-
-import { useDarkMode } from "../hooks/useDarkMode";
-import { usePortfolioSelector } from "../hooks/usePortfolio";
-import { getHistoricalData } from "../services/yahooFinanceService";
-import { DateRange } from "../types";
-import { calculatePortfolioValue } from "../utils/calculations/portfolioValue";
-import { getHexColor } from "../utils/formatters";
-import { ChartContent } from "./Chart/ChartContent";
-
-export default function PortfolioChart() {
- const [ isFullscreen, setIsFullscreen ] = useState(false);
- const [ hideAssets, setHideAssets ] = useState(false);
- const [ hiddenAssets, setHiddenAssets ] = useState>(new Set());
- const { isDarkMode } = useDarkMode();
-
- const { assets, dateRange, updateDateRange, updateAssetHistoricalData } = usePortfolioSelector((state) => ({
- assets: state.assets,
- dateRange: state.dateRange,
- updateDateRange: state.updateDateRange,
- updateAssetHistoricalData: state.updateAssetHistoricalData,
- }));
-
- const fetchHistoricalData = useCallback(
- async (startDate: Date, endDate: Date) => {
- for (const asset of assets) {
- const { historicalData, longName } = await getHistoricalData(asset.symbol, startDate, endDate);
- updateAssetHistoricalData(asset.id, historicalData, longName);
- }
- },
- [assets, updateAssetHistoricalData]
- );
-
- const debouncedFetchHistoricalData = useDebouncedCallback(fetchHistoricalData, 1500, {
- maxWait: 5000,
- });
-
- const assetColors: Record = useMemo(() => {
- const usedColors = new Set();
- return assets.reduce((colors, asset) => {
- const color = getHexColor(usedColors, isDarkMode);
- usedColors.add(color);
- return {
- ...colors,
- [asset.id]: color,
- };
- }, {});
- }, [assets, isDarkMode]);
-
- const data = useMemo(() => calculatePortfolioValue(assets, dateRange), [assets, dateRange]);
-
- const allAssetsInvestedKapitals = useMemo>(() => {
- const investedKapitals: Record = {};
-
- for (const asset of assets) {
- investedKapitals[asset.id] = asset.investments.reduce((acc, curr) => acc + curr.amount, 0);
- }
-
- return investedKapitals;
- }, [assets]);
-
- // Calculate percentage changes for each asset
- const processedData = useMemo(() => data.map(point => {
- const processed: { date: string, total: number, invested: number, percentageChange: number, ttwor: number, ttwor_percent: number, [key: string]: number | string } = {
- date: format(point.date, 'yyyy-MM-dd'),
- total: point.total,
- invested: point.invested,
- percentageChange: point.percentageChange,
- ttwor: 0,
- ttwor_percent: 0,
- };
-
- for (const asset of assets) {
- const initialPrice = data[0].assets[asset.id];
- const currentPrice = point.assets[asset.id];
- if (initialPrice && currentPrice) {
- processed[`${asset.id}_price`] = currentPrice;
- const percentDecimal = ((currentPrice - initialPrice) / initialPrice);
- processed[`${asset.id}_percent`] = percentDecimal * 100;
- processed.ttwor += allAssetsInvestedKapitals[asset.id] + allAssetsInvestedKapitals[asset.id] * percentDecimal;
- }
- }
-
- processed.ttwor_percent = (processed.ttwor - Object.values(allAssetsInvestedKapitals).reduce((acc, curr) => acc + curr, 0)) / Object.values(allAssetsInvestedKapitals).reduce((acc, curr) => acc + curr, 0) * 100;
-
-
- // add a processed["ttwor"] ttwor is what if you invested all of the kapital of all assets at the start of the period
- return processed;
- }), [data, assets, allAssetsInvestedKapitals]);
-
- const toggleAsset = useCallback((assetId: string) => {
- const newHiddenAssets = new Set(hiddenAssets);
- if (newHiddenAssets.has(assetId)) {
- newHiddenAssets.delete(assetId);
- } else {
- newHiddenAssets.add(assetId);
- }
- setHiddenAssets(newHiddenAssets);
- }, [hiddenAssets]);
-
- const toggleAllAssets = useCallback(() => {
- setHideAssets(!hideAssets);
- setHiddenAssets(new Set());
- }, [hideAssets]);
-
- const handleUpdateDateRange = useCallback((newRange: DateRange) => {
- updateDateRange(newRange);
- debouncedFetchHistoricalData(newRange.startDate, newRange.endDate);
- }, [updateDateRange, debouncedFetchHistoricalData]);
-
- const [renderKey, setRenderKey] = useState(0);
-
- const handleReRender = useCallback(() => {
- setRenderKey(prevKey => prevKey + 1);
- }, []);
-
- if (isFullscreen) {
- return (
-
-
-
Portfolio Chart
- setIsFullscreen(false)}
- className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
- >
-
-
-
-
-
- );
- }
-
- return (
-
-
-
- );
-};
+import { format } from "date-fns";
+import { X } from "lucide-react";
+import { useCallback, useMemo, useState } from "react";
+import { useDebouncedCallback } from "use-debounce";
+
+import { useDarkMode } from "../hooks/useDarkMode";
+import { usePortfolioSelector } from "../hooks/usePortfolio";
+import { getHistoricalData } from "../services/yahooFinanceService";
+import { DateRange } from "../types";
+import { calculatePortfolioValue } from "../utils/calculations/portfolioValue";
+import { getHexColor } from "../utils/formatters";
+import { ChartContent } from "./Chart/ChartContent";
+
+export default function PortfolioChart() {
+ const [ isFullscreen, setIsFullscreen ] = useState(false);
+ const [ hideAssets, setHideAssets ] = useState(false);
+ const [ hiddenAssets, setHiddenAssets ] = useState>(new Set());
+ const { isDarkMode } = useDarkMode();
+
+ const { assets, dateRange, updateDateRange, updateAssetHistoricalData, removeAsset } = usePortfolioSelector((state) => ({
+ assets: state.assets,
+ dateRange: state.dateRange,
+ updateDateRange: state.updateDateRange,
+ updateAssetHistoricalData: state.updateAssetHistoricalData,
+ removeAsset: state.removeAsset,
+ }));
+
+ const fetchHistoricalData = useCallback(
+ async (startDate: Date, endDate: Date) => {
+ for (const asset of assets) {
+ const { historicalData, longName } = await getHistoricalData(asset.symbol, startDate, endDate);
+ updateAssetHistoricalData(asset.id, historicalData, longName);
+ }
+ },
+ [assets, updateAssetHistoricalData]
+ );
+
+ const debouncedFetchHistoricalData = useDebouncedCallback(fetchHistoricalData, 1500, {
+ maxWait: 5000,
+ });
+
+ const assetColors: Record = useMemo(() => {
+ const usedColors = new Set();
+ return assets.reduce((colors, asset) => {
+ const color = getHexColor(usedColors, isDarkMode);
+ usedColors.add(color);
+ return {
+ ...colors,
+ [asset.id]: color,
+ };
+ }, {});
+ }, [assets, isDarkMode]);
+
+ const data = useMemo(() => calculatePortfolioValue(assets, dateRange), [assets, dateRange]);
+
+ const allAssetsInvestedKapitals = useMemo>(() => {
+ const investedKapitals: Record = {};
+
+ for (const asset of assets) {
+ investedKapitals[asset.id] = asset.investments.reduce((acc, curr) => acc + curr.amount, 0);
+ }
+
+ return investedKapitals;
+ }, [assets]);
+
+ // Compute the initial price for each asset as the first available value (instead of using data[0])
+ const initialPrices = useMemo(() => {
+ const prices: Record = {};
+ assets.forEach(asset => {
+ for (let i = 0; i < data.length; i++) {
+ const price = data[i].assets[asset.id];
+ if (price != null) { // check if data exists
+ prices[asset.id] = price;
+ break;
+ }
+ }
+ });
+ return prices;
+ }, [assets, data]);
+
+ // Calculate percentage changes for each asset using the first available price from initialPrices
+ const processedData = useMemo(() => data.map(point => {
+ const processed: { date: string, total: number, invested: number, percentageChange: number, ttwor: number, ttwor_percent: number, [key: string]: number | string } = {
+ date: format(point.date, 'yyyy-MM-dd'),
+ total: point.total,
+ invested: point.invested,
+ percentageChange: point.percentageChange,
+ ttwor: 0,
+ ttwor_percent: 0,
+ };
+
+ for (const asset of assets) {
+ const initialPrice = initialPrices[asset.id]; // use the newly computed initial price
+ const currentPrice = point.assets[asset.id];
+ if (initialPrice && currentPrice) {
+ processed[`${asset.id}_price`] = currentPrice;
+ const percentDecimal = ((currentPrice - initialPrice) / initialPrice);
+ processed[`${asset.id}_percent`] = percentDecimal * 100;
+ processed.ttwor += allAssetsInvestedKapitals[asset.id] + allAssetsInvestedKapitals[asset.id] * percentDecimal;
+ }
+ }
+
+ processed.ttwor_percent = (processed.ttwor - Object.values(allAssetsInvestedKapitals).reduce((acc, curr) => acc + curr, 0)) / Object.values(allAssetsInvestedKapitals).reduce((acc, curr) => acc + curr, 0) * 100;
+ return processed;
+ }), [data, assets, allAssetsInvestedKapitals, initialPrices]);
+
+ const toggleAsset = useCallback((assetId: string) => {
+ const newHiddenAssets = new Set(hiddenAssets);
+ if (newHiddenAssets.has(assetId)) {
+ newHiddenAssets.delete(assetId);
+ } else {
+ newHiddenAssets.add(assetId);
+ }
+ setHiddenAssets(newHiddenAssets);
+ }, [hiddenAssets]);
+
+ const toggleAllAssets = useCallback(() => {
+ setHideAssets(!hideAssets);
+ setHiddenAssets(new Set());
+ }, [hideAssets]);
+
+ const handleUpdateDateRange = useCallback((newRange: DateRange) => {
+ updateDateRange(newRange);
+ debouncedFetchHistoricalData(newRange.startDate, newRange.endDate);
+ }, [updateDateRange, debouncedFetchHistoricalData]);
+
+ const [renderKey, setRenderKey] = useState(0);
+
+ const handleReRender = useCallback(() => {
+ setRenderKey(prevKey => prevKey + 1);
+ }, []);
+
+ console.log(processedData);
+ console.log("TEST")
+ if (isFullscreen) {
+ return (
+
+
+
Portfolio Chart
+ setIsFullscreen(false)}
+ className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
+ >
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/PortfolioTable.tsx b/src/components/PortfolioTable.tsx
index afe9d2c..dd3d7a6 100644
--- a/src/components/PortfolioTable.tsx
+++ b/src/components/PortfolioTable.tsx
@@ -1,603 +1,603 @@
-import { format, isBefore } from "date-fns";
-import {
- BarChart, BarChart2, Download, FileDown, LineChart, Loader2, Pencil, RefreshCw, ShoppingBag,
- Trash2, TrendingDown, TrendingUp
-} from "lucide-react";
-import { memo, useCallback, useMemo, useState } from "react";
-import toast from "react-hot-toast";
-
-import { usePortfolioSelector } from "../hooks/usePortfolio";
-import { Investment } from "../types";
-import { calculateInvestmentPerformance } from "../utils/calculations/performance";
-import { downloadTableAsCSV, generatePortfolioPDF } from "../utils/export";
-import { AssetPerformanceModal } from "./Chart/AssetPerformanceModal";
-import { PortfolioPerformanceModal } from "./Chart/PortfolioPerformanceModal";
-import { EditInvestmentModal } from "./Modals/EditInvestmentModal";
-import { EditSavingsPlanModal } from "./Modals/EditSavingsPlanModal";
-import { FutureProjectionModal } from "./Modals/FutureProjectionModal";
-import { Tooltip } from "./utils/ToolTip";
-
-interface SavingsPlanPerformance {
- assetName: string;
- amount: number;
- totalInvested: number;
- currentValue: number;
- performancePercentage: number;
- performancePerAnnoPerformance: number;
- allocation?: number;
-}
-
-export default memo(function PortfolioTable() {
- const { assets, removeInvestment, clearInvestments } = usePortfolioSelector((state) => ({
- assets: state.assets,
- removeInvestment: state.removeInvestment,
- clearInvestments: state.clearInvestments,
- }));
-
- const [editingInvestment, setEditingInvestment] = useState<{
- investment: Investment;
- assetId: string;
- } | null>(null);
- const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
- const [isUpdatingSavingsPlan, setIsUpdatingSavingsPlan] = useState(false);
- const [editingSavingsPlan, setEditingSavingsPlan] = useState<{
- assetId: string;
- groupId: string;
- amount: number;
- dayOfMonth: number;
- interval: number;
- dynamic?: {
- type: 'percentage' | 'fixed';
- value: number;
- yearInterval: number;
- };
- } | null>(null);
- const [showPortfolioPerformance, setShowPortfolioPerformance] = useState(false);
-
- const performance = useMemo(() => calculateInvestmentPerformance(assets), [assets]);
-
- const averagePerformance = useMemo(() => {
- return ((performance.investments.reduce((sum, inv) => sum + inv.performancePercentage, 0) / performance.investments.length) || 0).toFixed(2);
- }, [performance.investments]);
-
- const handleDelete = useCallback((investmentId: string, assetId: string) => {
- if (window.confirm("Are you sure you want to delete this investment?")) {
- try {
- removeInvestment(assetId, investmentId);
- toast.success('Investment deleted successfully');
- } catch (error:any) {
- toast.error('Failed to delete investment' + String(error?.message || error));
- }
- }
- }, [removeInvestment]);
-
- const handleClearAll = useCallback(() => {
- if (window.confirm("Are you sure you want to clear all investments?")) {
- try {
- clearInvestments();
- toast.success('All investments cleared successfully');
- } catch (error:any) {
- toast.error('Failed to clear investments' + String(error?.message || error));
- }
- }
- }, [clearInvestments]);
-
- const performanceTooltip = useMemo(() => (
-
-
The performance of your portfolio is {performance.summary.performancePercentage.toFixed(2)}%
-
The average (acc.) performance of all positions is {averagePerformance}%
-
The average (p.a.) performance of every year is {(performance.summary.performancePerAnnoPerformance || 0)?.toFixed(2)}%
-
Best p.a.: {(performance.summary.bestPerformancePerAnno?.[0]?.percentage || 0)?.toFixed(2)}% ({performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})
-
Worst p.a.: {(performance.summary.worstPerformancePerAnno?.[0]?.percentage || 0)?.toFixed(2)}% ({performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})
-
- Note: An average performance of positions doesn't always match your entire portfolio's average,
- especially with single investments or investments on different time ranges.
-
-
- ), [performance.summary.performancePercentage, averagePerformance, performance.summary.performancePerAnnoPerformance, performance.summary.bestPerformancePerAnno, performance.summary.worstPerformancePerAnno]);
-
- const buyInTooltip = useMemo(() => (
-
-
"Buy-in" shows the asset's price when that position was bought.
-
"Avg" shows the average buy-in price across all positions for that asset.
-
- ), []);
-
- const currentAmountTooltip = useMemo(() => (
- "The current value of your investment based on the latest market price."
- ), []);
-
- const ttworTooltip = useMemo(() => (
-
-
Time Travel Without Risk (TTWOR) shows how your portfolio would have performed if all investments had been made at the beginning of the period.
-
- It helps to evaluate the impact of your investment timing strategy compared to a single early investment.
-
-
- ), []);
-
- const [showProjection, setShowProjection] = useState(false);
-
- const isSavingsPlanOverviewDisabled = useMemo(() => {
- return !assets.some(asset => asset.investments.some(inv => inv.type === 'periodic'));
- }, [assets]);
-
- const savingsPlansPerformance = useMemo(() => {
- if(isSavingsPlanOverviewDisabled) return [];
- const performance: SavingsPlanPerformance[] = [];
- const totalSavingsPlansAmount = assets
- .map(v => v.investments)
- .flat()
- .filter(inv => inv.type === 'periodic')
- .reduce((sum, inv) => sum + inv.amount, 0);
-
- // Second pass to calculate individual performances with allocation
- for (const asset of assets) {
- const savingsPlans = asset.investments.filter(inv => inv.type === 'periodic');
- const amount = savingsPlans.reduce((sum, inv) => sum + inv.amount, 0);
- if (savingsPlans.length > 0) {
- const assetPerformance = calculateInvestmentPerformance([{
- ...asset,
- investments: savingsPlans
- }]);
- performance.push({
- assetName: asset.name,
- amount: savingsPlans[0].amount,
- ...assetPerformance.summary,
- allocation: amount / totalSavingsPlansAmount * 100
- });
- }
- }
- return performance;
- }, [assets, isSavingsPlanOverviewDisabled]);
-
- const savingsPlansSummary = useMemo(() => {
- if (savingsPlansPerformance.length === 0) return null;
-
- const totalCurrentValue = savingsPlansPerformance.reduce((sum, plan) => sum + plan.currentValue, 0);
- const totalInvested = savingsPlansPerformance.reduce((sum, plan) => sum + plan.totalInvested, 0);
- const weightedPerformance = savingsPlansPerformance.reduce((sum, plan) => {
- return sum + (plan.performancePercentage * (plan.currentValue / totalCurrentValue));
- }, 0);
- const weightedPerformancePA = savingsPlansPerformance.reduce((sum, plan) => {
- return sum + (plan.performancePerAnnoPerformance * (plan.currentValue / totalCurrentValue));
- }, 0);
-
- return {
- totalAmount: savingsPlansPerformance.reduce((sum, plan) => sum + plan.amount, 0),
- totalInvested,
- totalCurrentValue,
- weightedPerformance,
- weightedPerformancePA,
- };
- }, [savingsPlansPerformance]);
-
- const handleGeneratePDF = async () => {
- setIsGeneratingPDF(true);
- try {
- await generatePortfolioPDF(
- assets,
- performance,
- savingsPlansPerformance,
- performance.summary.performancePerAnnoPerformance
- );
- toast.success('PDF generated successfully');
- } catch (error:any) {
- toast.error('Failed to generate PDF' + String(error?.message || error));
- } finally {
- setIsGeneratingPDF(false);
- }
- };
-
- const handleDeleteSavingsPlan = useCallback((assetId: string, groupId: string) => {
- if (window.confirm("Are you sure you want to delete this savings plan? All related investments will be removed.")) {
- try {
- setIsUpdatingSavingsPlan(true);
- setTimeout(() => {
- try {
- const asset = assets.find(a => a.id === assetId);
- if (!asset) throw new Error('Asset not found');
- const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
- investments.forEach(inv => {
- removeInvestment(assetId, inv.id);
- });
- toast.success('Savings plan deleted successfully');
- } catch (error:any) {
- toast.error('Failed to delete savings plan: ' + String(error?.message || error));
- } finally {
- setIsUpdatingSavingsPlan(false);
- }
- }, 10);
- } catch (error:any) {
- toast.error('Failed to delete savings plan: ' + String(error?.message || error));
- }
- }
- }, [assets, removeInvestment]);
-
- const [selectedAsset, setSelectedAsset] = useState<{
- name: string;
- performances: { year: number; percentage: number }[];
- } | null>(null);
-
- return (
-
-
-
-
- Assets Performance Overview
-
-
Calculated performance of each asset as of "paper"
-
- {assets.map(asset => {
- const datas = Array.from(asset.historicalData.values());
- const startPrice = datas.shift();
- const endPrice = datas.pop();
- const avgPerformance = performance.summary.annualPerformancesPerAsset.get(asset.id);
- const averagePerf = ((avgPerformance?.reduce?.((acc, curr) => acc + curr.percentage, 0) || 0) / (avgPerformance?.length || 1));
-
- return (
-
-
{asset.name}
-
-
-
-
- Start Price:
- Current Price:
-
-
-
-
- β¬{startPrice?.toFixed(2) || 'N/A'}
- β¬{endPrice?.toFixed(2) || 'N/A'}
- ({endPrice && startPrice && endPrice - startPrice > 0 ? '+' : ''}{endPrice && startPrice && ((endPrice - startPrice) / startPrice * 100).toFixed(2)}%)
-
-
-
-
-
avgPerformance && setSelectedAsset({
- name: asset.name,
- performances: avgPerformance
- })}
- className="w-full mt-2 p-3 border bg-gray-100 border-gray-500 dark:border-gray-500 dark:bg-slate-800 rounded-lg flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
- >
- Avg. Performance:
- = 0 ? 'text-green-500' : 'text-red-500'
- }`}>
- {averagePerf.toFixed(2)}%
- {averagePerf >= 0 ? (
-
- ) : (
-
- )}
-
-
-
-
- );
- })}
-
- {selectedAsset && (
-
setSelectedAsset(null)}
- />
- )}
-
-
-
-
Portfolio's Positions Overview
-
-
- Clear All Investments
-
- setShowProjection(true)}
- disabled={performance.investments.length === 0}
- className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
- Future Projection
-
-
-
- {isGeneratingPDF ? (
-
- ) : (
-
- )}
- {isGeneratingPDF ? 'Generating...' : 'Save Analysis'}
-
-
- setShowPortfolioPerformance(true)}
- disabled={performance.investments.length === 0}
- className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
- Portfolio Performance History
-
-
-
-
- {!isSavingsPlanOverviewDisabled && savingsPlansPerformance.length > 0 && (
-
-
-
Savings Plans Performance
- downloadTableAsCSV(savingsPlansPerformance, 'savings-plans-performance')}
- className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
- title="Download CSV"
- >
-
-
-
-
-
-
- Asset
- Interval Amount
- Allocation
- Total Invested
- Current Value
- Performance (%)
- Performance (p.a.)
- Actions
-
-
-
- {savingsPlansSummary && (
-
- Total
- β¬{savingsPlansSummary.totalAmount.toFixed(2)}
- 100%
- β¬{savingsPlansSummary.totalInvested.toFixed(2)}
- β¬{savingsPlansSummary.totalCurrentValue.toFixed(2)}
- {savingsPlansSummary.weightedPerformance.toFixed(2)}%
- {savingsPlansSummary.weightedPerformancePA.toFixed(2)}%
-
-
- )}
- {savingsPlansPerformance.sort((a, b) => Number(b.allocation || 0) - Number(a.allocation || 0)).map((plan) => {
- const asset = assets.find(a => a.name === plan.assetName)!;
- const firstInvestment = asset.investments.find(inv => inv.type === 'periodic')!;
- const groupId = firstInvestment.periodicGroupId!;
-
- return (
-
- {plan.assetName}
- β¬{plan.amount.toFixed(2)}
- {plan.allocation?.toFixed(2)}%
- β¬{plan.totalInvested.toFixed(2)}
- β¬{plan.currentValue.toFixed(2)}
- {plan.performancePercentage.toFixed(2)}%
- {plan.performancePerAnnoPerformance.toFixed(2)}%
-
-
-
setEditingSavingsPlan({
- assetId: asset.id,
- groupId,
- amount: firstInvestment.amount,
- dayOfMonth: firstInvestment.date?.getDate() || 0,
- interval: 1,
- })}
- className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
- >
- {isUpdatingSavingsPlan || editingSavingsPlan ? (
-
- ) : (
- )}
-
-
handleDeleteSavingsPlan(asset.id, groupId)}
- className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded text-red-500 transition-colors"
- >
- {isUpdatingSavingsPlan || editingSavingsPlan ? (
-
- ) : (
-
- )}
-
-
-
-
- );
- })}
-
-
-
- )}
-
-
-
-
Positions Overview
- downloadTableAsCSV([
- {
- id: "",
- assetName: "Total Portfolio",
- date: "",
- investedAmount: performance.summary.totalInvested.toFixed(2),
- investedAtPrice: "",
- currentValue: performance.summary.currentValue.toFixed(2),
- performancePercentage: `${performance.summary.performancePercentage.toFixed(2)}% (avg. acc. ${averagePerformance}%) (avg. p.a. ${(performance.summary.performancePerAnnoPerformance || 0).toFixed(2)}%)`,
- periodicGroupId: "",
- },
- {
- id: "",
- assetName: "TTWOR",
- date: "",
- investedAmount: performance.summary.totalInvested.toFixed(2),
- investedAtPrice: "",
- currentValue: performance.summary.ttworValue.toFixed(2),
- performancePercentage: `${performance.summary.ttworPercentage.toFixed(2)}%`,
- periodicGroupId: "",
- },
- ...performance.investments
- ], 'portfolio-positions')}
- className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
- title="Download CSV"
- >
-
-
-
-
-
-
-
- Asset
- Type
- Date
- Invested Amount
-
-
- Current Amount
-
-
-
-
- Buy-In (avg)
-
-
-
-
- Performance (%)
-
-
- Actions
-
-
-
- {performance.summary && (
- <>
-
-
- Total Portfolio
-
-
- β¬{performance.summary.totalInvested.toFixed(2)}
- β¬{performance.summary.currentValue.toFixed(2)}
-
-
- {performance.summary.performancePercentage.toFixed(2)}%
-
- (avg. acc. {averagePerformance}%)
- (avg. p.a. {(performance.summary.performancePerAnnoPerformance || 0).toFixed(2)}%)
- (best p.a. {performance.summary.bestPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})
- (worst p.a. {performance.summary.worstPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})
-
-
-
-
-
-
-
- TTWOR
-
- {new Date(performance.investments[0]?.date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
- β¬{performance.summary.totalInvested.toFixed(2)}
- β¬{performance.summary.ttworValue.toFixed(2)}
-
- {performance.summary.ttworPercentage.toFixed(2)}%
-
-
- >
- )}
- {performance.investments.sort((a, b) => isBefore(a.date, b.date) ? -1 : 1).map((inv, index) => {
- const asset = assets.find(a => a.name === inv.assetName)!;
- const investment = asset.investments.find(i => i.id === inv.id)! || inv;
- const filtered = performance.investments.filter(v => v.assetName === inv.assetName);
- const avgBuyIn = filtered.reduce((acc, curr) => acc + curr.investedAtPrice, 0) / filtered.length;
- const isLast = index === performance.investments.length - 1;
-
- return (
-
- {inv.assetName}
-
- {investment?.type === 'periodic' ? (
-
-
- SavingsPlan
-
- ) : (
-
-
- OneTime
-
- )}
-
- {format(new Date(inv.date), 'dd.MM.yyyy')}
- β¬{inv.investedAmount.toFixed(2)}
- β¬{inv.currentValue.toFixed(2)}
- β¬{inv.investedAtPrice.toFixed(2)} (β¬{avgBuyIn.toFixed(2)})
- {inv.performancePercentage.toFixed(2)}%
-
-
-
setEditingInvestment({ investment, assetId: asset.id })}
- className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
- >
-
-
-
handleDelete(inv.id, asset.id)}
- className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded text-red-500 transition-colors"
- >
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
- {editingInvestment && (
-
setEditingInvestment(null)}
- />
- )}
- {showProjection && (
- setShowProjection(false)}
- />
- )}
- {editingSavingsPlan && (
- setEditingSavingsPlan(null)}
- />
- )}
- {showPortfolioPerformance && (
- setShowPortfolioPerformance(false)}
- />
- )}
-
- );
-});
+import { format, isBefore } from "date-fns";
+import {
+ BarChart, BarChart2, Download, FileDown, LineChart, Loader2, Pencil, RefreshCw, ShoppingBag,
+ Trash2, TrendingDown, TrendingUp
+} from "lucide-react";
+import { memo, useCallback, useMemo, useState } from "react";
+import toast from "react-hot-toast";
+
+import { usePortfolioSelector } from "../hooks/usePortfolio";
+import { Investment } from "../types";
+import { calculateInvestmentPerformance } from "../utils/calculations/performance";
+import { downloadTableAsCSV, generatePortfolioPDF } from "../utils/export";
+import { AssetPerformanceModal } from "./Chart/AssetPerformanceModal";
+import { PortfolioPerformanceModal } from "./Chart/PortfolioPerformanceModal";
+import { EditInvestmentModal } from "./Modals/EditInvestmentModal";
+import { EditSavingsPlanModal } from "./Modals/EditSavingsPlanModal";
+import { FutureProjectionModal } from "./Modals/FutureProjectionModal";
+import { Tooltip } from "./utils/ToolTip";
+
+interface SavingsPlanPerformance {
+ assetName: string;
+ amount: number;
+ totalInvested: number;
+ currentValue: number;
+ performancePercentage: number;
+ performancePerAnnoPerformance: number;
+ allocation?: number;
+}
+
+export default memo(function PortfolioTable() {
+ const { assets, removeInvestment, clearInvestments } = usePortfolioSelector((state) => ({
+ assets: state.assets,
+ removeInvestment: state.removeInvestment,
+ clearInvestments: state.clearInvestments,
+ }));
+
+ const [editingInvestment, setEditingInvestment] = useState<{
+ investment: Investment;
+ assetId: string;
+ } | null>(null);
+ const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
+ const [isUpdatingSavingsPlan, setIsUpdatingSavingsPlan] = useState(false);
+ const [editingSavingsPlan, setEditingSavingsPlan] = useState<{
+ assetId: string;
+ groupId: string;
+ amount: number;
+ dayOfMonth: number;
+ interval: number;
+ dynamic?: {
+ type: 'percentage' | 'fixed';
+ value: number;
+ yearInterval: number;
+ };
+ } | null>(null);
+ const [showPortfolioPerformance, setShowPortfolioPerformance] = useState(false);
+
+ const performance = useMemo(() => calculateInvestmentPerformance(assets), [assets]);
+
+ const averagePerformance = useMemo(() => {
+ return ((performance.investments.reduce((sum, inv) => sum + inv.performancePercentage, 0) / performance.investments.length) || 0).toFixed(2);
+ }, [performance.investments]);
+
+ const handleDelete = useCallback((investmentId: string, assetId: string) => {
+ if (window.confirm("Are you sure you want to delete this investment?")) {
+ try {
+ removeInvestment(assetId, investmentId);
+ toast.success('Investment deleted successfully');
+ } catch (error:any) {
+ toast.error('Failed to delete investment' + String(error?.message || error));
+ }
+ }
+ }, [removeInvestment]);
+
+ const handleClearAll = useCallback(() => {
+ if (window.confirm("Are you sure you want to clear all investments?")) {
+ try {
+ clearInvestments();
+ toast.success('All investments cleared successfully');
+ } catch (error:any) {
+ toast.error('Failed to clear investments' + String(error?.message || error));
+ }
+ }
+ }, [clearInvestments]);
+
+ const performanceTooltip = useMemo(() => (
+
+
The performance of your portfolio is {performance.summary.performancePercentage.toFixed(2)}%
+
The average (acc.) performance of all positions is {averagePerformance}%
+
The average (p.a.) performance of every year is {(performance.summary.performancePerAnnoPerformance || 0)?.toFixed(2)}%
+
Best p.a.: {(performance.summary.bestPerformancePerAnno?.[0]?.percentage || 0)?.toFixed(2)}% ({performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})
+
Worst p.a.: {(performance.summary.worstPerformancePerAnno?.[0]?.percentage || 0)?.toFixed(2)}% ({performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})
+
+ Note: An average performance of positions doesn't always match your entire portfolio's average,
+ especially with single investments or investments on different time ranges.
+
+
+ ), [performance.summary.performancePercentage, averagePerformance, performance.summary.performancePerAnnoPerformance, performance.summary.bestPerformancePerAnno, performance.summary.worstPerformancePerAnno]);
+
+ const buyInTooltip = useMemo(() => (
+
+
"Buy-in" shows the asset's price when that position was bought.
+
"Avg" shows the average buy-in price across all positions for that asset.
+
+ ), []);
+
+ const currentAmountTooltip = useMemo(() => (
+ "The current value of your investment based on the latest market price."
+ ), []);
+
+ const ttworTooltip = useMemo(() => (
+
+
Time Travel Without Risk (TTWOR) shows how your portfolio would have performed if all investments had been made at the beginning of the period.
+
+ It helps to evaluate the impact of your investment timing strategy compared to a single early investment.
+
+
+ ), []);
+
+ const [showProjection, setShowProjection] = useState(false);
+
+ const isSavingsPlanOverviewDisabled = useMemo(() => {
+ return !assets.some(asset => asset.investments.some(inv => inv.type === 'periodic'));
+ }, [assets]);
+
+ const savingsPlansPerformance = useMemo(() => {
+ if(isSavingsPlanOverviewDisabled) return [];
+ const performance: SavingsPlanPerformance[] = [];
+ const totalSavingsPlansAmount = assets
+ .map(v => v.investments)
+ .flat()
+ .filter(inv => inv.type === 'periodic')
+ .reduce((sum, inv) => sum + inv.amount, 0);
+
+ // Second pass to calculate individual performances with allocation
+ for (const asset of assets) {
+ const savingsPlans = asset.investments.filter(inv => inv.type === 'periodic');
+ const amount = savingsPlans.reduce((sum, inv) => sum + inv.amount, 0);
+ if (savingsPlans.length > 0) {
+ const assetPerformance = calculateInvestmentPerformance([{
+ ...asset,
+ investments: savingsPlans
+ }]);
+ performance.push({
+ assetName: asset.name,
+ amount: savingsPlans[0].amount,
+ ...assetPerformance.summary,
+ allocation: amount / totalSavingsPlansAmount * 100
+ });
+ }
+ }
+ return performance;
+ }, [assets, isSavingsPlanOverviewDisabled]);
+
+ const savingsPlansSummary = useMemo(() => {
+ if (savingsPlansPerformance.length === 0) return null;
+
+ const totalCurrentValue = savingsPlansPerformance.reduce((sum, plan) => sum + plan.currentValue, 0);
+ const totalInvested = savingsPlansPerformance.reduce((sum, plan) => sum + plan.totalInvested, 0);
+ const weightedPerformance = savingsPlansPerformance.reduce((sum, plan) => {
+ return sum + (plan.performancePercentage * (plan.currentValue / totalCurrentValue));
+ }, 0);
+ const weightedPerformancePA = savingsPlansPerformance.reduce((sum, plan) => {
+ return sum + (plan.performancePerAnnoPerformance * (plan.currentValue / totalCurrentValue));
+ }, 0);
+
+ return {
+ totalAmount: savingsPlansPerformance.reduce((sum, plan) => sum + plan.amount, 0),
+ totalInvested,
+ totalCurrentValue,
+ weightedPerformance,
+ weightedPerformancePA,
+ };
+ }, [savingsPlansPerformance]);
+
+ const handleGeneratePDF = async () => {
+ setIsGeneratingPDF(true);
+ try {
+ await generatePortfolioPDF(
+ assets,
+ performance,
+ savingsPlansPerformance,
+ performance.summary.performancePerAnnoPerformance
+ );
+ toast.success('PDF generated successfully');
+ } catch (error:any) {
+ toast.error('Failed to generate PDF' + String(error?.message || error));
+ } finally {
+ setIsGeneratingPDF(false);
+ }
+ };
+
+ const handleDeleteSavingsPlan = useCallback((assetId: string, groupId: string) => {
+ if (window.confirm("Are you sure you want to delete this savings plan? All related investments will be removed.")) {
+ try {
+ setIsUpdatingSavingsPlan(true);
+ setTimeout(() => {
+ try {
+ const asset = assets.find(a => a.id === assetId);
+ if (!asset) throw new Error('Asset not found');
+ const investments = asset.investments.filter(inv => inv.periodicGroupId === groupId);
+ investments.forEach(inv => {
+ removeInvestment(assetId, inv.id);
+ });
+ toast.success('Savings plan deleted successfully');
+ } catch (error:any) {
+ toast.error('Failed to delete savings plan: ' + String(error?.message || error));
+ } finally {
+ setIsUpdatingSavingsPlan(false);
+ }
+ }, 10);
+ } catch (error:any) {
+ toast.error('Failed to delete savings plan: ' + String(error?.message || error));
+ }
+ }
+ }, [assets, removeInvestment]);
+
+ const [selectedAsset, setSelectedAsset] = useState<{
+ name: string;
+ performances: { year: number; percentage: number }[];
+ } | null>(null);
+
+ return (
+
+
+
+
+ Assets Performance Overview
+
+
Calculated performance of each asset as of "paper"
+
+ {assets.map(asset => {
+ const datas = Array.from(asset.historicalData.values());
+ const startPrice = datas.shift();
+ const endPrice = datas.pop();
+ const avgPerformance = performance.summary.annualPerformancesPerAsset.get(asset.id);
+ const averagePerf = ((avgPerformance?.reduce?.((acc, curr) => acc + curr.percentage, 0) || 0) / (avgPerformance?.length || 1));
+
+ return (
+
+
{asset.name}
+
+
+
+
+ Start Price:
+ Current Price:
+
+
+
+
+ β¬{startPrice?.toFixed(2) || 'N/A'}
+ β¬{endPrice?.toFixed(2) || 'N/A'}
+ ({endPrice && startPrice && endPrice - startPrice > 0 ? '+' : ''}{endPrice && startPrice && ((endPrice - startPrice) / startPrice * 100).toFixed(2)}%)
+
+
+
+
+
avgPerformance && setSelectedAsset({
+ name: asset.name,
+ performances: avgPerformance
+ })}
+ className="w-full mt-2 p-3 border bg-gray-100 border-gray-500 dark:border-gray-500 dark:bg-slate-800 rounded-lg flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
+ >
+ Avg. Performance:
+ = 0 ? 'text-green-500' : 'text-red-500'
+ }`}>
+ {averagePerf.toFixed(2)}%
+ {averagePerf >= 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+ })}
+
+ {selectedAsset && (
+
setSelectedAsset(null)}
+ />
+ )}
+
+
+
+
Portfolio's Positions Overview
+
+
+ Clear All Investments
+
+ setShowProjection(true)}
+ disabled={performance.investments.length === 0}
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+
+ Future Projection
+
+
+
+ {isGeneratingPDF ? (
+
+ ) : (
+
+ )}
+ {isGeneratingPDF ? 'Generating...' : 'Save Analysis'}
+
+
+ setShowPortfolioPerformance(true)}
+ disabled={performance.investments.length === 0}
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+
+ Portfolio Performance History
+
+
+
+
+ {!isSavingsPlanOverviewDisabled && savingsPlansPerformance.length > 0 && (
+
+
+
Savings Plans Performance
+ downloadTableAsCSV(savingsPlansPerformance, 'savings-plans-performance')}
+ className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
+ title="Download CSV"
+ >
+
+
+
+
+
+
+ Asset
+ Interval Amount
+ Allocation
+ Total Invested
+ Current Value
+ Performance (%)
+ Performance (p.a.)
+ Actions
+
+
+
+ {savingsPlansSummary && (
+
+ Total
+ β¬{savingsPlansSummary.totalAmount.toFixed(2)}
+ 100%
+ β¬{savingsPlansSummary.totalInvested.toFixed(2)}
+ β¬{savingsPlansSummary.totalCurrentValue.toFixed(2)}
+ {savingsPlansSummary.weightedPerformance.toFixed(2)}%
+ {savingsPlansSummary.weightedPerformancePA.toFixed(2)}%
+
+
+ )}
+ {savingsPlansPerformance.sort((a, b) => Number(b.allocation || 0) - Number(a.allocation || 0)).map((plan) => {
+ const asset = assets.find(a => a.name === plan.assetName)!;
+ const firstInvestment = asset.investments.find(inv => inv.type === 'periodic')!;
+ const groupId = firstInvestment.periodicGroupId!;
+
+ return (
+
+ {plan.assetName}
+ β¬{plan.amount.toFixed(2)}
+ {plan.allocation?.toFixed(2)}%
+ β¬{plan.totalInvested.toFixed(2)}
+ β¬{plan.currentValue.toFixed(2)}
+ {plan.performancePercentage.toFixed(2)}%
+ {plan.performancePerAnnoPerformance.toFixed(2)}%
+
+
+
setEditingSavingsPlan({
+ assetId: asset.id,
+ groupId,
+ amount: firstInvestment.amount,
+ dayOfMonth: firstInvestment.date?.getDate() || 0,
+ interval: 1,
+ })}
+ className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
+ >
+ {isUpdatingSavingsPlan || editingSavingsPlan ? (
+
+ ) : (
+ )}
+
+
handleDeleteSavingsPlan(asset.id, groupId)}
+ className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded text-red-500 transition-colors"
+ >
+ {isUpdatingSavingsPlan || editingSavingsPlan ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+
+
Positions Overview
+ downloadTableAsCSV([
+ {
+ id: "",
+ assetName: "Total Portfolio",
+ date: "",
+ investedAmount: performance.summary.totalInvested.toFixed(2),
+ investedAtPrice: "",
+ currentValue: performance.summary.currentValue.toFixed(2),
+ performancePercentage: `${performance.summary.performancePercentage.toFixed(2)}% (avg. acc. ${averagePerformance}%) (avg. p.a. ${(performance.summary.performancePerAnnoPerformance || 0).toFixed(2)}%)`,
+ periodicGroupId: "",
+ },
+ {
+ id: "",
+ assetName: "TTWOR",
+ date: "",
+ investedAmount: performance.summary.totalInvested.toFixed(2),
+ investedAtPrice: "",
+ currentValue: performance.summary.ttworValue.toFixed(2),
+ performancePercentage: `${performance.summary.ttworPercentage.toFixed(2)}%`,
+ periodicGroupId: "",
+ },
+ ...performance.investments
+ ], 'portfolio-positions')}
+ className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
+ title="Download CSV"
+ >
+
+
+
+
+
+
+
+ Asset
+ Type
+ Date
+ Invested Amount
+
+
+ Current Amount
+
+
+
+
+ Buy-In (avg)
+
+
+
+
+ Performance (%)
+
+
+ Actions
+
+
+
+ {performance.summary && (
+ <>
+
+
+ Total Portfolio
+
+
+ β¬{performance.summary.totalInvested.toFixed(2)}
+ β¬{performance.summary.currentValue.toFixed(2)}
+
+
+ {performance.summary.performancePercentage.toFixed(2)}%
+
+ (avg. acc. {averagePerformance}%)
+ (avg. p.a. {(performance.summary.performancePerAnnoPerformance || 0).toFixed(2)}%)
+ (best p.a. {performance.summary.bestPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.bestPerformancePerAnno?.[0]?.year || "N/A"})
+ (worst p.a. {performance.summary.worstPerformancePerAnno?.[0]?.percentage?.toFixed(2) || "0.00"}% {performance.summary.worstPerformancePerAnno?.[0]?.year || "N/A"})
+
+
+
+
+
+
+
+ TTWOR
+
+ {new Date(performance.investments[0]?.date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
+ β¬{performance.summary.totalInvested.toFixed(2)}
+ β¬{performance.summary.ttworValue.toFixed(2)}
+
+ {performance.summary.ttworPercentage.toFixed(2)}%
+
+
+ >
+ )}
+ {performance.investments.sort((a, b) => isBefore(a.date, b.date) ? -1 : 1).map((inv, index) => {
+ const asset = assets.find(a => a.name === inv.assetName)!;
+ const investment = asset.investments.find(i => i.id === inv.id)! || inv;
+ const filtered = performance.investments.filter(v => v.assetName === inv.assetName);
+ const avgBuyIn = filtered.reduce((acc, curr) => acc + curr.investedAtPrice, 0) / filtered.length;
+ const isLast = index === performance.investments.length - 1;
+
+ return (
+
+ {inv.assetName}
+
+ {investment?.type === 'periodic' ? (
+
+
+ SavingsPlan
+
+ ) : (
+
+
+ OneTime
+
+ )}
+
+ {format(new Date(inv.date), 'dd.MM.yyyy')}
+ β¬{inv.investedAmount.toFixed(2)}
+ β¬{inv.currentValue.toFixed(2)}
+ β¬{inv.investedAtPrice.toFixed(2)} (β¬{avgBuyIn.toFixed(2)})
+ {inv.performancePercentage.toFixed(2)}%
+
+
+
setEditingInvestment({ investment, assetId: asset.id })}
+ className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded transition-colors"
+ >
+
+
+
handleDelete(inv.id, asset.id)}
+ className="p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded text-red-500 transition-colors"
+ >
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ {editingInvestment && (
+
setEditingInvestment(null)}
+ />
+ )}
+ {showProjection && (
+ setShowProjection(false)}
+ />
+ )}
+ {editingSavingsPlan && (
+ setEditingSavingsPlan(null)}
+ />
+ )}
+ {showPortfolioPerformance && (
+ setShowPortfolioPerformance(false)}
+ />
+ )}
+
+ );
+});
diff --git a/src/components/utils/DateRangePicker.tsx b/src/components/utils/DateRangePicker.tsx
index 2125107..65e84d8 100644
--- a/src/components/utils/DateRangePicker.tsx
+++ b/src/components/utils/DateRangePicker.tsx
@@ -1,93 +1,93 @@
-import { useRef } from "react";
-import { useDebouncedCallback } from "use-debounce";
-
-import { useLocaleDateFormat } from "../../hooks/useLocalDateFormat";
-import { formatDateToISO, isValidDate } from "../../utils/formatters";
-
-interface DateRangePickerProps {
- startDate: Date;
- endDate: Date;
- onStartDateChange: (date: Date) => void;
- onEndDateChange: (date: Date) => void;
-}
-
-export const DateRangePicker = ({
- startDate,
- endDate,
- onStartDateChange,
- onEndDateChange,
-}: DateRangePickerProps) => {
- const startDateRef = useRef(null);
- const endDateRef = useRef(null);
- const localeDateFormat = useLocaleDateFormat();
-
- const debouncedStartDateChange = useDebouncedCallback(
- (dateString: string) => {
- if (isValidDate(dateString)) {
- const newDate = new Date(dateString);
-
- if (newDate.getTime() !== startDate.getTime()) {
- onStartDateChange(newDate);
- }
- }
- },
- 750
- );
-
- const debouncedEndDateChange = useDebouncedCallback(
- (dateString: string) => {
- if (isValidDate(dateString)) {
- const newDate = new Date(dateString);
-
- if (newDate.getTime() !== endDate.getTime()) {
- onEndDateChange(newDate);
- }
- }
- },
- 750
- );
-
- const handleStartDateChange = () => {
- if (startDateRef.current) {
- debouncedStartDateChange(startDateRef.current.value);
- }
- };
-
- const handleEndDateChange = () => {
- if (endDateRef.current) {
- debouncedEndDateChange(endDateRef.current.value);
- }
- };
-
- return (
-
- );
-};
+import { useRef } from "react";
+import { useDebouncedCallback } from "use-debounce";
+
+import { useLocaleDateFormat } from "../../hooks/useLocalDateFormat";
+import { formatDateToISO, isValidDate } from "../../utils/formatters";
+
+interface DateRangePickerProps {
+ startDate: Date;
+ endDate: Date;
+ onStartDateChange: (date: Date) => void;
+ onEndDateChange: (date: Date) => void;
+}
+
+export const DateRangePicker = ({
+ startDate,
+ endDate,
+ onStartDateChange,
+ onEndDateChange,
+}: DateRangePickerProps) => {
+ const startDateRef = useRef(null);
+ const endDateRef = useRef(null);
+ const localeDateFormat = useLocaleDateFormat();
+
+ const debouncedStartDateChange = useDebouncedCallback(
+ (dateString: string) => {
+ if (isValidDate(dateString)) {
+ const newDate = new Date(dateString);
+
+ if (newDate.getTime() !== startDate.getTime()) {
+ onStartDateChange(newDate);
+ }
+ }
+ },
+ 750
+ );
+
+ const debouncedEndDateChange = useDebouncedCallback(
+ (dateString: string) => {
+ if (isValidDate(dateString)) {
+ const newDate = new Date(dateString);
+
+ if (newDate.getTime() !== endDate.getTime()) {
+ onEndDateChange(newDate);
+ }
+ }
+ },
+ 750
+ );
+
+ const handleStartDateChange = () => {
+ if (startDateRef.current) {
+ debouncedStartDateChange(startDateRef.current.value);
+ }
+ };
+
+ const handleEndDateChange = () => {
+ if (endDateRef.current) {
+ debouncedEndDateChange(endDateRef.current.value);
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/utils/LoadingPlaceholder.tsx b/src/components/utils/LoadingPlaceholder.tsx
index 7d89551..8a1d2c7 100644
--- a/src/components/utils/LoadingPlaceholder.tsx
+++ b/src/components/utils/LoadingPlaceholder.tsx
@@ -1,11 +1,11 @@
-import { Loader2 } from "lucide-react";
-
-interface LoadingPlaceholderProps {
- className?: string;
-}
-
-export const LoadingPlaceholder = ({ className = "" }: LoadingPlaceholderProps) => (
-
-
-
-);
+import { Loader2 } from "lucide-react";
+
+interface LoadingPlaceholderProps {
+ className?: string;
+}
+
+export const LoadingPlaceholder = ({ className = "" }: LoadingPlaceholderProps) => (
+
+
+
+);
diff --git a/src/components/utils/ToolTip.tsx b/src/components/utils/ToolTip.tsx
index 0153b13..07b17d5 100644
--- a/src/components/utils/ToolTip.tsx
+++ b/src/components/utils/ToolTip.tsx
@@ -1,29 +1,29 @@
-import { HelpCircle } from "lucide-react";
-import { ReactNode, useState } from "react";
-
-interface TooltipProps {
- content: string | ReactNode;
- children: ReactNode;
-}
-
-export const Tooltip = ({ content, children }: TooltipProps) => {
- const [show, setShow] = useState(false);
-
- return (
-
-
setShow(true)}
- onMouseLeave={() => setShow(false)}
- >
- {children}
-
-
- {show && (
-
- {content}
-
- )}
-
- );
-};
+import { HelpCircle } from "lucide-react";
+import { ReactNode, useState } from "react";
+
+interface TooltipProps {
+ content: string | ReactNode;
+ children: ReactNode;
+}
+
+export const Tooltip = ({ content, children }: TooltipProps) => {
+ const [show, setShow] = useState(false);
+
+ return (
+
+
setShow(true)}
+ onMouseLeave={() => setShow(false)}
+ >
+ {children}
+
+
+ {show && (
+
+ {content}
+
+ )}
+
+ );
+};
diff --git a/src/hooks/useDarkMode.tsx b/src/hooks/useDarkMode.tsx
index 98fed17..631f7d4 100644
--- a/src/hooks/useDarkMode.tsx
+++ b/src/hooks/useDarkMode.tsx
@@ -1,11 +1,11 @@
-import { useContext } from "react";
-
-import { DarkModeContext } from "../providers/DarkModeProvider";
-
-export const useDarkMode = () => {
- const context = useContext(DarkModeContext);
- if (!context) {
- throw new Error('useDarkMode must be used within a DarkModeProvider');
- }
- return context;
-};
+import { useContext } from "react";
+
+import { DarkModeContext } from "../providers/DarkModeProvider";
+
+export const useDarkMode = () => {
+ const context = useContext(DarkModeContext);
+ if (!context) {
+ throw new Error('useDarkMode must be used within a DarkModeProvider');
+ }
+ return context;
+};
diff --git a/src/hooks/useLocalDateFormat.tsx b/src/hooks/useLocalDateFormat.tsx
index b5951a4..12ebc3d 100644
--- a/src/hooks/useLocalDateFormat.tsx
+++ b/src/hooks/useLocalDateFormat.tsx
@@ -1,20 +1,20 @@
-import { useMemo } from "react";
-
-export const useLocaleDateFormat = () => {
- return useMemo(() => {
- const formatter = new Intl.DateTimeFormat(undefined, {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- });
-
- const testDate = new Date(2024, 0, 1);
- const formattedParts = formatter.formatToParts(testDate);
-
- const order = formattedParts
- .filter(part => part.type !== 'literal') // Entferne Trennzeichen
- .map(part => part.type);
-
- return order.join('/').toUpperCase().replace(/DAY/g, 'DD').replace(/MONTH/g, 'MM').replace(/YEAR/g, 'YYYY');
- }, []);
-};
+import { useMemo } from "react";
+
+export const useLocaleDateFormat = () => {
+ return useMemo(() => {
+ const formatter = new Intl.DateTimeFormat(undefined, {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ });
+
+ const testDate = new Date(2024, 0, 1);
+ const formattedParts = formatter.formatToParts(testDate);
+
+ const order = formattedParts
+ .filter(part => part.type !== 'literal') // Entferne Trennzeichen
+ .map(part => part.type);
+
+ return order.join('/').toUpperCase().replace(/DAY/g, 'DD').replace(/MONTH/g, 'MM').replace(/YEAR/g, 'YYYY');
+ }, []);
+};
diff --git a/src/hooks/usePortfolio.tsx b/src/hooks/usePortfolio.tsx
index 21e1456..085fa21 100644
--- a/src/hooks/usePortfolio.tsx
+++ b/src/hooks/usePortfolio.tsx
@@ -1,18 +1,18 @@
-import { useContext, useMemo } from "react";
-
-import { PortfolioContext, PortfolioContextType } from "../providers/PortfolioProvider";
-
-// main way of how to access the context
-const usePortfolio = () => {
- const context = useContext(PortfolioContext);
- if (!context) {
- throw new Error('usePortfolio must be used within a PortfolioProvider');
- }
- return context;
-};
-
-// performance optimized way of accessing the context
-export const usePortfolioSelector = (selector: (state: PortfolioContextType) => T): T => {
- const context = usePortfolio();
- return useMemo(() => selector(context), [selector, context]);
-};
+import { useContext, useMemo } from "react";
+
+import { PortfolioContext, PortfolioContextType } from "../providers/PortfolioProvider";
+
+// main way of how to access the context
+const usePortfolio = () => {
+ const context = useContext(PortfolioContext);
+ if (!context) {
+ throw new Error('usePortfolio must be used within a PortfolioProvider');
+ }
+ return context;
+};
+
+// performance optimized way of accessing the context
+export const usePortfolioSelector = (selector: (state: PortfolioContextType) => T): T => {
+ const context = usePortfolio();
+ return useMemo(() => selector(context), [selector, context]);
+};
diff --git a/src/index.css b/src/index.css
index bce3f0b..18466f1 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,125 +1,125 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-/* Modern Scrollbar Styling */
-/* Webkit (Chrome, Safari, Edge) */
-::-webkit-scrollbar {
- width: 8px;
- height: 8px;
-}
-
-::-webkit-scrollbar-track {
- background: transparent;
-}
-
-::-webkit-scrollbar-thumb {
- background-color: #94a3b8;
- border-radius: 9999px;
- border: 2px solid transparent;
- background-clip: content-box;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background-color: #64748b;
-}
-
-/* Ensure transparent background for the scrollbar area */
-::-webkit-scrollbar-corner,
-::-webkit-scrollbar-track-piece {
- background: transparent !important;
-}
-
-/* Dark mode */
-.dark ::-webkit-scrollbar {
- background: black !important;
-}
-
-.dark ::-webkit-scrollbar-track {
- background: black !important;
-}
-
-.dark ::-webkit-scrollbar-thumb {
- background-color: #475569 !important;
-}
-
-.dark ::-webkit-scrollbar-thumb:hover {
- background-color: #64748b !important;
-}
-
-/* Firefox */
-* {
- scrollbar-width: thin;
- scrollbar-color: #94a3b8 #1d2127;
-}
-
-.dark * {
- scrollbar-color: #475569 #1d212799 !important;
-}
-
-/* For Internet Explorer */
-body {
- -ms-overflow-style: auto;
-}
-
-/* Remove default white background in dark mode */
-.dark ::-webkit-scrollbar,
-.dark ::-webkit-scrollbar-track,
-.dark ::-webkit-scrollbar-corner,
-.dark ::-webkit-scrollbar-track-piece {
- background-color: transparent !important;
-}
-
-/* Ensure the app background extends properly */
-html,
-body {
- background: inherit;
-}
-
-/* Scrollbar Styling fΓΌr Investment Form */
-.scrollbar-styled {
- scrollbar-gutter: stable both-edges;
- overflow-y: scroll !important;
-}
-
-.scrollbar-styled::-webkit-scrollbar {
- width: 8px;
- height: 8px;
- display: block;
-}
-
-.scrollbar-styled::-webkit-scrollbar-track {
- background: transparent;
- margin: 4px;
-}
-
-.scrollbar-styled::-webkit-scrollbar-thumb {
- background-color: #94a3b8;
- border-radius: 9999px;
- border: 2px solid transparent;
- background-clip: content-box;
- min-height: 40px;
-}
-
-.scrollbar-styled::-webkit-scrollbar-thumb:hover {
- background-color: #64748b;
-}
-
-/* Dark mode */
-.dark .scrollbar-styled::-webkit-scrollbar-thumb {
- background-color: #475569;
-}
-
-.dark .scrollbar-styled::-webkit-scrollbar-thumb:hover {
- background-color: #64748b;
-}
-
-/* Firefox */
-.scrollbar-styled {
- scrollbar-width: thin;
- scrollbar-color: #94a3b8 transparent;
-}
-
-.dark .scrollbar-styled {
- scrollbar-color: #475569 transparent;
-}
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Modern Scrollbar Styling */
+/* Webkit (Chrome, Safari, Edge) */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: #94a3b8;
+ border-radius: 9999px;
+ border: 2px solid transparent;
+ background-clip: content-box;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background-color: #64748b;
+}
+
+/* Ensure transparent background for the scrollbar area */
+::-webkit-scrollbar-corner,
+::-webkit-scrollbar-track-piece {
+ background: transparent !important;
+}
+
+/* Dark mode */
+.dark ::-webkit-scrollbar {
+ background: black !important;
+}
+
+.dark ::-webkit-scrollbar-track {
+ background: black !important;
+}
+
+.dark ::-webkit-scrollbar-thumb {
+ background-color: #475569 !important;
+}
+
+.dark ::-webkit-scrollbar-thumb:hover {
+ background-color: #64748b !important;
+}
+
+/* Firefox */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: #94a3b8 #1d2127;
+}
+
+.dark * {
+ scrollbar-color: #475569 #1d212799 !important;
+}
+
+/* For Internet Explorer */
+body {
+ -ms-overflow-style: auto;
+}
+
+/* Remove default white background in dark mode */
+.dark ::-webkit-scrollbar,
+.dark ::-webkit-scrollbar-track,
+.dark ::-webkit-scrollbar-corner,
+.dark ::-webkit-scrollbar-track-piece {
+ background-color: transparent !important;
+}
+
+/* Ensure the app background extends properly */
+html,
+body {
+ background: inherit;
+}
+
+/* Scrollbar Styling fΓΌr Investment Form */
+.scrollbar-styled {
+ scrollbar-gutter: stable both-edges;
+ overflow-y: scroll !important;
+}
+
+.scrollbar-styled::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ display: block;
+}
+
+.scrollbar-styled::-webkit-scrollbar-track {
+ background: transparent;
+ margin: 4px;
+}
+
+.scrollbar-styled::-webkit-scrollbar-thumb {
+ background-color: #94a3b8;
+ border-radius: 9999px;
+ border: 2px solid transparent;
+ background-clip: content-box;
+ min-height: 40px;
+}
+
+.scrollbar-styled::-webkit-scrollbar-thumb:hover {
+ background-color: #64748b;
+}
+
+/* Dark mode */
+.dark .scrollbar-styled::-webkit-scrollbar-thumb {
+ background-color: #475569;
+}
+
+.dark .scrollbar-styled::-webkit-scrollbar-thumb:hover {
+ background-color: #64748b;
+}
+
+/* Firefox */
+.scrollbar-styled {
+ scrollbar-width: thin;
+ scrollbar-color: #94a3b8 transparent;
+}
+
+.dark .scrollbar-styled {
+ scrollbar-color: #475569 transparent;
+}
diff --git a/src/main.tsx b/src/main.tsx
index 01ce20f..e532992 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,15 +1,19 @@
-import "./index.css";
-
-import { StrictMode } from "react";
-import { createRoot } from "react-dom/client";
-
-import App from "./App.tsx";
-import { DarkModeProvider } from "./providers/DarkModeProvider.tsx";
-
-createRoot(document.getElementById('root')!).render(
-
-
-
-
-
-);
+import "./index.css";
+
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
+
+import App from "./App.tsx";
+import { DarkModeProvider } from "./providers/DarkModeProvider.tsx";
+
+// Let App handle the route definitions
+const router = createBrowserRouter(App);
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+);
diff --git a/src/pages/StockExplorer.tsx b/src/pages/StockExplorer.tsx
new file mode 100644
index 0000000..601c0ae
--- /dev/null
+++ b/src/pages/StockExplorer.tsx
@@ -0,0 +1,680 @@
+import { format, subYears } from "date-fns";
+import { ChevronDown, ChevronLeft, Filter, Plus, RefreshCw, Search, X } from "lucide-react";
+import { useCallback, useEffect, useState } from "react";
+import toast from "react-hot-toast";
+import { Link } from "react-router-dom";
+import {
+ CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis
+} from "recharts";
+
+import { useDarkMode } from "../hooks/useDarkMode";
+import { EQUITY_TYPES, getHistoricalData, searchAssets } from "../services/yahooFinanceService";
+import { Asset } from "../types";
+import { getHexColor } from "../utils/formatters";
+
+// Time period options
+const TIME_PERIODS = {
+ YTD: "Year to Date",
+ "1Y": "1 Year",
+ "3Y": "3 Years",
+ "5Y": "5 Years",
+ "10Y": "10 Years",
+ "15Y": "15 Years",
+ "20Y": "20 Years",
+ MAX: "Maximum",
+ CUSTOM: "Custom Range"
+};
+
+// Equity type options
+const EQUITY_TYPESMAP: Record = {
+ all: "All Types",
+ ETF: "ETFs",
+ Stock: "Stocks",
+ "Etf or Stock": "ETF or Stock",
+ Mutualfund: "Mutual Funds",
+ Index: "Indices",
+ Currency: "Currencies",
+ Cryptocurrency: "Cryptocurrencies",
+ Future: "Futures",
+};
+
+const StockExplorer = () => {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [searchResults, setSearchResults] = useState([]);
+ const [selectedStocks, setSelectedStocks] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [searchLoading, setSearchLoading] = useState(false);
+ const [timePeriod, setTimePeriod] = useState("1Y");
+ const [equityType, setEquityType] = useState("all");
+ const [showEquityTypeDropdown, setShowEquityTypeDropdown] = useState(false);
+ const [dateRange, setDateRange] = useState({
+ startDate: subYears(new Date(), 1),
+ endDate: new Date()
+ });
+ const [customDateRange, setCustomDateRange] = useState({
+ startDate: subYears(new Date(), 1),
+ endDate: new Date()
+ });
+ const [stockData, setStockData] = useState([]);
+ const [stockColors, setStockColors] = useState>({});
+ const { isDarkMode } = useDarkMode();
+
+ // Handle search
+ const handleSearch = useCallback(async () => {
+ if (!searchQuery || searchQuery.length < 2) {
+ // Clear results if query is too short
+ setSearchResults([]);
+ return;
+ }
+
+ setSearchLoading(true);
+ try {
+ // Convert the equity type to a comma-separated string for the API
+ const typeParam = EQUITY_TYPES[equityType];
+
+ console.log(`Searching for "${searchQuery}" with type "${typeParam}"`);
+
+ const results = await searchAssets(searchQuery, typeParam);
+
+ console.log("Search results:", results);
+
+ // Filter out stocks already in the selected list
+ const filteredResults = results.filter(
+ result => !selectedStocks.some(stock => stock.symbol === result.symbol)
+ );
+
+ setSearchResults(filteredResults);
+
+ if (filteredResults.length === 0 && results.length > 0) {
+ toast.custom((t: any) => (
+
+ All matching results are already in your comparison
+
+ ));
+ } else if (filteredResults.length === 0) {
+ toast.error(`No ${equityType === 'all' ? '' : EQUITY_TYPESMAP[equityType]} results found for "${searchQuery}"`);
+ }
+ } catch (error) {
+ console.error("Search error:", error);
+ toast.error("Failed to search for stocks");
+ } finally {
+ setSearchLoading(false);
+ }
+ }, [searchQuery, equityType, selectedStocks]);
+
+ // Handle enter key press in search input
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ handleSearch();
+ }
+ };
+
+ // Add stock to comparison
+ const addStock = useCallback(async (stock: Asset) => {
+ // Check if the stock is already selected
+ if (selectedStocks.some(s => s.symbol === stock.symbol)) {
+ toast.error(`${stock.name} is already in your comparison`);
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const { historicalData, longName } = await getHistoricalData(
+ stock.symbol,
+ dateRange.startDate,
+ dateRange.endDate
+ );
+
+ if (historicalData.size === 0) {
+ toast.error(`No historical data available for ${stock.name}`);
+ return;
+ }
+
+ const stockWithHistory = {
+ ...stock,
+ name: longName || stock.name,
+ historicalData,
+ investments: [] // Empty as we're just exploring
+ };
+
+ // Update selected stocks without causing an extra refresh
+ setSelectedStocks(prev => [...prev, stockWithHistory]);
+
+ // Assign a color
+ setStockColors(prev => {
+ const usedColors = new Set(Object.values(prev));
+ const color = getHexColor(usedColors, isDarkMode);
+ return { ...prev, [stockWithHistory.id]: color };
+ });
+
+ // Don't clear results, so users can add multiple stocks
+ // Just clear the specific stock that was added
+ setSearchResults(prev => prev.filter(result => result.symbol !== stock.symbol));
+
+ toast.success(`Added ${stockWithHistory.name} to comparison`);
+ } catch (error) {
+ console.error("Error adding stock:", error);
+ toast.error(`Failed to add ${stock.name}`);
+ } finally {
+ setLoading(false);
+ }
+ }, [dateRange, isDarkMode, selectedStocks]);
+
+ // Remove stock from comparison
+ const removeStock = useCallback((stockId: string) => {
+ setSelectedStocks(prev => prev.filter(stock => stock.id !== stockId));
+ }, []);
+
+ // Update time period and date range
+ const updateTimePeriod = useCallback((period: keyof typeof TIME_PERIODS) => {
+ setTimePeriod(period);
+
+ const endDate = new Date();
+ let startDate;
+
+ switch (period) {
+ case "YTD":
+ startDate = new Date(endDate.getFullYear(), 0, 1); // Jan 1 of current year
+ break;
+ case "1Y":
+ startDate = subYears(endDate, 1);
+ break;
+ case "3Y":
+ startDate = subYears(endDate, 3);
+ break;
+ case "5Y":
+ startDate = subYears(endDate, 5);
+ break;
+ case "10Y":
+ startDate = subYears(endDate, 10);
+ break;
+ case "15Y":
+ startDate = subYears(endDate, 15);
+ break;
+ case "20Y":
+ startDate = subYears(endDate, 20);
+ break;
+ case "MAX":
+ startDate = new Date(1970, 0, 1); // Very early date for "max"
+ break;
+ case "CUSTOM":
+ // Keep the existing custom range
+ startDate = customDateRange.startDate;
+ break;
+ default:
+ startDate = subYears(endDate, 1);
+ }
+
+ if (period !== "CUSTOM") {
+ setDateRange({ startDate, endDate });
+ } else {
+ setDateRange(customDateRange);
+ }
+ }, [customDateRange]);
+
+ // Process the stock data for display
+ const processStockData = useCallback((stocks: Asset[]) => {
+ // Create a combined dataset with data points for all dates
+ const allDates = new Set();
+ const stockValues: Record> = {};
+
+ // First gather all dates and initial values
+ stocks.forEach(stock => {
+ stockValues[stock.id] = {};
+
+ stock.historicalData.forEach((value, dateStr) => {
+ allDates.add(dateStr);
+ stockValues[stock.id][dateStr] = value;
+ });
+ });
+
+ // Convert to array of data points
+ const sortedDates = Array.from(allDates).sort();
+ return sortedDates.map(dateStr => {
+ const dataPoint: Record = { date: dateStr };
+
+ // Add base value for each stock
+ stocks.forEach(stock => {
+ if (stockValues[stock.id][dateStr] !== undefined) {
+ dataPoint[stock.id] = stockValues[stock.id][dateStr];
+ }
+ });
+
+ // Calculate percentage change for each stock
+ stocks.forEach(stock => {
+ // Find first available value for this stock
+ const firstValue = Object.values(stockValues[stock.id])[0];
+ const currentValue = stockValues[stock.id][dateStr];
+
+ if (firstValue && currentValue) {
+ dataPoint[`${stock.id}_percent`] =
+ ((currentValue - firstValue) / firstValue) * 100;
+ }
+ });
+
+ return dataPoint;
+ });
+ }, []);
+
+ // Refresh stock data when stocks or date range changes
+ const refreshStockData = useCallback(async () => {
+ if (selectedStocks.length === 0) return;
+
+ setLoading(true);
+ try {
+ // Fetch updated data for each stock
+ const updatedStocks = await Promise.all(
+ selectedStocks.map(async stock => {
+ const { historicalData, longName } = await getHistoricalData(
+ stock.symbol,
+ dateRange.startDate,
+ dateRange.endDate
+ );
+
+ return {
+ ...stock,
+ name: longName || stock.name,
+ historicalData
+ };
+ })
+ );
+
+ // Update chart data
+ setStockData(processStockData(updatedStocks));
+
+ // Unconditionally update selectedStocks so the table refreshes
+ setSelectedStocks(updatedStocks);
+
+ toast.success("Stock data refreshed");
+ } catch (error) {
+ console.error("Error refreshing data:", error);
+ toast.error("Failed to refresh stock data");
+ } finally {
+ setLoading(false);
+ }
+ }, [dateRange, processStockData]);
+
+ // Calculate performance metrics for each stock with best/worst year
+ const calculatePerformanceMetrics = useCallback((stock: Asset) => {
+ const historicalData = Array.from(stock.historicalData.entries());
+ if (historicalData.length < 2) return {
+ ytd: "N/A",
+ total: "N/A",
+ annualized: "N/A",
+ };
+
+ // Sort by date
+ historicalData.sort((a, b) =>
+ new Date(a[0]).getTime() - new Date(b[0]).getTime()
+ );
+
+ const firstValue = historicalData[0][1];
+ const lastValue = historicalData[historicalData.length - 1][1];
+
+ // Calculate total return
+ const totalPercentChange = ((lastValue - firstValue) / firstValue) * 100;
+
+ // Calculate annualized return using a more precise year duration (365.25 days) and standard CAGR
+ const firstDate = new Date(historicalData[0][0]);
+ const lastDate = new Date(historicalData[historicalData.length - 1][0]);
+ const yearsDiff = (lastDate.getTime() - firstDate.getTime()) / (1000 * 60 * 60 * 24 * 365.25);
+ const annualizedReturn = (Math.pow(lastValue / firstValue, 1 / yearsDiff) - 1) * 100;
+
+ return {
+ total: `${totalPercentChange.toFixed(2)}%`,
+ annualized: `${annualizedReturn.toFixed(2)}%/year`,
+ };
+ }, []);
+
+ // Effect to refresh data when time period or stocks change
+ useEffect(() => {
+ // Only refresh when stocks are added/removed or dateRange changes
+ refreshStockData();
+ // Don't include refreshStockData in dependencies
+ }, [selectedStocks.length, dateRange]);
+
+ // Update custom date range
+ const handleCustomDateChange = useCallback((start: Date, end: Date) => {
+ const newRange = { startDate: start, endDate: end };
+ setCustomDateRange(newRange);
+ if (timePeriod === "CUSTOM") {
+ setDateRange(newRange);
+ }
+ }, [timePeriod]);
+
+ // Add debugging for chart display
+ useEffect(() => {
+ if (selectedStocks.length > 0) {
+ console.log("Selected stocks:", selectedStocks);
+ console.log("Stock data for chart:", stockData);
+ }
+ }, [selectedStocks, stockData]);
+
+ // Ensure processStockData is called immediately when selectedStocks changes
+ useEffect(() => {
+ if (selectedStocks.length > 0) {
+ const processedData = processStockData(selectedStocks);
+ setStockData(processedData);
+ }
+ }, [selectedStocks, processStockData]);
+
+ return (
+
+
+
+
+
+ Back to Home
+
+
Stock Explorer
+
+
+ {/* Search and add stocks */}
+
+
Add Assets to Compare
+
+
+
+
setSearchQuery(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Search for stocks, ETFs, indices..."
+ className="w-full p-2 border rounded disabled:opacity-50 disabled:cursor-not-allowed dark:bg-slate-700 dark:text-white dark:border-slate-600"
+ />
+ {searchLoading && (
+
+ )}
+
+
+ {/* Equity Type Dropdown */}
+
+
setShowEquityTypeDropdown(!showEquityTypeDropdown)}
+ className="flex items-center gap-2 border p-2 rounded dark:bg-slate-700 dark:text-white dark:border-slate-600 min-w-[140px]"
+ >
+
+ {EQUITY_TYPESMAP[equityType]}
+
+
+
+ {showEquityTypeDropdown && (
+
+ {Object.entries(EQUITY_TYPESMAP).map(([key, label]) => (
+ {
+ setEquityType(key as keyof typeof EQUITY_TYPESMAP);
+ setShowEquityTypeDropdown(false);
+ }}
+ className={`block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-slate-600 dark:text-white ${equityType === key ? 'bg-blue-50 dark:bg-blue-900/30' : ''
+ }`}
+ >
+ {label}
+
+ ))}
+
+ )}
+
+
+
+
+ Search
+
+
+
+ {/* Search results */}
+ {searchResults.length > 0 && (
+
+
+
+ {searchResults.length} result{searchResults.length !== 1 ? 's' : ''} found for "{searchQuery}"
+
+
+ {searchResults.map(result => (
+
+
+
{result.name}
+
+ {result.symbol} | {result.quoteType?.toUpperCase() || "Unknown"}
+ {result.isin && ` | ${result.isin}`}
+ {result.price && ` | ${result.price}`}
+ {result.priceChangePercent && ` | ${result.priceChangePercent}`}
+
+
+
+
addStock(result)}
+ className="bg-green-500 text-white p-1 rounded hover:bg-green-600"
+ title="Add to comparison"
+ >
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Selected stocks */}
+
+
Selected Stocks
+ {selectedStocks.length === 0 ? (
+
No stocks selected for comparison
+ ) : (
+
+ {selectedStocks.map(stock => {
+ const metrics = calculatePerformanceMetrics(stock);
+ return (
+
+
+
{stock.name}
+
+ ({metrics.total})
+
+
removeStock(stock.id)}
+ className="text-red-500 hover:text-red-700"
+ title="Remove"
+ >
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+ {/* Time period selector */}
+
+
+
Time Period
+
+ {loading ? (
+
+ ) : (
+
+ )}
+ Refresh{loading && "ing"} Data
+
+
+
+
+ {Object.entries(TIME_PERIODS).map(([key, label]) => (
+ updateTimePeriod(key as keyof typeof TIME_PERIODS)}
+ className={`px-3 py-1 rounded ${timePeriod === key
+ ? 'bg-blue-500 text-white'
+ : 'bg-gray-100 dark:bg-slate-700 text-gray-800 dark:text-gray-200'
+ }`}
+ >
+ {label}
+
+ ))}
+
+
+ {/* Custom date range selector (only visible when CUSTOM is selected) */}
+ {timePeriod === "CUSTOM" && (
+
+ )}
+
+
+ Showing data from {format(dateRange.startDate, 'MMM d, yyyy')} to {format(dateRange.endDate, 'MMM d, yyyy')}
+
+
+
+ {/* Chart */}
+ {selectedStocks.length > 0 && stockData.length > 0 && (
+
+
+
Performance Comparison
+
+
+
+
+
+
+ format(new Date(date), 'dd.MM.yyyy')}
+ tick={{ fill: isDarkMode ? '#D8D8D8' : '#4E4E4E' }}
+ />
+ `${value.toFixed(2)}%`}
+ />
+ {
+ const stockId = name.replace('_percent', '');
+ const price = props.payload[stockId] || 0;
+ const stockName = selectedStocks.find(s => s.id === stockId)?.name || name;
+ return [
+ `${value.toFixed(2)}% (β¬${price.toFixed(2)})`,
+ stockName
+ ];
+ }}
+ labelFormatter={(date) => format(new Date(date), 'dd.MM.yyyy')}
+ />
+
+
+ {/* Only percentage lines */}
+ {selectedStocks.map(stock => (
+
+ ))}
+
+
+
+
+ {/* Performance metrics table */}
+
+
+
+
+ Stock
+ Total Return
+ Annualized Return
+ Current Price
+
+
+
+ {selectedStocks.map(stock => {
+ const metrics = calculatePerformanceMetrics(stock);
+ const historicalData = Array.from(stock.historicalData.entries());
+ const currentPrice = historicalData.length > 0
+ ? historicalData[historicalData.length - 1][1]
+ : 0;
+
+ return (
+
+
+
+
+ {metrics.total}
+ {metrics.annualized}
+ β¬{currentPrice.toFixed(2)}
+
+ );
+ })}
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default StockExplorer;
diff --git a/src/providers/DarkModeProvider.tsx b/src/providers/DarkModeProvider.tsx
index b1ee206..d6fd1e2 100644
--- a/src/providers/DarkModeProvider.tsx
+++ b/src/providers/DarkModeProvider.tsx
@@ -1,38 +1,38 @@
-import { createContext, useEffect, useState } from "react";
-
-interface DarkModeContextType {
- isDarkMode: boolean;
- toggleDarkMode: () => void;
-}
-
-export const DarkModeContext = createContext(undefined);
-
-export const DarkModeProvider = ({ children }: { children: React.ReactNode }) => {
- const [isDarkMode, setIsDarkMode] = useState(() => {
- if (typeof window !== 'undefined') {
- const saved = localStorage.getItem('darkMode');
- if (saved !== null) {
- return saved === 'true';
- }
- return window.matchMedia('(prefers-color-scheme: dark)')?.matches ?? true;
- }
- return true;
- });
-
- useEffect(() => {
- localStorage.setItem('darkMode', isDarkMode.toString());
- if (isDarkMode) {
- document.documentElement.classList.add('dark');
- } else {
- document.documentElement.classList.remove('dark');
- }
- }, [isDarkMode]);
-
- const toggleDarkMode = () => setIsDarkMode(prev => !prev);
-
- return (
-
- {children}
-
- );
-};
+import { createContext, useEffect, useState } from "react";
+
+interface DarkModeContextType {
+ isDarkMode: boolean;
+ toggleDarkMode: () => void;
+}
+
+export const DarkModeContext = createContext(undefined);
+
+export const DarkModeProvider = ({ children }: { children: React.ReactNode }) => {
+ const [isDarkMode, setIsDarkMode] = useState(() => {
+ if (typeof window !== 'undefined') {
+ const saved = localStorage.getItem('darkMode');
+ if (saved !== null) {
+ return saved === 'true';
+ }
+ return window.matchMedia('(prefers-color-scheme: dark)')?.matches ?? true;
+ }
+ return true;
+ });
+
+ useEffect(() => {
+ localStorage.setItem('darkMode', isDarkMode.toString());
+ if (isDarkMode) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ }, [isDarkMode]);
+
+ const toggleDarkMode = () => setIsDarkMode(prev => !prev);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/providers/PortfolioProvider.tsx b/src/providers/PortfolioProvider.tsx
index 12876a1..26668fe 100644
--- a/src/providers/PortfolioProvider.tsx
+++ b/src/providers/PortfolioProvider.tsx
@@ -1,172 +1,172 @@
-import { startOfYear } from "date-fns";
-import { createContext, useMemo, useReducer } from "react";
-
-import { Asset, DateRange, Investment } from "../types";
-
-// State Types
-interface PortfolioState {
- assets: Asset[];
- isLoading: boolean;
- dateRange: DateRange;
-}
-
-// Action Types
-type PortfolioAction =
- | { type: 'SET_LOADING'; payload: boolean }
- | { type: 'ADD_ASSET'; payload: Asset }
- | { type: 'REMOVE_ASSET'; payload: string }
- | { type: 'CLEAR_ASSETS' }
- | { type: 'ADD_INVESTMENT'; payload: { assetId: string; investment: Investment | Investment[] } }
- | { type: 'REMOVE_INVESTMENT'; payload: { assetId: string; investmentId: string } }
- | { type: 'UPDATE_DATE_RANGE'; payload: DateRange }
- | { type: 'UPDATE_ASSET_HISTORICAL_DATA'; payload: { assetId: string; historicalData: Asset['historicalData']; longName?: string } }
- | { type: 'UPDATE_INVESTMENT'; payload: { assetId: string; investmentId: string; investment: Investment } }
- | { type: 'CLEAR_INVESTMENTS' }
- | { type: 'SET_ASSETS'; payload: Asset[] };
-
-// Initial State
-const initialState: PortfolioState = {
- assets: [],
- isLoading: false,
- dateRange: {
- startDate: startOfYear(new Date()),
- endDate: new Date(),
- },
-};
-
-// Reducer
-const portfolioReducer = (state: PortfolioState, action: PortfolioAction): PortfolioState => {
- switch (action.type) {
- case 'SET_LOADING':
- return { ...state, isLoading: action.payload };
-
- case 'ADD_ASSET':
- return { ...state, assets: [...state.assets, action.payload] };
-
- case 'REMOVE_ASSET':
- return {
- ...state,
- assets: state.assets.filter(asset => asset.id !== action.payload)
- };
-
- case 'CLEAR_ASSETS':
- return { ...state, assets: [] };
-
- case 'ADD_INVESTMENT':
- return {
- ...state,
- assets: state.assets.map(asset =>
- asset.id === action.payload.assetId
- ? { ...asset, investments: [...asset.investments, ...(Array.isArray(action.payload.investment) ? action.payload.investment : [action.payload.investment])] }
- : asset
- )
- };
-
- case 'REMOVE_INVESTMENT':
- return {
- ...state,
- assets: state.assets.map(asset =>
- asset.id === action.payload.assetId
- ? {
- ...asset,
- investments: asset.investments.filter(inv => inv.id !== action.payload.investmentId)
- }
- : asset
- )
- };
-
- case 'UPDATE_DATE_RANGE':
- return { ...state, dateRange: action.payload };
-
- case 'UPDATE_ASSET_HISTORICAL_DATA':
- return {
- ...state,
- assets: state.assets.map(asset =>
- asset.id === action.payload.assetId
- ? {
- ...asset,
- historicalData: action.payload.historicalData,
- name: action.payload.longName || asset.name
- }
- : asset
- )
- };
-
- case 'UPDATE_INVESTMENT':
- return {
- ...state,
- assets: state.assets.map(asset =>
- asset.id === action.payload.assetId
- ? {
- ...asset,
- investments: asset.investments.map(inv =>
- inv.id === action.payload.investmentId ? action.payload.investment : inv
- )
- }
- : asset
- )
- };
-
- case 'CLEAR_INVESTMENTS':
- return {
- ...state,
- assets: state.assets.map(asset => ({ ...asset, investments: [] }))
- };
-
- case 'SET_ASSETS':
- return { ...state, assets: action.payload };
-
- default:
- return state;
- }
-};
-
-// Context
-export interface PortfolioContextType extends PortfolioState {
- setLoading: (loading: boolean) => void;
- addAsset: (asset: Asset) => void;
- removeAsset: (assetId: string) => void;
- clearAssets: () => void;
- addInvestment: (assetId: string, investment: Investment | Investment[]) => void;
- removeInvestment: (assetId: string, investmentId: string) => void;
- updateDateRange: (dateRange: DateRange) => void;
- updateAssetHistoricalData: (assetId: string, historicalData: Asset['historicalData'], longName?: string) => void;
- updateInvestment: (assetId: string, investmentId: string, investment: Investment) => void;
- clearInvestments: () => void;
- setAssets: (assets: Asset[]) => void;
-}
-
-export const PortfolioContext = createContext(null);
-
-// Provider Component
-export const PortfolioProvider = ({ children }: { children: React.ReactNode }) => {
- const [state, dispatch] = useReducer(portfolioReducer, initialState);
-
- // Memoized actions
- const actions = useMemo(() => ({
- setLoading: (loading: boolean) => dispatch({ type: 'SET_LOADING', payload: loading }),
- addAsset: (asset: Asset) => dispatch({ type: 'ADD_ASSET', payload: asset }),
- removeAsset: (assetId: string) => dispatch({ type: 'REMOVE_ASSET', payload: assetId }),
- clearAssets: () => dispatch({ type: 'CLEAR_ASSETS' }),
- addInvestment: (assetId: string, investment: Investment | Investment[]) =>
- dispatch({ type: 'ADD_INVESTMENT', payload: { assetId, investment } }),
- removeInvestment: (assetId: string, investmentId: string) =>
- dispatch({ type: 'REMOVE_INVESTMENT', payload: { assetId, investmentId } }),
- updateDateRange: (dateRange: DateRange) =>
- dispatch({ type: 'UPDATE_DATE_RANGE', payload: dateRange }),
- updateAssetHistoricalData: (assetId: string, historicalData: Asset['historicalData'], longName?: string) =>
- dispatch({ type: 'UPDATE_ASSET_HISTORICAL_DATA', payload: { assetId, historicalData, longName } }),
- updateInvestment: (assetId: string, investmentId: string, investment: Investment) =>
- dispatch({ type: 'UPDATE_INVESTMENT', payload: { assetId, investmentId, investment } }),
- clearInvestments: () => dispatch({ type: 'CLEAR_INVESTMENTS' }),
- setAssets: (assets: Asset[]) => dispatch({ type: 'SET_ASSETS', payload: assets }),
- }), []);
-
- const value = useMemo(() => ({ ...state, ...actions }), [state, actions]);
-
- return (
-
- {children}
-
- );
-};
+import { startOfYear } from "date-fns";
+import { createContext, useMemo, useReducer } from "react";
+
+import { Asset, DateRange, Investment } from "../types";
+
+// State Types
+interface PortfolioState {
+ assets: Asset[];
+ isLoading: boolean;
+ dateRange: DateRange;
+}
+
+// Action Types
+type PortfolioAction =
+ | { type: 'SET_LOADING'; payload: boolean }
+ | { type: 'ADD_ASSET'; payload: Asset }
+ | { type: 'REMOVE_ASSET'; payload: string }
+ | { type: 'CLEAR_ASSETS' }
+ | { type: 'ADD_INVESTMENT'; payload: { assetId: string; investment: Investment | Investment[] } }
+ | { type: 'REMOVE_INVESTMENT'; payload: { assetId: string; investmentId: string } }
+ | { type: 'UPDATE_DATE_RANGE'; payload: DateRange }
+ | { type: 'UPDATE_ASSET_HISTORICAL_DATA'; payload: { assetId: string; historicalData: Asset['historicalData']; longName?: string } }
+ | { type: 'UPDATE_INVESTMENT'; payload: { assetId: string; investmentId: string; investment: Investment } }
+ | { type: 'CLEAR_INVESTMENTS' }
+ | { type: 'SET_ASSETS'; payload: Asset[] };
+
+// Initial State
+const initialState: PortfolioState = {
+ assets: [],
+ isLoading: false,
+ dateRange: {
+ startDate: startOfYear(new Date()),
+ endDate: new Date(),
+ },
+};
+
+// Reducer
+const portfolioReducer = (state: PortfolioState, action: PortfolioAction): PortfolioState => {
+ switch (action.type) {
+ case 'SET_LOADING':
+ return { ...state, isLoading: action.payload };
+
+ case 'ADD_ASSET':
+ return { ...state, assets: [...state.assets, action.payload] };
+
+ case 'REMOVE_ASSET':
+ return {
+ ...state,
+ assets: state.assets.filter(asset => asset.id !== action.payload)
+ };
+
+ case 'CLEAR_ASSETS':
+ return { ...state, assets: [] };
+
+ case 'ADD_INVESTMENT':
+ return {
+ ...state,
+ assets: state.assets.map(asset =>
+ asset.id === action.payload.assetId
+ ? { ...asset, investments: [...asset.investments, ...(Array.isArray(action.payload.investment) ? action.payload.investment : [action.payload.investment])] }
+ : asset
+ )
+ };
+
+ case 'REMOVE_INVESTMENT':
+ return {
+ ...state,
+ assets: state.assets.map(asset =>
+ asset.id === action.payload.assetId
+ ? {
+ ...asset,
+ investments: asset.investments.filter(inv => inv.id !== action.payload.investmentId)
+ }
+ : asset
+ )
+ };
+
+ case 'UPDATE_DATE_RANGE':
+ return { ...state, dateRange: action.payload };
+
+ case 'UPDATE_ASSET_HISTORICAL_DATA':
+ return {
+ ...state,
+ assets: state.assets.map(asset =>
+ asset.id === action.payload.assetId
+ ? {
+ ...asset,
+ historicalData: action.payload.historicalData,
+ name: action.payload.longName || asset.name
+ }
+ : asset
+ )
+ };
+
+ case 'UPDATE_INVESTMENT':
+ return {
+ ...state,
+ assets: state.assets.map(asset =>
+ asset.id === action.payload.assetId
+ ? {
+ ...asset,
+ investments: asset.investments.map(inv =>
+ inv.id === action.payload.investmentId ? action.payload.investment : inv
+ )
+ }
+ : asset
+ )
+ };
+
+ case 'CLEAR_INVESTMENTS':
+ return {
+ ...state,
+ assets: state.assets.map(asset => ({ ...asset, investments: [] }))
+ };
+
+ case 'SET_ASSETS':
+ return { ...state, assets: action.payload };
+
+ default:
+ return state;
+ }
+};
+
+// Context
+export interface PortfolioContextType extends PortfolioState {
+ setLoading: (loading: boolean) => void;
+ addAsset: (asset: Asset) => void;
+ removeAsset: (assetId: string) => void;
+ clearAssets: () => void;
+ addInvestment: (assetId: string, investment: Investment | Investment[]) => void;
+ removeInvestment: (assetId: string, investmentId: string) => void;
+ updateDateRange: (dateRange: DateRange) => void;
+ updateAssetHistoricalData: (assetId: string, historicalData: Asset['historicalData'], longName?: string) => void;
+ updateInvestment: (assetId: string, investmentId: string, investment: Investment) => void;
+ clearInvestments: () => void;
+ setAssets: (assets: Asset[]) => void;
+}
+
+export const PortfolioContext = createContext(null);
+
+// Provider Component
+export const PortfolioProvider = ({ children }: { children: React.ReactNode }) => {
+ const [state, dispatch] = useReducer(portfolioReducer, initialState);
+
+ // Memoized actions
+ const actions = useMemo(() => ({
+ setLoading: (loading: boolean) => dispatch({ type: 'SET_LOADING', payload: loading }),
+ addAsset: (asset: Asset) => dispatch({ type: 'ADD_ASSET', payload: asset }),
+ removeAsset: (assetId: string) => dispatch({ type: 'REMOVE_ASSET', payload: assetId }),
+ clearAssets: () => dispatch({ type: 'CLEAR_ASSETS' }),
+ addInvestment: (assetId: string, investment: Investment | Investment[]) =>
+ dispatch({ type: 'ADD_INVESTMENT', payload: { assetId, investment } }),
+ removeInvestment: (assetId: string, investmentId: string) =>
+ dispatch({ type: 'REMOVE_INVESTMENT', payload: { assetId, investmentId } }),
+ updateDateRange: (dateRange: DateRange) =>
+ dispatch({ type: 'UPDATE_DATE_RANGE', payload: dateRange }),
+ updateAssetHistoricalData: (assetId: string, historicalData: Asset['historicalData'], longName?: string) =>
+ dispatch({ type: 'UPDATE_ASSET_HISTORICAL_DATA', payload: { assetId, historicalData, longName } }),
+ updateInvestment: (assetId: string, investmentId: string, investment: Investment) =>
+ dispatch({ type: 'UPDATE_INVESTMENT', payload: { assetId, investmentId, investment } }),
+ clearInvestments: () => dispatch({ type: 'CLEAR_INVESTMENTS' }),
+ setAssets: (assets: Asset[]) => dispatch({ type: 'SET_ASSETS', payload: assets }),
+ }), []);
+
+ const value = useMemo(() => ({ ...state, ...actions }), [state, actions]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/services/yahooFinanceService.ts b/src/services/yahooFinanceService.ts
index 0da1548..332da1c 100644
--- a/src/services/yahooFinanceService.ts
+++ b/src/services/yahooFinanceService.ts
@@ -1,100 +1,119 @@
-import type { Asset, YahooSearchResponse, YahooChartResult } from "../types";
-import toast from "react-hot-toast";
-
-import { formatDateToISO } from "../utils/formatters";
-
-// this is only needed when hosted staticly without a proxy server or smt
-// TODO change it to use the proxy server
-const isDev = import.meta.env.DEV;
-const CORS_PROXY = 'https://corsproxy.io/?url=';
-const YAHOO_API = 'https://query1.finance.yahoo.com';
-const API_BASE = isDev ? '/yahoo' : `${CORS_PROXY}${encodeURIComponent(YAHOO_API)}`;
-
-export const EQUITY_TYPES = {
- all: "etf,equity,mutualfund,index,currency,cryptocurrency,future",
- ETF: "etf",
- Stock: "equity",
- "Etf or Stock": "etf,equity",
- Mutualfund: "mutualfund",
- Index: "index",
- Currency: "currency",
- Cryptocurrency: "cryptocurrency",
- Future: "future",
-};
-
-export const searchAssets = async (query: string, equityType: string): Promise => {
- try {
- const params = new URLSearchParams({
- query,
- lang: 'en-US',
- type: equityType,
- longName: 'true',
- });
-
- const url = `${API_BASE}/v1/finance/lookup${!isDev ? encodeURIComponent(`?${params}`) : `?${params}`}`;
- const response = await fetch(url);
- if (!response.ok) throw new Error('Network response was not ok');
-
- const data = await response.json() as YahooSearchResponse;
-
- if (data.finance.error) {
- throw new Error(data.finance.error);
- }
-
- if (!data.finance.result?.[0]?.documents) {
- return [];
- }
-
- return data.finance.result[0].documents
- .filter(quote => equityType.split(",").map(v => v.toLowerCase()).includes(quote.quoteType.toLowerCase()))
- .map((quote) => ({
- id: quote.symbol,
- isin: '', // not provided by Yahoo Finance API
- wkn: '', // not provided by Yahoo Finance API
- name: quote.shortName,
- rank: quote.rank,
- symbol: quote.symbol,
- quoteType: quote.quoteType,
- price: quote.regularMarketPrice.fmt,
- priceChange: quote.regularMarketChange.fmt,
- priceChangePercent: quote.regularMarketPercentChange.fmt,
- exchange: quote.exchange,
- historicalData: new Map(),
- investments: [],
- }));
- } catch (error) {
- console.error('Error searching assets:', error);
- toast.error('Failed to search assets. Please try again later.');
- return [];
- }
-};
-
-export const getHistoricalData = async (symbol: string, startDate: Date, endDate: Date) => {
- try {
- const start = Math.floor(startDate.getTime() / 1000);
- const end = Math.floor(endDate.getTime() / 1000);
-
- const params = new URLSearchParams({
- period1: start.toString(),
- period2: end.toString(),
- interval: '1d',
- });
-
- const url = `${API_BASE}/v8/finance/chart/${symbol}${!isDev ? encodeURIComponent(`?${params}`) : `?${params}`}`;
- const response = await fetch(url);
- if (!response.ok) throw new Error('Network response was not ok');
-
- const data = await response.json();
- const { timestamp, indicators, meta } = data.chart.result[0] as YahooChartResult;
- const quotes = indicators.quote[0];
-
- return {
- historicalData: new Map(timestamp.map((time: number, index: number) => [formatDateToISO(new Date(time * 1000)), quotes.close[index]])),
- longName: meta.longName
- }
- } catch (error) {
- console.error('Error fetching historical data:', error);
- toast.error(`Failed to fetch historical data for ${symbol}. Please try again later.`);
- return { historicalData: new Map(), longName: '' };
- }
-};
+import type { Asset, YahooSearchResponse, YahooChartResult } from "../types";
+import toast from "react-hot-toast";
+
+import { formatDateToISO } from "../utils/formatters";
+
+// this is only needed when hosted staticly without a proxy server or smt
+// TODO change it to use the proxy server
+const isDev = import.meta.env.DEV;
+const CORS_PROXY = 'https://corsproxy.io/?url=';
+const YAHOO_API = 'https://query1.finance.yahoo.com';
+const API_BASE = isDev ? '/yahoo' : `${CORS_PROXY}${encodeURIComponent(YAHOO_API)}`;
+
+export const EQUITY_TYPES = {
+ all: "etf,equity,mutualfund,index,currency,cryptocurrency,future",
+ ETF: "etf",
+ Stock: "equity",
+ "Etf or Stock": "etf,equity",
+ Mutualfund: "mutualfund",
+ Index: "index",
+ Currency: "currency",
+ Cryptocurrency: "cryptocurrency",
+ Future: "future",
+};
+
+export const searchAssets = async (query: string, equityType: string): Promise => {
+ try {
+ // Log input parameters for debugging
+ console.log(`Searching for "${query}" with type "${equityType}"`);
+
+ const params = new URLSearchParams({
+ query,
+ lang: 'en-US',
+ type: equityType,
+ longName: 'true',
+ });
+
+ const url = `${API_BASE}/v1/finance/lookup${!isDev ? encodeURIComponent(`?${params}`) : `?${params}`}`;
+ console.log(`Request URL: ${url}`);
+
+ const response = await fetch(url);
+ if (!response.ok) {
+ console.error(`Network error: ${response.status} ${response.statusText}`);
+ throw new Error('Network response was not ok');
+ }
+
+ const data = await response.json() as YahooSearchResponse;
+ console.log("API response:", data);
+
+ if (data.finance.error) {
+ console.error(`API error: ${data.finance.error}`);
+ throw new Error(data.finance.error);
+ }
+
+ if (!data.finance.result?.[0]?.documents) {
+ console.log("No results found");
+ return [];
+ }
+
+ const equityTypes = equityType.split(",").map(v => v.toLowerCase());
+
+ return data.finance.result[0].documents
+ .filter(quote => {
+ const matches = equityTypes.includes(quote.quoteType.toLowerCase());
+ if (!matches) {
+ console.log(`Filtering out ${quote.symbol} (${quote.quoteType}) as it doesn't match ${equityTypes.join(', ')}`);
+ }
+ return matches;
+ })
+ .map((quote) => ({
+ id: quote.symbol,
+ isin: '', // not provided by Yahoo Finance API
+ wkn: '', // not provided by Yahoo Finance API
+ name: quote.shortName,
+ rank: quote.rank,
+ symbol: quote.symbol,
+ quoteType: quote.quoteType,
+ price: quote.regularMarketPrice.fmt,
+ priceChange: quote.regularMarketChange.fmt,
+ priceChangePercent: quote.regularMarketPercentChange.fmt,
+ exchange: quote.exchange,
+ historicalData: new Map(),
+ investments: [],
+ }));
+ } catch (error) {
+ console.error('Error searching assets:', error);
+ toast.error('Failed to search assets. Please try again later.');
+ return [];
+ }
+};
+
+export const getHistoricalData = async (symbol: string, startDate: Date, endDate: Date) => {
+ try {
+ const start = Math.floor(startDate.getTime() / 1000);
+ const end = Math.floor(endDate.getTime() / 1000);
+
+ const params = new URLSearchParams({
+ period1: start.toString(),
+ period2: end.toString(),
+ interval: '1d',
+ });
+
+ const url = `${API_BASE}/v8/finance/chart/${symbol}${!isDev ? encodeURIComponent(`?${params}`) : `?${params}`}`;
+ const response = await fetch(url);
+ if (!response.ok) throw new Error('Network response was not ok');
+
+ const data = await response.json();
+ const { timestamp, indicators, meta } = data.chart.result[0] as YahooChartResult;
+ const quotes = indicators.quote[0];
+
+ return {
+ historicalData: new Map(timestamp.map((time: number, index: number) => [formatDateToISO(new Date(time * 1000)), quotes.close[index]])),
+ longName: meta.longName
+ }
+ } catch (error) {
+ console.error('Error fetching historical data:', error);
+ toast.error(`Failed to fetch historical data for ${symbol}. Please try again later.`);
+ return { historicalData: new Map(), longName: '' };
+ }
+};
diff --git a/src/types/index.ts b/src/types/index.ts
index f4ea685..b1c7f7a 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,210 +1,210 @@
-export interface Asset {
- id: string;
- isin: string;
- name: string;
- quoteType: string;
- price?: string;
- priceChange?: string;
- priceChangePercent?: string;
- rank: string;
- wkn: string;
- symbol: string;
- historicalData: Map;
- investments: Investment[];
-}
-
-export interface Investment {
- id: string;
- assetId: string;
- type: 'single' | 'periodic';
- amount: number;
- date?: Date;
- periodicGroupId?: string;
-}
-
-export interface PeriodicSettings {
- dayOfMonth: number;
- interval: number;
- intervalUnit: 'days' | 'weeks' | 'months' | 'quarters' | 'years';
- startDate: Date;
- dynamic?: {
- type: 'percentage' | 'fixed';
- value: number;
- yearInterval: number;
- };
-}
-
-export interface InvestmentPerformance {
- id: string;
- assetName: string;
- date: Date;
- investedAmount: number;
- investedAtPrice: number;
- currentValue: number;
- performancePercentage: number;
- periodicGroupId?: string;
-}
-
-export interface DateRange {
- startDate: Date;
- endDate: Date;
-}
-
-export interface InvestmentPerformance {
- id: string;
- assetName: string;
- date: Date;
- investedAmount: number;
- investedAtPrice: number;
- currentValue: number;
- performancePercentage: number;
-}
-
-export interface PortfolioPerformance {
- investments: InvestmentPerformance[];
- summary: {
- totalInvested: number;
- currentValue: number;
- annualPerformancesPerAsset: Map;
- performancePercentage: number;
- performancePerAnnoPerformance: number;
- ttworValue: number;
- ttworPercentage: number;
- bestPerformancePerAnno: { percentage: number, year: number }[];
- worstPerformancePerAnno: { percentage: number, year: number }[];
- annualPerformances: { year: number; percentage: number; }[];
- };
-}
-
-export type DayData = {
- date: Date;
- total: number;
- invested: number;
- percentageChange: number;
- /* Current price of asset */
- assets: { [key: string]: number };
-};
-
-export interface WithdrawalPlan {
- amount: number;
- interval: 'monthly' | 'yearly';
- startTrigger: 'date' | 'portfolioValue' | 'auto';
- startDate?: Date;
- startPortfolioValue?: number;
- enabled: boolean;
- autoStrategy?: {
- type: 'maintain' | 'deplete' | 'grow';
- targetYears?: number;
- targetGrowth?: number;
- };
-}
-
-export interface ProjectionData {
- date: Date;
- value: number;
- invested: number;
- withdrawals: number;
- totalWithdrawn: number;
-}
-
-export interface SustainabilityAnalysis {
- yearsToReachTarget: number;
- targetValue: number;
- sustainableYears: number | 'infinite';
-}
-
-export interface PeriodicSettings {
- startDate: Date;
- dayOfMonth: number;
- interval: number;
- amount: number;
- dynamic?: {
- type: 'percentage' | 'fixed';
- value: number;
- yearInterval: number;
- };
-}
-
-interface YahooQuoteDocument {
- symbol: string;
- shortName: string;
- rank: string;
- regularMarketPrice: {
- raw: number;
- fmt: string;
- };
- regularMarketChange: {
- raw: number;
- fmt: string;
- };
- regularMarketPercentChange: {
- raw: number;
- fmt: string;
- };
- exchange: string;
- quoteType: string;
-}
-
-export interface YahooSearchResponse {
- finance: {
- result: [{
- documents: YahooQuoteDocument[];
- }];
- error: null | string;
- };
-}
-
-export interface YahooChartResult {
- timestamp: number[];
- meta: {
- currency: string;
- symbol: string;
- exchangeName: string;
- fullExchangeName: string;
- instrumentType: string;
- firstTradeDate: number;
- regularMarketTime: number;
- hasPrePostMarketData: boolean;
- gmtoffset: number;
- timezone: string;
- exchangeTimezoneName: string;
- regularMarketPrice: number;
- fiftyTwoWeekHigh: number;
- fiftyTwoWeekLow: number;
- regularMarketDayHigh: number;
- regularMarketDayLow: number;
- regularMarketVolume: number;
- longName: string;
- shortName: string;
- chartPreviousClose: number;
- priceHint: number;
- currentTradingPeriod: {
- pre: {
- timezone: string;
- start: number;
- end: number;
- gmtoffset: number;
- };
- regular: {
- timezone: string;
- start: number;
- end: number;
- gmtoffset: number;
- };
- post: {
- timezone: string;
- start: number;
- end: number;
- gmtoffset: number;
- };
- };
- dataGranularity: string;
- range: string;
- validRanges: string[];
- }
- indicators: {
- quote: [{
- close: number[];
- }];
- };
-}
+export interface Asset {
+ id: string;
+ isin: string;
+ name: string;
+ quoteType: string;
+ price?: string;
+ priceChange?: string;
+ priceChangePercent?: string;
+ rank: string;
+ wkn: string;
+ symbol: string;
+ historicalData: Map;
+ investments: Investment[];
+}
+
+export interface Investment {
+ id: string;
+ assetId: string;
+ type: 'single' | 'periodic';
+ amount: number;
+ date?: Date;
+ periodicGroupId?: string;
+}
+
+export interface PeriodicSettings {
+ dayOfMonth: number;
+ interval: number;
+ intervalUnit: 'days' | 'weeks' | 'months' | 'quarters' | 'years';
+ startDate: Date;
+ dynamic?: {
+ type: 'percentage' | 'fixed';
+ value: number;
+ yearInterval: number;
+ };
+}
+
+export interface InvestmentPerformance {
+ id: string;
+ assetName: string;
+ date: Date;
+ investedAmount: number;
+ investedAtPrice: number;
+ currentValue: number;
+ performancePercentage: number;
+ periodicGroupId?: string;
+}
+
+export interface DateRange {
+ startDate: Date;
+ endDate: Date;
+}
+
+export interface InvestmentPerformance {
+ id: string;
+ assetName: string;
+ date: Date;
+ investedAmount: number;
+ investedAtPrice: number;
+ currentValue: number;
+ performancePercentage: number;
+}
+
+export interface PortfolioPerformance {
+ investments: InvestmentPerformance[];
+ summary: {
+ totalInvested: number;
+ currentValue: number;
+ annualPerformancesPerAsset: Map;
+ performancePercentage: number;
+ performancePerAnnoPerformance: number;
+ ttworValue: number;
+ ttworPercentage: number;
+ bestPerformancePerAnno: { percentage: number, year: number }[];
+ worstPerformancePerAnno: { percentage: number, year: number }[];
+ annualPerformances: { year: number; percentage: number; }[];
+ };
+}
+
+export type DayData = {
+ date: Date;
+ total: number;
+ invested: number;
+ percentageChange: number;
+ /* Current price of asset */
+ assets: { [key: string]: number };
+};
+
+export interface WithdrawalPlan {
+ amount: number;
+ interval: 'monthly' | 'yearly';
+ startTrigger: 'date' | 'portfolioValue' | 'auto';
+ startDate?: Date;
+ startPortfolioValue?: number;
+ enabled: boolean;
+ autoStrategy?: {
+ type: 'maintain' | 'deplete' | 'grow';
+ targetYears?: number;
+ targetGrowth?: number;
+ };
+}
+
+export interface ProjectionData {
+ date: Date;
+ value: number;
+ invested: number;
+ withdrawals: number;
+ totalWithdrawn: number;
+}
+
+export interface SustainabilityAnalysis {
+ yearsToReachTarget: number;
+ targetValue: number;
+ sustainableYears: number | 'infinite';
+}
+
+export interface PeriodicSettings {
+ startDate: Date;
+ dayOfMonth: number;
+ interval: number;
+ amount: number;
+ dynamic?: {
+ type: 'percentage' | 'fixed';
+ value: number;
+ yearInterval: number;
+ };
+}
+
+interface YahooQuoteDocument {
+ symbol: string;
+ shortName: string;
+ rank: string;
+ regularMarketPrice: {
+ raw: number;
+ fmt: string;
+ };
+ regularMarketChange: {
+ raw: number;
+ fmt: string;
+ };
+ regularMarketPercentChange: {
+ raw: number;
+ fmt: string;
+ };
+ exchange: string;
+ quoteType: string;
+}
+
+export interface YahooSearchResponse {
+ finance: {
+ result: [{
+ documents: YahooQuoteDocument[];
+ }];
+ error: null | string;
+ };
+}
+
+export interface YahooChartResult {
+ timestamp: number[];
+ meta: {
+ currency: string;
+ symbol: string;
+ exchangeName: string;
+ fullExchangeName: string;
+ instrumentType: string;
+ firstTradeDate: number;
+ regularMarketTime: number;
+ hasPrePostMarketData: boolean;
+ gmtoffset: number;
+ timezone: string;
+ exchangeTimezoneName: string;
+ regularMarketPrice: number;
+ fiftyTwoWeekHigh: number;
+ fiftyTwoWeekLow: number;
+ regularMarketDayHigh: number;
+ regularMarketDayLow: number;
+ regularMarketVolume: number;
+ longName: string;
+ shortName: string;
+ chartPreviousClose: number;
+ priceHint: number;
+ currentTradingPeriod: {
+ pre: {
+ timezone: string;
+ start: number;
+ end: number;
+ gmtoffset: number;
+ };
+ regular: {
+ timezone: string;
+ start: number;
+ end: number;
+ gmtoffset: number;
+ };
+ post: {
+ timezone: string;
+ start: number;
+ end: number;
+ gmtoffset: number;
+ };
+ };
+ dataGranularity: string;
+ range: string;
+ validRanges: string[];
+ }
+ indicators: {
+ quote: [{
+ close: number[];
+ }];
+ };
+}
diff --git a/src/utils/calculations/assetValue.ts b/src/utils/calculations/assetValue.ts
index 14d5493..f96995e 100644
--- a/src/utils/calculations/assetValue.ts
+++ b/src/utils/calculations/assetValue.ts
@@ -1,123 +1,123 @@
-import { addDays, addMonths, addWeeks, addYears, isAfter, isSameDay, setDate } from "date-fns";
-
-import { formatDateToISO } from "../formatters";
-
-import type { Asset, Investment, PeriodicSettings } from "../../types";
-export const calculateAssetValueAtDate = (asset: Asset, date: Date, currentPrice: number) => {
- let totalShares = 0;
-
- const buyIns: number[] = [];
- // Calculate shares for each investment up to the given date
- for (const investment of asset.investments) {
- const invDate = new Date(investment.date!);
- if (isAfter(invDate, date) || isSameDay(invDate, date)) continue;
-
- // Find price at investment date
- let investmentPrice = asset.historicalData.get(formatDateToISO(invDate)) || 0;
- // if no investment price found, try to find the nearest price
- if(!investmentPrice) {
- let previousDate = invDate;
- let afterDate = invDate;
- while(!investmentPrice) {
- previousDate = addDays(previousDate, -1);
- afterDate = addDays(afterDate, 1);
- investmentPrice = asset.historicalData.get(formatDateToISO(previousDate)) || asset.historicalData.get(formatDateToISO(afterDate)) || 0;
- }
- }
-
- if (investmentPrice > 0) {
- totalShares += investment.amount / investmentPrice;
- buyIns.push(investmentPrice);
- }
- }
-
- // Return current value of all shares
- return {
- investedValue: totalShares * currentPrice,
- avgBuyIn: buyIns.reduce((a, b) => a + b, 0) / buyIns.length,
- }
-};
-
-
-export const generatePeriodicInvestments = (settings: PeriodicSettings, endDate: Date, assetId: string): Investment[] => {
- const investments: Investment[] = [];
- const periodicGroupId = crypto.randomUUID();
-
- // Create UTC dates
- let currentDate = new Date(Date.UTC(
- settings.startDate.getUTCFullYear(),
- settings.startDate.getUTCMonth(),
- settings.startDate.getUTCDate()
- ));
-
- const end = new Date(Date.UTC(
- endDate.getUTCFullYear(),
- endDate.getUTCMonth(),
- endDate.getUTCDate()
- ));
-
- let currentAmount = settings.amount;
-
- while (currentDate <= end) {
- // For monthly/yearly intervals, ensure we're on the correct day of month
- if (settings.intervalUnit !== 'days') {
- currentDate = setDate(currentDate, settings.dayOfMonth);
- }
-
- // Only add investment if we haven't passed the end date
- if (currentDate <= end) {
- // Handle dynamic increases if configured
- if (settings.dynamic) {
- const yearsSinceStart =
- (currentDate.getTime() - settings.startDate.getTime()) /
- (1000 * 60 * 60 * 24 * 365);
-
- if (yearsSinceStart > 0 && yearsSinceStart % settings.dynamic.yearInterval === 0) {
- if (settings.dynamic.type === 'percentage') {
- currentAmount *= (1 + (settings.dynamic.value / 100));
- } else {
- currentAmount += settings.dynamic.value;
- }
- }
- }
-
- investments.push({
- id: crypto.randomUUID(),
- type: 'periodic',
- amount: currentAmount,
- date: currentDate,
- periodicGroupId,
- assetId
- });
- }
-
- // Calculate next date based on interval unit
- switch (settings.intervalUnit) {
- case 'days':
- currentDate = addDays(currentDate, settings.interval);
- break;
- case 'weeks':
- currentDate = addWeeks(currentDate, settings.interval);
- break;
- case 'months':
- currentDate = addMonths(currentDate, settings.interval);
- // Ensure we maintain the correct day of month using UTC
- if (currentDate.getUTCDate() !== settings.dayOfMonth) {
- currentDate = setDate(currentDate, settings.dayOfMonth);
- }
- break;
- case 'quarters':
- currentDate = addMonths(currentDate, settings.interval * 3);
- break;
- case 'years':
- currentDate = addYears(currentDate, settings.interval);
- // Ensure we maintain the correct day of month using UTC
- if (currentDate.getUTCDate() !== settings.dayOfMonth) {
- currentDate = setDate(currentDate, settings.dayOfMonth);
- }
- break;
- }
- }
-
- return investments;
-};
+import { addDays, addMonths, addWeeks, addYears, isAfter, isSameDay, setDate } from "date-fns";
+
+import { formatDateToISO } from "../formatters";
+
+import type { Asset, Investment, PeriodicSettings } from "../../types";
+export const calculateAssetValueAtDate = (asset: Asset, date: Date, currentPrice: number) => {
+ let totalShares = 0;
+
+ const buyIns: number[] = [];
+ // Calculate shares for each investment up to the given date
+ for (const investment of asset.investments) {
+ const invDate = new Date(investment.date!);
+ if (isAfter(invDate, date) || isSameDay(invDate, date)) continue;
+
+ // Find price at investment date
+ let investmentPrice = asset.historicalData.get(formatDateToISO(invDate)) || 0;
+ // if no investment price found, try to find the nearest price
+ if(!investmentPrice) {
+ let previousDate = invDate;
+ let afterDate = invDate;
+ while(!investmentPrice) {
+ previousDate = addDays(previousDate, -1);
+ afterDate = addDays(afterDate, 1);
+ investmentPrice = asset.historicalData.get(formatDateToISO(previousDate)) || asset.historicalData.get(formatDateToISO(afterDate)) || 0;
+ }
+ }
+
+ if (investmentPrice > 0) {
+ totalShares += investment.amount / investmentPrice;
+ buyIns.push(investmentPrice);
+ }
+ }
+
+ // Return current value of all shares
+ return {
+ investedValue: totalShares * currentPrice,
+ avgBuyIn: buyIns.reduce((a, b) => a + b, 0) / buyIns.length,
+ }
+};
+
+
+export const generatePeriodicInvestments = (settings: PeriodicSettings, endDate: Date, assetId: string): Investment[] => {
+ const investments: Investment[] = [];
+ const periodicGroupId = crypto.randomUUID();
+
+ // Create UTC dates
+ let currentDate = new Date(Date.UTC(
+ settings.startDate.getUTCFullYear(),
+ settings.startDate.getUTCMonth(),
+ settings.startDate.getUTCDate()
+ ));
+
+ const end = new Date(Date.UTC(
+ endDate.getUTCFullYear(),
+ endDate.getUTCMonth(),
+ endDate.getUTCDate()
+ ));
+
+ let currentAmount = settings.amount;
+
+ while (currentDate <= end) {
+ // For monthly/yearly intervals, ensure we're on the correct day of month
+ if (settings.intervalUnit !== 'days') {
+ currentDate = setDate(currentDate, settings.dayOfMonth);
+ }
+
+ // Only add investment if we haven't passed the end date
+ if (currentDate <= end) {
+ // Handle dynamic increases if configured
+ if (settings.dynamic) {
+ const yearsSinceStart =
+ (currentDate.getTime() - settings.startDate.getTime()) /
+ (1000 * 60 * 60 * 24 * 365);
+
+ if (yearsSinceStart > 0 && yearsSinceStart % settings.dynamic.yearInterval === 0) {
+ if (settings.dynamic.type === 'percentage') {
+ currentAmount *= (1 + (settings.dynamic.value / 100));
+ } else {
+ currentAmount += settings.dynamic.value;
+ }
+ }
+ }
+
+ investments.push({
+ id: crypto.randomUUID(),
+ type: 'periodic',
+ amount: currentAmount,
+ date: currentDate,
+ periodicGroupId,
+ assetId
+ });
+ }
+
+ // Calculate next date based on interval unit
+ switch (settings.intervalUnit) {
+ case 'days':
+ currentDate = addDays(currentDate, settings.interval);
+ break;
+ case 'weeks':
+ currentDate = addWeeks(currentDate, settings.interval);
+ break;
+ case 'months':
+ currentDate = addMonths(currentDate, settings.interval);
+ // Ensure we maintain the correct day of month using UTC
+ if (currentDate.getUTCDate() !== settings.dayOfMonth) {
+ currentDate = setDate(currentDate, settings.dayOfMonth);
+ }
+ break;
+ case 'quarters':
+ currentDate = addMonths(currentDate, settings.interval * 3);
+ break;
+ case 'years':
+ currentDate = addYears(currentDate, settings.interval);
+ // Ensure we maintain the correct day of month using UTC
+ if (currentDate.getUTCDate() !== settings.dayOfMonth) {
+ currentDate = setDate(currentDate, settings.dayOfMonth);
+ }
+ break;
+ }
+ }
+
+ return investments;
+};
diff --git a/src/utils/calculations/futureProjection.ts b/src/utils/calculations/futureProjection.ts
index 25b465c..6293a7c 100644
--- a/src/utils/calculations/futureProjection.ts
+++ b/src/utils/calculations/futureProjection.ts
@@ -1,255 +1,255 @@
-import { addMonths, differenceInYears } from "date-fns";
-
-import type {
- ProjectionData, SustainabilityAnalysis, WithdrawalPlan, Asset, Investment
-} from "../../types";
-
-const findOptimalStartingPoint = (
- currentPortfolioValue: number,
- monthlyGrowth: number,
- desiredWithdrawal: number,
- strategy: WithdrawalPlan['autoStrategy'],
- interval: 'monthly' | 'yearly'
-): { startDate: Date; requiredPortfolioValue: number } => {
- const monthlyWithdrawal = interval === 'yearly' ? desiredWithdrawal / 12 : desiredWithdrawal;
- let requiredPortfolioValue = 0;
-
- // Declare variables outside switch
- const months = (strategy?.targetYears || 30) * 12;
- const r = monthlyGrowth;
- const targetGrowth = (strategy?.targetGrowth || 2) / 100;
- const targetMonthlyGrowth = Math.pow(1 + targetGrowth, 1 / 12) - 1;
-
- switch (strategy?.type) {
- case 'maintain':
- requiredPortfolioValue = monthlyWithdrawal / monthlyGrowth;
- break;
- case 'deplete':
- requiredPortfolioValue = (monthlyWithdrawal * (Math.pow(1 + r, months) - 1)) / (r * Math.pow(1 + r, months));
- break;
- case 'grow':
- requiredPortfolioValue = monthlyWithdrawal / (monthlyGrowth - targetMonthlyGrowth);
- break;
- }
-
- // Calculate when we'll reach the required value
- const monthsToReach = Math.ceil(
- Math.log(requiredPortfolioValue / currentPortfolioValue) /
- Math.log(1 + monthlyGrowth)
- );
-
- const startDate = new Date();
- startDate.setMonth(startDate.getMonth() + Math.max(0, monthsToReach));
-
- return {
- startDate,
- requiredPortfolioValue,
- };
-};
-
-export const calculateFutureProjection = async (
- currentAssets: Asset[],
- yearsToProject: number,
- annualReturnRate: number,
- withdrawalPlan?: WithdrawalPlan,
- startFromZero: boolean = false
-): Promise<{
- projection: ProjectionData[];
- sustainability: SustainabilityAnalysis;
-}> => {
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- const projectionData: ProjectionData[] = [];
- const maxProjectionYears = 100; // Project up to 100 years to find true sustainability
- const endDateForDisplay = addMonths(new Date(), yearsToProject * 12);
- const endDateForCalculation = addMonths(new Date(), maxProjectionYears * 12);
-
- // Get all periodic investment patterns
- const periodicInvestments = currentAssets.flatMap(asset => {
- const patterns = new Map();
-
- // When startFromZero is true, only include periodic investments
- asset.investments.forEach(inv => {
- if (inv.type === 'periodic' && inv.periodicGroupId) {
- if (!patterns.has(inv.periodicGroupId)) {
- patterns.set(inv.periodicGroupId, []);
- }
- patterns.get(inv.periodicGroupId)!.push(inv);
- }
- });
-
- return Array.from(patterns.values())
- .map(group => ({
- pattern: group.sort((a, b) =>
- new Date(a.date!).getTime() - new Date(b.date!).getTime()
- )
- }));
- });
-
- // Project future investments
- const futureInvestments = periodicInvestments.flatMap(({ pattern }) => {
- if (pattern.length < 2) return [];
-
- const lastInvestment = pattern[pattern.length - 1];
- const secondLastInvestment = pattern[pattern.length - 2];
-
- const interval = new Date(lastInvestment.date!).getTime() -
- new Date(secondLastInvestment.date!).getTime();
- const amountDiff = lastInvestment.amount - secondLastInvestment.amount;
-
- const future: Investment[] = [];
- let currentDate = new Date(lastInvestment.date!);
- let currentAmount = lastInvestment.amount;
-
- while (currentDate <= endDateForCalculation) {
- currentDate = new Date(currentDate.getTime() + interval);
- currentAmount += amountDiff;
-
- future.push({
- ...lastInvestment,
- date: currentDate,
- amount: currentAmount,
- });
- }
-
- return future;
- });
-
- // Calculate monthly values
- let currentDate = new Date();
-
- // Initialize totalInvested based on startFromZero flag
- let totalInvested = 0;
- if (!startFromZero) {
- // Include all investments if not starting from zero
- totalInvested = currentAssets.reduce(
- (sum, asset) => sum + asset.investments.reduce(
- (assetSum, inv) => assetSum + inv.amount, 0
- ), 0
- );
- } else {
- // Start from zero when startFromZero is true
- totalInvested = 0;
- // Don't initialize with any periodic investments - they'll accumulate over time
- }
-
- let totalWithdrawn = 0;
- let yearsToReachTarget = 0;
- let targetValue = 0;
- let sustainableYears: number | 'infinite' = 'infinite';
- let portfolioValue = startFromZero ? 0 : totalInvested; // Start from 0 if startFromZero is true
- let withdrawalsStarted = false;
- let withdrawalStartDate: Date | null = null;
- let portfolioDepletionDate: Date | null = null;
-
- // Calculate optimal withdrawal plan if auto strategy is selected
- if (withdrawalPlan?.enabled && withdrawalPlan.startTrigger === 'auto') {
- const { startDate, requiredPortfolioValue } = findOptimalStartingPoint(
- portfolioValue,
- Math.pow(1 + annualReturnRate / 100, 1 / 12) - 1,
- withdrawalPlan.amount,
- withdrawalPlan.autoStrategy,
- withdrawalPlan.interval
- );
-
- withdrawalPlan.startDate = startDate;
- withdrawalPlan.startPortfolioValue = requiredPortfolioValue;
- }
-
- while (currentDate <= endDateForCalculation) {
- // Check if withdrawals should start
- if (!withdrawalsStarted && withdrawalPlan?.enabled) {
- withdrawalsStarted = withdrawalPlan.startTrigger === 'date'
- ? new Date(currentDate) >= new Date(withdrawalPlan.startDate!)
- : portfolioValue >= (withdrawalPlan.startPortfolioValue || 0);
-
- if (withdrawalsStarted) {
- withdrawalStartDate = new Date(currentDate);
- }
- }
-
- // Handle monthly growth if portfolio isn't depleted
- if (portfolioValue > 0) {
- const monthlyReturn = Math.pow(1 + annualReturnRate / 100, 1 / 12) - 1;
- portfolioValue *= (1 + monthlyReturn);
- }
-
- // Add new investments only if withdrawals haven't started
- if (!withdrawalsStarted) {
- const monthInvestments = futureInvestments.filter(
- inv => new Date(inv.date!).getMonth() === currentDate.getMonth() &&
- new Date(inv.date!).getFullYear() === currentDate.getFullYear()
- );
-
- const monthlyInvestment = monthInvestments.reduce(
- (sum, inv) => sum + inv.amount, 0
- );
-
- // Always add periodic investments to both totalInvested and portfolioValue
- totalInvested += monthlyInvestment;
- portfolioValue += monthlyInvestment;
- }
-
- // Handle withdrawals
- let monthlyWithdrawal = 0;
- if (withdrawalsStarted && portfolioValue > 0) {
- monthlyWithdrawal = withdrawalPlan!.interval === 'monthly'
- ? withdrawalPlan!.amount
- : (currentDate.getMonth() === 0 ? withdrawalPlan!.amount : 0);
-
- portfolioValue -= monthlyWithdrawal;
- if (portfolioValue < 0) {
- monthlyWithdrawal += portfolioValue; // Adjust final withdrawal
- portfolioValue = 0;
- if (sustainableYears === 'infinite') {
- sustainableYears = differenceInYears(currentDate, withdrawalStartDate!);
- }
- }
- totalWithdrawn += monthlyWithdrawal;
- }
-
- // Update target metrics
- if (withdrawalsStarted && !targetValue) {
- targetValue = portfolioValue;
- yearsToReachTarget = differenceInYears(currentDate, new Date());
- }
-
- if (portfolioValue <= 0 && !portfolioDepletionDate) {
- portfolioDepletionDate = new Date(currentDate);
- }
-
- // Only add to projection data if within display timeframe
- if (currentDate <= endDateForDisplay) {
- projectionData.push({
- date: currentDate,
- value: Math.max(0, portfolioValue),
- invested: totalInvested,
- withdrawals: monthlyWithdrawal,
- totalWithdrawn,
- });
- }
-
- currentDate = addMonths(currentDate, 1);
- }
-
- // Calculate actual sustainability duration
- let actualSustainableYears: number | 'infinite' = 'infinite';
- if (portfolioDepletionDate) {
- actualSustainableYears = differenceInYears(
- portfolioDepletionDate,
- withdrawalStartDate || new Date()
- );
- } else if (portfolioValue > 0) {
- // If portfolio is still growing after maxProjectionYears, it's truly sustainable
- actualSustainableYears = 'infinite';
- }
-
- return {
- projection: projectionData,
- sustainability: {
- yearsToReachTarget,
- targetValue,
- sustainableYears: actualSustainableYears,
- },
- };
-};
+import { addMonths, differenceInYears } from "date-fns";
+
+import type {
+ ProjectionData, SustainabilityAnalysis, WithdrawalPlan, Asset, Investment
+} from "../../types";
+
+const findOptimalStartingPoint = (
+ currentPortfolioValue: number,
+ monthlyGrowth: number,
+ desiredWithdrawal: number,
+ strategy: WithdrawalPlan['autoStrategy'],
+ interval: 'monthly' | 'yearly'
+): { startDate: Date; requiredPortfolioValue: number } => {
+ const monthlyWithdrawal = interval === 'yearly' ? desiredWithdrawal / 12 : desiredWithdrawal;
+ let requiredPortfolioValue = 0;
+
+ // Declare variables outside switch
+ const months = (strategy?.targetYears || 30) * 12;
+ const r = monthlyGrowth;
+ const targetGrowth = (strategy?.targetGrowth || 2) / 100;
+ const targetMonthlyGrowth = Math.pow(1 + targetGrowth, 1 / 12) - 1;
+
+ switch (strategy?.type) {
+ case 'maintain':
+ requiredPortfolioValue = monthlyWithdrawal / monthlyGrowth;
+ break;
+ case 'deplete':
+ requiredPortfolioValue = (monthlyWithdrawal * (Math.pow(1 + r, months) - 1)) / (r * Math.pow(1 + r, months));
+ break;
+ case 'grow':
+ requiredPortfolioValue = monthlyWithdrawal / (monthlyGrowth - targetMonthlyGrowth);
+ break;
+ }
+
+ // Calculate when we'll reach the required value
+ const monthsToReach = Math.ceil(
+ Math.log(requiredPortfolioValue / currentPortfolioValue) /
+ Math.log(1 + monthlyGrowth)
+ );
+
+ const startDate = new Date();
+ startDate.setMonth(startDate.getMonth() + Math.max(0, monthsToReach));
+
+ return {
+ startDate,
+ requiredPortfolioValue,
+ };
+};
+
+export const calculateFutureProjection = async (
+ currentAssets: Asset[],
+ yearsToProject: number,
+ annualReturnRate: number,
+ withdrawalPlan?: WithdrawalPlan,
+ startFromZero: boolean = false
+): Promise<{
+ projection: ProjectionData[];
+ sustainability: SustainabilityAnalysis;
+}> => {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ const projectionData: ProjectionData[] = [];
+ const maxProjectionYears = 100; // Project up to 100 years to find true sustainability
+ const endDateForDisplay = addMonths(new Date(), yearsToProject * 12);
+ const endDateForCalculation = addMonths(new Date(), maxProjectionYears * 12);
+
+ // Get all periodic investment patterns
+ const periodicInvestments = currentAssets.flatMap(asset => {
+ const patterns = new Map();
+
+ // When startFromZero is true, only include periodic investments
+ asset.investments.forEach(inv => {
+ if (inv.type === 'periodic' && inv.periodicGroupId) {
+ if (!patterns.has(inv.periodicGroupId)) {
+ patterns.set(inv.periodicGroupId, []);
+ }
+ patterns.get(inv.periodicGroupId)!.push(inv);
+ }
+ });
+
+ return Array.from(patterns.values())
+ .map(group => ({
+ pattern: group.sort((a, b) =>
+ new Date(a.date!).getTime() - new Date(b.date!).getTime()
+ )
+ }));
+ });
+
+ // Project future investments
+ const futureInvestments = periodicInvestments.flatMap(({ pattern }) => {
+ if (pattern.length < 2) return [];
+
+ const lastInvestment = pattern[pattern.length - 1];
+ const secondLastInvestment = pattern[pattern.length - 2];
+
+ const interval = new Date(lastInvestment.date!).getTime() -
+ new Date(secondLastInvestment.date!).getTime();
+ const amountDiff = lastInvestment.amount - secondLastInvestment.amount;
+
+ const future: Investment[] = [];
+ let currentDate = new Date(lastInvestment.date!);
+ let currentAmount = lastInvestment.amount;
+
+ while (currentDate <= endDateForCalculation) {
+ currentDate = new Date(currentDate.getTime() + interval);
+ currentAmount += amountDiff;
+
+ future.push({
+ ...lastInvestment,
+ date: currentDate,
+ amount: currentAmount,
+ });
+ }
+
+ return future;
+ });
+
+ // Calculate monthly values
+ let currentDate = new Date();
+
+ // Initialize totalInvested based on startFromZero flag
+ let totalInvested = 0;
+ if (!startFromZero) {
+ // Include all investments if not starting from zero
+ totalInvested = currentAssets.reduce(
+ (sum, asset) => sum + asset.investments.reduce(
+ (assetSum, inv) => assetSum + inv.amount, 0
+ ), 0
+ );
+ } else {
+ // Start from zero when startFromZero is true
+ totalInvested = 0;
+ // Don't initialize with any periodic investments - they'll accumulate over time
+ }
+
+ let totalWithdrawn = 0;
+ let yearsToReachTarget = 0;
+ let targetValue = 0;
+ let sustainableYears: number | 'infinite' = 'infinite';
+ let portfolioValue = startFromZero ? 0 : totalInvested; // Start from 0 if startFromZero is true
+ let withdrawalsStarted = false;
+ let withdrawalStartDate: Date | null = null;
+ let portfolioDepletionDate: Date | null = null;
+
+ // Calculate optimal withdrawal plan if auto strategy is selected
+ if (withdrawalPlan?.enabled && withdrawalPlan.startTrigger === 'auto') {
+ const { startDate, requiredPortfolioValue } = findOptimalStartingPoint(
+ portfolioValue,
+ Math.pow(1 + annualReturnRate / 100, 1 / 12) - 1,
+ withdrawalPlan.amount,
+ withdrawalPlan.autoStrategy,
+ withdrawalPlan.interval
+ );
+
+ withdrawalPlan.startDate = startDate;
+ withdrawalPlan.startPortfolioValue = requiredPortfolioValue;
+ }
+
+ while (currentDate <= endDateForCalculation) {
+ // Check if withdrawals should start
+ if (!withdrawalsStarted && withdrawalPlan?.enabled) {
+ withdrawalsStarted = withdrawalPlan.startTrigger === 'date'
+ ? new Date(currentDate) >= new Date(withdrawalPlan.startDate!)
+ : portfolioValue >= (withdrawalPlan.startPortfolioValue || 0);
+
+ if (withdrawalsStarted) {
+ withdrawalStartDate = new Date(currentDate);
+ }
+ }
+
+ // Handle monthly growth if portfolio isn't depleted
+ if (portfolioValue > 0) {
+ const monthlyReturn = Math.pow(1 + annualReturnRate / 100, 1 / 12) - 1;
+ portfolioValue *= (1 + monthlyReturn);
+ }
+
+ // Add new investments only if withdrawals haven't started
+ if (!withdrawalsStarted) {
+ const monthInvestments = futureInvestments.filter(
+ inv => new Date(inv.date!).getMonth() === currentDate.getMonth() &&
+ new Date(inv.date!).getFullYear() === currentDate.getFullYear()
+ );
+
+ const monthlyInvestment = monthInvestments.reduce(
+ (sum, inv) => sum + inv.amount, 0
+ );
+
+ // Always add periodic investments to both totalInvested and portfolioValue
+ totalInvested += monthlyInvestment;
+ portfolioValue += monthlyInvestment;
+ }
+
+ // Handle withdrawals
+ let monthlyWithdrawal = 0;
+ if (withdrawalsStarted && portfolioValue > 0) {
+ monthlyWithdrawal = withdrawalPlan!.interval === 'monthly'
+ ? withdrawalPlan!.amount
+ : (currentDate.getMonth() === 0 ? withdrawalPlan!.amount : 0);
+
+ portfolioValue -= monthlyWithdrawal;
+ if (portfolioValue < 0) {
+ monthlyWithdrawal += portfolioValue; // Adjust final withdrawal
+ portfolioValue = 0;
+ if (sustainableYears === 'infinite') {
+ sustainableYears = differenceInYears(currentDate, withdrawalStartDate!);
+ }
+ }
+ totalWithdrawn += monthlyWithdrawal;
+ }
+
+ // Update target metrics
+ if (withdrawalsStarted && !targetValue) {
+ targetValue = portfolioValue;
+ yearsToReachTarget = differenceInYears(currentDate, new Date());
+ }
+
+ if (portfolioValue <= 0 && !portfolioDepletionDate) {
+ portfolioDepletionDate = new Date(currentDate);
+ }
+
+ // Only add to projection data if within display timeframe
+ if (currentDate <= endDateForDisplay) {
+ projectionData.push({
+ date: currentDate,
+ value: Math.max(0, portfolioValue),
+ invested: totalInvested,
+ withdrawals: monthlyWithdrawal,
+ totalWithdrawn,
+ });
+ }
+
+ currentDate = addMonths(currentDate, 1);
+ }
+
+ // Calculate actual sustainability duration
+ let actualSustainableYears: number | 'infinite' = 'infinite';
+ if (portfolioDepletionDate) {
+ actualSustainableYears = differenceInYears(
+ portfolioDepletionDate,
+ withdrawalStartDate || new Date()
+ );
+ } else if (portfolioValue > 0) {
+ // If portfolio is still growing after maxProjectionYears, it's truly sustainable
+ actualSustainableYears = 'infinite';
+ }
+
+ return {
+ projection: projectionData,
+ sustainability: {
+ yearsToReachTarget,
+ targetValue,
+ sustainableYears: actualSustainableYears,
+ },
+ };
+};
diff --git a/src/utils/calculations/performance.ts b/src/utils/calculations/performance.ts
index 4427232..bbeba8f 100644
--- a/src/utils/calculations/performance.ts
+++ b/src/utils/calculations/performance.ts
@@ -1,233 +1,233 @@
-import { addDays, isBefore } from "date-fns";
-
-import { formatDateToISO } from "../formatters";
-
-import type { Asset, InvestmentPerformance, PortfolioPerformance } from "../../types";
-export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerformance => {
- const investments: InvestmentPerformance[] = [];
- let totalInvested = 0;
- let totalCurrentValue = 0;
- let earliestDate: Date | null = null;
-
- // TTWOR Berechnung
- const firstDayPrices: Record = {};
- const currentPrices: Record = {};
- const investedPerAsset: Record = {};
-
- // Sammle erste und letzte Preise fΓΌr jedes Asset
- for (const asset of assets) {
- const keys = Array.from(asset.historicalData.values());
- const firstDay = keys[0];
- const lastDay = keys[keys.length - 1];
- firstDayPrices[asset.id] = firstDay;
- currentPrices[asset.id] = lastDay;
- investedPerAsset[asset.id] = asset.investments.reduce((sum, inv) => sum + inv.amount, 0);
- }
-
- // Berechne TTWOR
- const ttworValue = Object.entries(investedPerAsset).reduce((acc, [assetId, invested]) => {
- if (firstDayPrices[assetId] && currentPrices[assetId] && firstDayPrices[assetId] > 0) {
- const shares = invested / firstDayPrices[assetId];
- return acc + (shares * currentPrices[assetId]);
- }
- return acc;
- }, 0);
-
- // Calculate portfolio-level annual performances
- const annualPerformances: { year: number; percentage: number }[] = [];
- const annualPerformancesPerAsset = new Map();
-
- // Finde das frΓΌheste Investmentdatum
- for (const asset of assets) {
- for (const investment of asset.investments) {
- const investmentDate = new Date(investment.date!);
- if (!earliestDate || isBefore(investmentDate, earliestDate)) {
- earliestDate = investmentDate;
- }
- }
- const historicalData = Array.from(asset.historicalData.entries());
- const firstDate = new Date(historicalData[0][0]);
- const temp_assetAnnualPerformances: { year: number; percentage: number; price: number }[] = [];
- for (let year = firstDate.getFullYear(); year <= new Date().getFullYear(); year++) {
- const yearStart = new Date(year, 0, 1);
- const yearEnd = new Date(year, 11, 31);
- let startDate = yearStart;
- let endDate = yearEnd;
- let startPrice = asset.historicalData.get(formatDateToISO(startDate));
- let endPrice = asset.historicalData.get(formatDateToISO(endDate));
- while(!startPrice || !endPrice) {
- startDate = addDays(startDate, 1);
- endDate = addDays(endDate, -1);
- endPrice = endPrice || asset.historicalData.get(formatDateToISO(endDate)) || 0;
- startPrice = startPrice || asset.historicalData.get(formatDateToISO(startDate)) || 0;
- if(endDate.getTime() < yearStart.getTime() || startDate.getTime() > yearEnd.getTime()) {
- break;
- }
- }
- if (startPrice && endPrice) {
- const percentage = ((endPrice - startPrice) / startPrice) * 100;
- temp_assetAnnualPerformances.push({
- year,
- percentage,
- price: (endPrice + startPrice) / 2
- });
- }
- }
- annualPerformancesPerAsset.set(asset.id, temp_assetAnnualPerformances);
- }
-
-
- // Calculate portfolio performance for each year
- const now = new Date();
- const startYear = earliestDate ? earliestDate.getFullYear() : now.getFullYear();
- const endYear = now.getFullYear();
-
- for (let year = startYear; year <= endYear; year++) {
- const yearStart = new Date(year, 0, 1); // 1. Januar
- const yearEnd = year === endYear ? new Date(year, now.getMonth(), now.getDate()) : new Date(year, 11, 31); // Aktuelles Datum oder 31. Dez.
-
- const yearInvestments: { percent: number; weight: number }[] = [];
-
- for (const asset of assets) {
- // Get prices for the start and end of the year
- let startPrice = 0;
- let endPrice = 0;
- let startDate = yearStart;
- let endDate = yearEnd;
- while(!startPrice || !endPrice) {
- startDate = addDays(startDate, 1);
- endDate = addDays(endDate, -1);
- endPrice = endPrice || asset.historicalData.get(formatDateToISO(endDate)) || 0;
- startPrice = startPrice || asset.historicalData.get(formatDateToISO(startDate)) || 0;
- if(endDate.getTime() < yearStart.getTime() || startDate.getTime() > yearEnd.getTime()) {
- break;
- }
- }
-
- if (startPrice === 0 || endPrice === 0) {
- console.warn(`Skipping asset for year ${year} due to missing start or end price`);
- continue;
- }
-
-
- // Get all investments made before or during this year
- const relevantInvestments = asset.investments.filter(inv =>
- new Date(inv.date!) <= yearEnd && new Date(inv.date!) >= yearStart
- );
-
- for (const investment of relevantInvestments) {
- const invDate = new Date(investment.date!);
-
- let buyInPrice = asset.historicalData.get(formatDateToISO(invDate)) || 0;
-
- // try to find the next closest price prior previousdates
- if(!buyInPrice) {
- let previousDate = invDate;
- let afterDate = invDate;
- while(!buyInPrice) {
- previousDate = addDays(previousDate, -1);
- afterDate = addDays(afterDate, 1);
- buyInPrice = asset.historicalData.get(formatDateToISO(previousDate)) || asset.historicalData.get(formatDateToISO(afterDate)) || 0;
- }
- }
-
- if (buyInPrice > 0) {
- const shares = investment.amount / buyInPrice;
- const endValue = shares * endPrice;
- const startValue = shares * startPrice;
- yearInvestments.push({
- percent: ((endValue - startValue) / startValue) * 100,
- weight: startValue
- });
- }
- }
- }
-
-
- // Calculate weighted average performance for the year
- if (yearInvestments.length > 0) {
- const totalWeight = yearInvestments.reduce((sum, inv) => sum + inv.weight, 0);
- const percentage = yearInvestments.reduce((sum, inv) =>
- sum + (inv.percent * (inv.weight / totalWeight)), 0);
-
- if (!isNaN(percentage)) {
- annualPerformances.push({ year, percentage });
- } else {
- console.warn(`Invalid percentage calculated for year ${year}`);
- }
- } else {
- console.warn(`Skipping year ${year} due to zero portfolio values`);
- }
- }
-
- // Get best and worst years for the entire portfolio
- const bestPerformancePerAnno = annualPerformances.length > 0
- ? Array.from(annualPerformances).sort((a, b) => b.percentage - a.percentage)
- : [];
-
- const worstPerformancePerAnno = Array.from(bestPerformancePerAnno).reverse()
-
- // Normale Performance-Berechnungen...
- for (const asset of assets) {
- const historicalVals = Array.from(asset.historicalData.values());
- const currentPrice = historicalVals[historicalVals.length - 1] || 0;
-
- for (const investment of asset.investments) {
- const invDate = new Date(investment.date!);
- let buyInPrice = asset.historicalData.get(formatDateToISO(invDate)) || 0;
- if(!buyInPrice) {
- let previousDate = invDate;
- let afterDate = invDate;
- while(!buyInPrice) {
- previousDate = addDays(previousDate, -1);
- afterDate = addDays(afterDate, 1);
- buyInPrice = asset.historicalData.get(formatDateToISO(previousDate)) || asset.historicalData.get(formatDateToISO(afterDate)) || 0;
- }
- }
-
- const shares = buyInPrice > 0 ? investment.amount / buyInPrice : 0;
- const currentValue = shares * currentPrice;
-
- investments.push({
- id: investment.id,
- assetName: asset.name,
- date: investment.date!,
- investedAmount: investment.amount,
- investedAtPrice: buyInPrice,
- periodicGroupId: investment.periodicGroupId,
- currentValue,
- performancePercentage: investment.amount > 0
- ? (((currentValue - investment.amount) / investment.amount)) * 100
- : 0,
- });
-
- totalInvested += investment.amount;
- totalCurrentValue += currentValue;
- }
- }
-
- const ttworPercentage = totalInvested > 0
- ? ((ttworValue - totalInvested) / totalInvested) * 100
- : 0;
-
-
- const performancePerAnnoPerformance = annualPerformances.reduce((acc, curr) => acc + curr.percentage, 0) / annualPerformances.length;
-
- return {
- investments,
- summary: {
- totalInvested,
- currentValue: totalCurrentValue,
- annualPerformancesPerAsset,
- performancePercentage: totalInvested > 0
- ? ((totalCurrentValue - totalInvested) / totalInvested) * 100
- : 0,
- performancePerAnnoPerformance,
- ttworValue,
- ttworPercentage,
- worstPerformancePerAnno: worstPerformancePerAnno,
- bestPerformancePerAnno: bestPerformancePerAnno,
- annualPerformances: annualPerformances
- },
- };
-};
+import { addDays, isBefore } from "date-fns";
+
+import { formatDateToISO } from "../formatters";
+
+import type { Asset, InvestmentPerformance, PortfolioPerformance } from "../../types";
+export const calculateInvestmentPerformance = (assets: Asset[]): PortfolioPerformance => {
+ const investments: InvestmentPerformance[] = [];
+ let totalInvested = 0;
+ let totalCurrentValue = 0;
+ let earliestDate: Date | null = null;
+
+ // TTWOR Berechnung
+ const firstDayPrices: Record = {};
+ const currentPrices: Record = {};
+ const investedPerAsset: Record = {};
+
+ // Sammle erste und letzte Preise fΓΌr jedes Asset
+ for (const asset of assets) {
+ const keys = Array.from(asset.historicalData.values());
+ const firstDay = keys[0];
+ const lastDay = keys[keys.length - 1];
+ firstDayPrices[asset.id] = firstDay;
+ currentPrices[asset.id] = lastDay;
+ investedPerAsset[asset.id] = asset.investments.reduce((sum, inv) => sum + inv.amount, 0);
+ }
+
+ // Berechne TTWOR
+ const ttworValue = Object.entries(investedPerAsset).reduce((acc, [assetId, invested]) => {
+ if (firstDayPrices[assetId] && currentPrices[assetId] && firstDayPrices[assetId] > 0) {
+ const shares = invested / firstDayPrices[assetId];
+ return acc + (shares * currentPrices[assetId]);
+ }
+ return acc;
+ }, 0);
+
+ // Calculate portfolio-level annual performances
+ const annualPerformances: { year: number; percentage: number }[] = [];
+ const annualPerformancesPerAsset = new Map();
+
+ // Finde das frΓΌheste Investmentdatum
+ for (const asset of assets) {
+ for (const investment of asset.investments) {
+ const investmentDate = new Date(investment.date!);
+ if (!earliestDate || isBefore(investmentDate, earliestDate)) {
+ earliestDate = investmentDate;
+ }
+ }
+ const historicalData = Array.from(asset.historicalData.entries());
+ const firstDate = new Date(historicalData[0][0]);
+ const temp_assetAnnualPerformances: { year: number; percentage: number; price: number }[] = [];
+ for (let year = firstDate.getFullYear(); year <= new Date().getFullYear(); year++) {
+ const yearStart = new Date(year, 0, 1);
+ const yearEnd = new Date(year, 11, 31);
+ let startDate = yearStart;
+ let endDate = yearEnd;
+ let startPrice = asset.historicalData.get(formatDateToISO(startDate));
+ let endPrice = asset.historicalData.get(formatDateToISO(endDate));
+ while(!startPrice || !endPrice) {
+ startDate = addDays(startDate, 1);
+ endDate = addDays(endDate, -1);
+ endPrice = endPrice || asset.historicalData.get(formatDateToISO(endDate)) || 0;
+ startPrice = startPrice || asset.historicalData.get(formatDateToISO(startDate)) || 0;
+ if(endDate.getTime() < yearStart.getTime() || startDate.getTime() > yearEnd.getTime()) {
+ break;
+ }
+ }
+ if (startPrice && endPrice) {
+ const percentage = ((endPrice - startPrice) / startPrice) * 100;
+ temp_assetAnnualPerformances.push({
+ year,
+ percentage,
+ price: (endPrice + startPrice) / 2
+ });
+ }
+ }
+ annualPerformancesPerAsset.set(asset.id, temp_assetAnnualPerformances);
+ }
+
+
+ // Calculate portfolio performance for each year
+ const now = new Date();
+ const startYear = earliestDate ? earliestDate.getFullYear() : now.getFullYear();
+ const endYear = now.getFullYear();
+
+ for (let year = startYear; year <= endYear; year++) {
+ const yearStart = new Date(year, 0, 1); // 1. Januar
+ const yearEnd = year === endYear ? new Date(year, now.getMonth(), now.getDate()) : new Date(year, 11, 31); // Aktuelles Datum oder 31. Dez.
+
+ const yearInvestments: { percent: number; weight: number }[] = [];
+
+ for (const asset of assets) {
+ // Get prices for the start and end of the year
+ let startPrice = 0;
+ let endPrice = 0;
+ let startDate = yearStart;
+ let endDate = yearEnd;
+ while(!startPrice || !endPrice) {
+ startDate = addDays(startDate, 1);
+ endDate = addDays(endDate, -1);
+ endPrice = endPrice || asset.historicalData.get(formatDateToISO(endDate)) || 0;
+ startPrice = startPrice || asset.historicalData.get(formatDateToISO(startDate)) || 0;
+ if(endDate.getTime() < yearStart.getTime() || startDate.getTime() > yearEnd.getTime()) {
+ break;
+ }
+ }
+
+ if (startPrice === 0 || endPrice === 0) {
+ console.warn(`Skipping asset for year ${year} due to missing start or end price`);
+ continue;
+ }
+
+
+ // Get all investments made before or during this year
+ const relevantInvestments = asset.investments.filter(inv =>
+ new Date(inv.date!) <= yearEnd && new Date(inv.date!) >= yearStart
+ );
+
+ for (const investment of relevantInvestments) {
+ const invDate = new Date(investment.date!);
+
+ let buyInPrice = asset.historicalData.get(formatDateToISO(invDate)) || 0;
+
+ // try to find the next closest price prior previousdates
+ if(!buyInPrice) {
+ let previousDate = invDate;
+ let afterDate = invDate;
+ while(!buyInPrice) {
+ previousDate = addDays(previousDate, -1);
+ afterDate = addDays(afterDate, 1);
+ buyInPrice = asset.historicalData.get(formatDateToISO(previousDate)) || asset.historicalData.get(formatDateToISO(afterDate)) || 0;
+ }
+ }
+
+ if (buyInPrice > 0) {
+ const shares = investment.amount / buyInPrice;
+ const endValue = shares * endPrice;
+ const startValue = shares * startPrice;
+ yearInvestments.push({
+ percent: ((endValue - startValue) / startValue) * 100,
+ weight: startValue
+ });
+ }
+ }
+ }
+
+
+ // Calculate weighted average performance for the year
+ if (yearInvestments.length > 0) {
+ const totalWeight = yearInvestments.reduce((sum, inv) => sum + inv.weight, 0);
+ const percentage = yearInvestments.reduce((sum, inv) =>
+ sum + (inv.percent * (inv.weight / totalWeight)), 0);
+
+ if (!isNaN(percentage)) {
+ annualPerformances.push({ year, percentage });
+ } else {
+ console.warn(`Invalid percentage calculated for year ${year}`);
+ }
+ } else {
+ console.warn(`Skipping year ${year} due to zero portfolio values`);
+ }
+ }
+
+ // Get best and worst years for the entire portfolio
+ const bestPerformancePerAnno = annualPerformances.length > 0
+ ? Array.from(annualPerformances).sort((a, b) => b.percentage - a.percentage)
+ : [];
+
+ const worstPerformancePerAnno = Array.from(bestPerformancePerAnno).reverse()
+
+ // Normale Performance-Berechnungen...
+ for (const asset of assets) {
+ const historicalVals = Array.from(asset.historicalData.values());
+ const currentPrice = historicalVals[historicalVals.length - 1] || 0;
+
+ for (const investment of asset.investments) {
+ const invDate = new Date(investment.date!);
+ let buyInPrice = asset.historicalData.get(formatDateToISO(invDate)) || 0;
+ if(!buyInPrice) {
+ let previousDate = invDate;
+ let afterDate = invDate;
+ while(!buyInPrice) {
+ previousDate = addDays(previousDate, -1);
+ afterDate = addDays(afterDate, 1);
+ buyInPrice = asset.historicalData.get(formatDateToISO(previousDate)) || asset.historicalData.get(formatDateToISO(afterDate)) || 0;
+ }
+ }
+
+ const shares = buyInPrice > 0 ? investment.amount / buyInPrice : 0;
+ const currentValue = shares * currentPrice;
+
+ investments.push({
+ id: investment.id,
+ assetName: asset.name,
+ date: investment.date!,
+ investedAmount: investment.amount,
+ investedAtPrice: buyInPrice,
+ periodicGroupId: investment.periodicGroupId,
+ currentValue,
+ performancePercentage: investment.amount > 0
+ ? (((currentValue - investment.amount) / investment.amount)) * 100
+ : 0,
+ });
+
+ totalInvested += investment.amount;
+ totalCurrentValue += currentValue;
+ }
+ }
+
+ const ttworPercentage = totalInvested > 0
+ ? ((ttworValue - totalInvested) / totalInvested) * 100
+ : 0;
+
+
+ const performancePerAnnoPerformance = annualPerformances.reduce((acc, curr) => acc + curr.percentage, 0) / annualPerformances.length;
+
+ return {
+ investments,
+ summary: {
+ totalInvested,
+ currentValue: totalCurrentValue,
+ annualPerformancesPerAsset,
+ performancePercentage: totalInvested > 0
+ ? ((totalCurrentValue - totalInvested) / totalInvested) * 100
+ : 0,
+ performancePerAnnoPerformance,
+ ttworValue,
+ ttworPercentage,
+ worstPerformancePerAnno: worstPerformancePerAnno,
+ bestPerformancePerAnno: bestPerformancePerAnno,
+ annualPerformances: annualPerformances
+ },
+ };
+};
diff --git a/src/utils/calculations/portfolioValue.ts b/src/utils/calculations/portfolioValue.ts
index 5904e57..6291363 100644
--- a/src/utils/calculations/portfolioValue.ts
+++ b/src/utils/calculations/portfolioValue.ts
@@ -1,96 +1,96 @@
-import { addDays, isAfter, isBefore, isSameDay } from "date-fns";
-
-import { formatDateToISO } from "../formatters";
-import { calculateAssetValueAtDate } from "./assetValue";
-
-import type { Asset, DateRange, DayData } from "../../types";
-interface WeightedPercent {
- percent: number;
- weight: number;
-}
-
-
-export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) => {
- const { startDate, endDate } = dateRange;
- const data: DayData[] = [];
-
- let currentDate = startDate;
- const end = endDate;
-
- const beforeValue: { [assetId: string]: number } = {};
-
- while (isBefore(currentDate, end)) {
- const dayData: DayData = {
- date: currentDate,
- total: 0,
- invested: 0,
- percentageChange: 0,
- assets: {},
- };
-
- const weightedPercents: WeightedPercent[] = [];
-
- for (const asset of assets) {
- // calculate the invested kapital
- for (const investment of asset.investments) {
- const invDate = new Date(investment.date!);
- if (!isAfter(invDate, currentDate) && !isSameDay(invDate, currentDate)) dayData.invested += investment.amount;
- }
-
- const currentValueOfAsset = asset.historicalData.get(formatDateToISO(dayData.date)) || beforeValue[asset.id];
- beforeValue[asset.id] = currentValueOfAsset;
-
- if (currentValueOfAsset !== undefined) {
- const { investedValue, avgBuyIn } = calculateAssetValueAtDate(
- asset,
- currentDate,
- currentValueOfAsset
- );
-
- dayData.total += investedValue || 0;
- dayData.assets[asset.id] = currentValueOfAsset;
-
- let performancePercentage = 0;
- if (investedValue > 0) {
- performancePercentage = ((currentValueOfAsset - avgBuyIn) / avgBuyIn) * 100;
- }
-
- weightedPercents.push({
- percent: performancePercentage,
- weight: investedValue
- });
- }
- }
-
- // Calculate weighted average percentage change
- if (weightedPercents.length > 0) {
- const totalWeight = weightedPercents.reduce((sum, wp) => sum + wp.weight, 0);
- dayData.percentageChange = weightedPercents.reduce((sum, wp) =>
- sum + (wp.percent * (wp.weight / totalWeight)), 0);
- } else {
- dayData.percentageChange = 0;
- }
-
- const totalInvested = dayData.invested; // Total invested amount for the day
- const totalCurrentValue = dayData.total; // Total current value for the day
-
- if (totalInvested > 0 && totalCurrentValue > 0) {
- dayData.percentageChange = ((totalCurrentValue - totalInvested) / totalInvested) * 100;
- } else {
- dayData.percentageChange = 0;
- }
-
-
- currentDate = addDays(currentDate, 1);
- data.push(dayData);
- }
-
- // Filter out days with incomplete asset data
- return data.filter(
- (dayData) => {
- const vals = Object.values(dayData.assets);
- if (!vals.length) return false;
- return !vals.some((value) => value === 0);
- }
- );
-};
+import { addDays, isAfter, isBefore, isSameDay } from "date-fns";
+
+import { formatDateToISO } from "../formatters";
+import { calculateAssetValueAtDate } from "./assetValue";
+
+import type { Asset, DateRange, DayData } from "../../types";
+interface WeightedPercent {
+ percent: number;
+ weight: number;
+}
+
+
+export const calculatePortfolioValue = (assets: Asset[], dateRange: DateRange) => {
+ const { startDate, endDate } = dateRange;
+ const data: DayData[] = [];
+
+ let currentDate = startDate;
+ const end = endDate;
+
+ const beforeValue: { [assetId: string]: number } = {};
+
+ while (isBefore(currentDate, end)) {
+ const dayData: DayData = {
+ date: currentDate,
+ total: 0,
+ invested: 0,
+ percentageChange: 0,
+ assets: {},
+ };
+
+ const weightedPercents: WeightedPercent[] = [];
+
+ for (const asset of assets) {
+ // calculate the invested kapital
+ for (const investment of asset.investments) {
+ const invDate = new Date(investment.date!);
+ if (!isAfter(invDate, currentDate) && !isSameDay(invDate, currentDate)) dayData.invested += investment.amount;
+ }
+
+ const currentValueOfAsset = asset.historicalData.get(formatDateToISO(dayData.date)) || beforeValue[asset.id];
+ beforeValue[asset.id] = currentValueOfAsset;
+
+ if (currentValueOfAsset !== undefined) {
+ const { investedValue, avgBuyIn } = calculateAssetValueAtDate(
+ asset,
+ currentDate,
+ currentValueOfAsset
+ );
+
+ dayData.total += investedValue || 0;
+ dayData.assets[asset.id] = currentValueOfAsset;
+
+ let performancePercentage = 0;
+ if (investedValue > 0) {
+ performancePercentage = ((currentValueOfAsset - avgBuyIn) / avgBuyIn) * 100;
+ }
+
+ weightedPercents.push({
+ percent: performancePercentage,
+ weight: investedValue
+ });
+ }
+ }
+
+ // Calculate weighted average percentage change
+ if (weightedPercents.length > 0) {
+ const totalWeight = weightedPercents.reduce((sum, wp) => sum + wp.weight, 0);
+ dayData.percentageChange = weightedPercents.reduce((sum, wp) =>
+ sum + (wp.percent * (wp.weight / totalWeight)), 0);
+ } else {
+ dayData.percentageChange = 0;
+ }
+
+ const totalInvested = dayData.invested; // Total invested amount for the day
+ const totalCurrentValue = dayData.total; // Total current value for the day
+
+ if (totalInvested > 0 && totalCurrentValue > 0) {
+ dayData.percentageChange = ((totalCurrentValue - totalInvested) / totalInvested) * 100;
+ } else {
+ dayData.percentageChange = 0;
+ }
+
+
+ currentDate = addDays(currentDate, 1);
+ data.push(dayData);
+ }
+
+ // Filter out days with incomplete asset data
+ return data.filter(
+ (dayData) => {
+ const vals = Object.values(dayData.assets);
+ // Keep days where at least one asset has data
+ return vals.length > 0 && vals.some(value => value > 0);
+ }
+ );
+};
diff --git a/src/utils/export.ts b/src/utils/export.ts
index ef1b113..5d742c9 100644
--- a/src/utils/export.ts
+++ b/src/utils/export.ts
@@ -1,339 +1,339 @@
-import "jspdf-autotable";
-
-import { isBefore, isSameDay } from "date-fns";
-import { jsPDF } from "jspdf";
-
-import { Asset, PortfolioPerformance } from "../types";
-import { calculateFutureProjection } from "./calculations/futureProjection";
-
-// Add type augmentation for the autotable plugin
-interface jsPDFWithPlugin extends jsPDF {
- autoTable: (options: any) => void;
-}
-
-const formatEuro = (value: number) => {
- return `β¬${value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ".").replace(".", ",").replace(/,(\d{3})/g, ".$1")}`;
-};
-
-export const downloadTableAsCSV = (tableData: any[], filename: string) => {
- const headers = Object.keys(tableData[0])
- .filter(header => !header.toLowerCase().includes('id'));
-
- const csvContent = [
- headers.map(title => title.charAt(0).toUpperCase() + title.slice(1)).join(','),
- ...tableData.map(row =>
- headers.map(header => {
- const value = row[header]?.toString().replace(/,/g, '');
- return isNaN(Number(value))
- ? `"${value}"`
- : formatEuro(Number(value)).replace('β¬', '');
- }).join(',')
- )
- ].join('\n');
-
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
- const link = document.createElement('a');
- link.href = URL.createObjectURL(blob);
- link.download = `${filename}.csv`;
- link.click();
-};
-
-export const generatePortfolioPDF = async (
- assets: Asset[],
- performance: PortfolioPerformance,
- savingsPlansPerformance: any[],
- performancePerAnno: number
-) => {
- const doc = new jsPDF() as jsPDFWithPlugin;
- doc.setFont('Arial', 'normal');
- let yPos = 20;
-
- // Title
- doc.setFontSize(20);
- doc.text('Portfolio Analysis Report', 15, yPos);
- yPos += 15;
-
- // Explanations
- doc.setFontSize(12);
- doc.setTextColor(100);
-
- // TTWOR Explanation
- doc.text('Understanding TTWOR (Time Travel Without Risk):', 15, yPos);
- yPos += 7;
- const ttworText =
- 'TTWOR shows how your portfolio would have performed if all investments had been made at ' +
- 'the beginning of the period. This metric helps evaluate the impact of your investment ' +
- 'timing strategy compared to a single early investment. A higher portfolio performance ' +
- 'than TTWOR indicates successful timing of investments.';
-
- const ttworLines = doc.splitTextToSize(ttworText, 180);
- doc.text(ttworLines, 20, yPos);
- yPos += ttworLines.length * 7;
-
- doc.setTextColor(0);
-
- // Portfolio Summary
- doc.setFontSize(16);
- doc.text('Portfolio Summary', 15, yPos);
- yPos += 10;
-
- doc.setFontSize(12);
- doc.text(`Total Invested: ${formatEuro(performance.summary.totalInvested)}`, 20, yPos);
- yPos += 7;
- doc.text(`Current Value: ${formatEuro(performance.summary.currentValue)}`, 20, yPos);
- yPos += 7;
- doc.text(`Performance: ${performance.summary.performancePercentage.toFixed(2)}% (p.a. ${performance.summary.performancePerAnnoPerformance.toFixed(2)}%)`, 20, yPos);
- yPos += 7;
-
- // TTWOR values in italic
- doc.setFont('Arial', 'italic');
- doc.text(`TTWOR* Value: ${formatEuro(performance.summary.ttworValue)} (would perform: ${performance.summary.ttworPercentage.toFixed(2)}%)`, 20, yPos);
- doc.setFont('Arial', 'normal');
- yPos += 15;
-
- // Add Positions Overview table
- doc.setFontSize(16);
- doc.text('Positions Overview', 15, yPos);
- yPos += 10;
-
- // Prepare positions data
- const positionsTableData = [
- // Summary row
- [
- 'Total Portfolio',
- '',
- '',
- formatEuro(performance.summary.totalInvested),
- formatEuro(performance.summary.currentValue),
- '',
- `${performance.summary.performancePercentage.toFixed(2)}% (p.a. ${performance.summary.performancePerAnnoPerformance.toFixed(2)}%)`,
- ],
- // TTWOR row
- [
- 'TTWOR*',
- '',
- performance.investments[0]?.date
- ? new Date(performance.investments[0].date).toLocaleDateString('de-DE')
- : '',
- formatEuro(performance.summary.totalInvested),
- formatEuro(performance.summary.ttworValue),
- '',
- `${performance.summary.ttworPercentage.toFixed(2)}%`,
- ],
- // Individual positions
- ...performance.investments.sort((a, b) => (isBefore(a.date, b.date) || isSameDay(a.date, b.date)) ? -1 : 1).map((inv) => {
- const asset = assets.find(a => a.name === inv.assetName)!;
- const investment = asset.investments.find(i => i.id === inv.id)! || inv;
- const filtered = performance.investments.filter((v: any) => v.assetName === inv.assetName);
- const avgBuyIn = filtered.reduce((acc: any, curr: any) => acc + curr.investedAtPrice, 0) / filtered.length;
-
- return [
- inv.assetName,
- investment.type === 'periodic' ? 'SavingsPlan' : 'OneTime',
- new Date(inv.date).toLocaleDateString('de-DE'),
- formatEuro(inv.investedAmount),
- formatEuro(inv.currentValue),
- `${formatEuro(inv.investedAtPrice)} (${formatEuro(avgBuyIn)})`,
- `${inv.performancePercentage.toFixed(2)}%`,
- ];
- }),
- ];
-
- doc.autoTable({
- startY: yPos,
- head: [['Asset', 'Type', 'Date', 'Invested Amount', 'Current Value', 'Buy-In (avg)', 'Performance']],
- body: positionsTableData,
- styles: {
- cellPadding: 2,
- fontSize: 8,
- },
- headStyles: {
- fillColor: [240, 240, 240],
- textColor: [0, 0, 0],
- fontStyle: 'bold',
- },
- // Style for summary and TTWOR rows
- rowStyles: (row:number) => {
- if (row === 0) return { fontStyle: 'bold', fillColor: [245, 245, 245] };
- if (row === 1) return { fontStyle: 'italic', textColor: [100, 100, 100] };
- return {};
- },
- });
- yPos = (doc as any).lastAutoTable.finalY + 15;
-
- // Savings Plans Table if exists
- if (savingsPlansPerformance.length > 0) {
- doc.setFontSize(16);
- doc.text('Savings Plans Performance', 15, yPos);
- yPos += 10;
-
- const savingsPlansTableData = savingsPlansPerformance.map(plan => [
- plan.assetName,
- formatEuro(plan.amount),
- formatEuro(plan.totalInvested),
- formatEuro(plan.currentValue),
- `${plan.performancePercentage.toFixed(2)}%`,
- `${plan.performancePerAnnoPerformance.toFixed(2)}%`
- ]);
-
- doc.autoTable({
- startY: yPos,
- head: [['Asset', 'Interval Amount', 'Total Invested', 'Current Value', 'Performance', 'Performance (p.a.)']],
- body: savingsPlansTableData,
- });
- yPos = (doc as any).lastAutoTable.finalY + 15;
- }
-
- // Add page break before future projections
- doc.addPage();
- yPos = 20;
-
- // Future Projections
- doc.setFontSize(16);
- doc.text('Future Projections', 15, yPos);
- yPos += 15;
- doc.setFontSize(12);
- doc.setTextColor(100);
- // Future Projections Explanation
- doc.text('About Future Projections:', 15, yPos);
- yPos += 7;
- const projectionText =
- 'The future projections are calculated using your portfolio\'s historical performance ' +
- `(${performancePerAnno.toFixed(2)}% p.a.) as a baseline. The chart shows different time horizons ` +
- 'to help visualize potential growth scenarios. These projections are estimates based on ' +
- 'historical data and should not be considered guaranteed returns.';
-
- doc.setTextColor(0);
- const projectionLines = doc.splitTextToSize(projectionText, 180);
- doc.text(projectionLines, 20, yPos);
- yPos += projectionLines.length * 7 - 7;
-
-
- const years = [10, 15, 20, 30, 40];
- const chartWidth = 180;
- const chartHeight = 100;
-
- // Calculate all projections first
- const allProjections = await Promise.all(years.map(async year => {
- const { projection } = await calculateFutureProjection(assets, year, performancePerAnno, {
- enabled: false,
- amount: 0,
- interval: 'monthly',
- startTrigger: 'date'
- });
- return { year, projection };
- }));
-
- // Show summary table
- const projectionSummary = allProjections.map(({ year, projection }) => {
- const projected = projection[projection.length - 1];
- return [
- `${year} Years`,
- formatEuro(projected.invested),
- formatEuro(projected.value),
- `${((projected.value - projected.invested) / projected.invested * 100).toFixed(2)}%`
- ];
- });
-
- doc.autoTable({
- startY: yPos,
- head: [['Timeframe', 'Invested Amount', 'Expected Value', '% Gain']],
- body: projectionSummary,
- });
- yPos = (doc as any).lastAutoTable.finalY + 15;
-
- // Draw combined chart
- const maxValue = Math.max(...allProjections.flatMap(p => p.projection.map(d => d.value)));
- const yAxisSteps = 5;
- const stepSize = maxValue / yAxisSteps;
- const legendHeight = 40; // Height for legend section
-
- // Draw axes
- doc.setDrawColor(200);
- doc.line(15, yPos, 15, yPos + chartHeight); // Y axis
- doc.line(15, yPos + chartHeight, 15 + chartWidth, yPos + chartHeight); // X axis
-
- // Draw Y-axis labels and grid lines
- doc.setFontSize(8);
- doc.setDrawColor(230);
- for (let i = 0; i <= yAxisSteps; i++) {
- const value = maxValue - (i * stepSize);
- const y = yPos + (i * (chartHeight / yAxisSteps));
- doc.text(formatEuro(value), 5, y + 3);
- doc.line(15, y, 15 + chartWidth, y); // Grid line
- }
-
- const colors: [number, number, number][] = [
- [0, 100, 255], // Blue
- [255, 100, 0], // Orange
- [0, 200, 100], // Green
- [200, 0, 200], // Purple
- [255, 0, 0], // Red
- ];
-
- // Draw lines for each projection
- allProjections.forEach(({ projection }, index) => {
- const points = projection.map((p, i) => [
- 15 + (i * (chartWidth / projection.length)),
- yPos + chartHeight - (p.value / maxValue * chartHeight)
- ]);
-
- doc.setDrawColor(...(colors[index]));
- doc.setLineWidth(0.5);
- points.forEach((point, i) => {
- if (i > 0) {
- doc.line(points[i - 1][0], points[i - 1][1], point[0], point[1]);
- }
- });
- });
-
- // Add date labels
- doc.setFontSize(8);
- doc.setDrawColor(0);
-
- // Draw legend at bottom
- const legendY = yPos + chartHeight + 20;
- const legendItemWidth = chartWidth / years.length;
-
- doc.setFontSize(8);
- allProjections.forEach(({ year }, index) => {
- const x = 15 + (index * legendItemWidth);
-
- // Draw color line
- doc.setDrawColor(...colors[index]);
- doc.setLineWidth(1);
- doc.line(x, legendY + 4, x + 15, legendY + 4);
-
- // Draw text
- doc.setTextColor(0);
- doc.text(`${year} Years`, x + 20, legendY + 6);
- });
-
- yPos += chartHeight + legendHeight; // Update yPos to include legend space
-
- // Add footer with link
- const footerText = 'Built by Tomato6966 - SourceCode';
- const link = 'https://github.com/Tomato6966/investment-portfolio-simulator';
-
- doc.setFontSize(10);
- doc.setTextColor(100);
- const pageHeight = doc.internal.pageSize.height;
-
- // Add to all pages
- // @ts-expect-error - doc.internal.getNumberOfPages() is not typed
- const totalPages = doc.internal.getNumberOfPages();
- for (let i = 1; i <= totalPages; i++) {
- doc.setPage(i);
-
- // Footer text with link
- doc.text(footerText, 15, pageHeight - 10);
-
- // Add link annotation
- doc.link(15, pageHeight - 15, doc.getTextWidth(footerText), 10, { url: link });
-
- // Page numbers
- doc.text(`Page ${i} of ${totalPages}`, doc.internal.pageSize.width - 30, pageHeight - 10);
- }
-
- doc.save('portfolio-analysis.pdf');
-};
+import "jspdf-autotable";
+
+import { isBefore, isSameDay } from "date-fns";
+import { jsPDF } from "jspdf";
+
+import { Asset, PortfolioPerformance } from "../types";
+import { calculateFutureProjection } from "./calculations/futureProjection";
+
+// Add type augmentation for the autotable plugin
+interface jsPDFWithPlugin extends jsPDF {
+ autoTable: (options: any) => void;
+}
+
+const formatEuro = (value: number) => {
+ return `β¬${value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ".").replace(".", ",").replace(/,(\d{3})/g, ".$1")}`;
+};
+
+export const downloadTableAsCSV = (tableData: any[], filename: string) => {
+ const headers = Object.keys(tableData[0])
+ .filter(header => !header.toLowerCase().includes('id'));
+
+ const csvContent = [
+ headers.map(title => title.charAt(0).toUpperCase() + title.slice(1)).join(','),
+ ...tableData.map(row =>
+ headers.map(header => {
+ const value = row[header]?.toString().replace(/,/g, '');
+ return isNaN(Number(value))
+ ? `"${value}"`
+ : formatEuro(Number(value)).replace('β¬', '');
+ }).join(',')
+ )
+ ].join('\n');
+
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = `${filename}.csv`;
+ link.click();
+};
+
+export const generatePortfolioPDF = async (
+ assets: Asset[],
+ performance: PortfolioPerformance,
+ savingsPlansPerformance: any[],
+ performancePerAnno: number
+) => {
+ const doc = new jsPDF() as jsPDFWithPlugin;
+ doc.setFont('Arial', 'normal');
+ let yPos = 20;
+
+ // Title
+ doc.setFontSize(20);
+ doc.text('Portfolio Analysis Report', 15, yPos);
+ yPos += 15;
+
+ // Explanations
+ doc.setFontSize(12);
+ doc.setTextColor(100);
+
+ // TTWOR Explanation
+ doc.text('Understanding TTWOR (Time Travel Without Risk):', 15, yPos);
+ yPos += 7;
+ const ttworText =
+ 'TTWOR shows how your portfolio would have performed if all investments had been made at ' +
+ 'the beginning of the period. This metric helps evaluate the impact of your investment ' +
+ 'timing strategy compared to a single early investment. A higher portfolio performance ' +
+ 'than TTWOR indicates successful timing of investments.';
+
+ const ttworLines = doc.splitTextToSize(ttworText, 180);
+ doc.text(ttworLines, 20, yPos);
+ yPos += ttworLines.length * 7;
+
+ doc.setTextColor(0);
+
+ // Portfolio Summary
+ doc.setFontSize(16);
+ doc.text('Portfolio Summary', 15, yPos);
+ yPos += 10;
+
+ doc.setFontSize(12);
+ doc.text(`Total Invested: ${formatEuro(performance.summary.totalInvested)}`, 20, yPos);
+ yPos += 7;
+ doc.text(`Current Value: ${formatEuro(performance.summary.currentValue)}`, 20, yPos);
+ yPos += 7;
+ doc.text(`Performance: ${performance.summary.performancePercentage.toFixed(2)}% (p.a. ${performance.summary.performancePerAnnoPerformance.toFixed(2)}%)`, 20, yPos);
+ yPos += 7;
+
+ // TTWOR values in italic
+ doc.setFont('Arial', 'italic');
+ doc.text(`TTWOR* Value: ${formatEuro(performance.summary.ttworValue)} (would perform: ${performance.summary.ttworPercentage.toFixed(2)}%)`, 20, yPos);
+ doc.setFont('Arial', 'normal');
+ yPos += 15;
+
+ // Add Positions Overview table
+ doc.setFontSize(16);
+ doc.text('Positions Overview', 15, yPos);
+ yPos += 10;
+
+ // Prepare positions data
+ const positionsTableData = [
+ // Summary row
+ [
+ 'Total Portfolio',
+ '',
+ '',
+ formatEuro(performance.summary.totalInvested),
+ formatEuro(performance.summary.currentValue),
+ '',
+ `${performance.summary.performancePercentage.toFixed(2)}% (p.a. ${performance.summary.performancePerAnnoPerformance.toFixed(2)}%)`,
+ ],
+ // TTWOR row
+ [
+ 'TTWOR*',
+ '',
+ performance.investments[0]?.date
+ ? new Date(performance.investments[0].date).toLocaleDateString('de-DE')
+ : '',
+ formatEuro(performance.summary.totalInvested),
+ formatEuro(performance.summary.ttworValue),
+ '',
+ `${performance.summary.ttworPercentage.toFixed(2)}%`,
+ ],
+ // Individual positions
+ ...performance.investments.sort((a, b) => (isBefore(a.date, b.date) || isSameDay(a.date, b.date)) ? -1 : 1).map((inv) => {
+ const asset = assets.find(a => a.name === inv.assetName)!;
+ const investment = asset.investments.find(i => i.id === inv.id)! || inv;
+ const filtered = performance.investments.filter((v: any) => v.assetName === inv.assetName);
+ const avgBuyIn = filtered.reduce((acc: any, curr: any) => acc + curr.investedAtPrice, 0) / filtered.length;
+
+ return [
+ inv.assetName,
+ investment.type === 'periodic' ? 'SavingsPlan' : 'OneTime',
+ new Date(inv.date).toLocaleDateString('de-DE'),
+ formatEuro(inv.investedAmount),
+ formatEuro(inv.currentValue),
+ `${formatEuro(inv.investedAtPrice)} (${formatEuro(avgBuyIn)})`,
+ `${inv.performancePercentage.toFixed(2)}%`,
+ ];
+ }),
+ ];
+
+ doc.autoTable({
+ startY: yPos,
+ head: [['Asset', 'Type', 'Date', 'Invested Amount', 'Current Value', 'Buy-In (avg)', 'Performance']],
+ body: positionsTableData,
+ styles: {
+ cellPadding: 2,
+ fontSize: 8,
+ },
+ headStyles: {
+ fillColor: [240, 240, 240],
+ textColor: [0, 0, 0],
+ fontStyle: 'bold',
+ },
+ // Style for summary and TTWOR rows
+ rowStyles: (row:number) => {
+ if (row === 0) return { fontStyle: 'bold', fillColor: [245, 245, 245] };
+ if (row === 1) return { fontStyle: 'italic', textColor: [100, 100, 100] };
+ return {};
+ },
+ });
+ yPos = (doc as any).lastAutoTable.finalY + 15;
+
+ // Savings Plans Table if exists
+ if (savingsPlansPerformance.length > 0) {
+ doc.setFontSize(16);
+ doc.text('Savings Plans Performance', 15, yPos);
+ yPos += 10;
+
+ const savingsPlansTableData = savingsPlansPerformance.map(plan => [
+ plan.assetName,
+ formatEuro(plan.amount),
+ formatEuro(plan.totalInvested),
+ formatEuro(plan.currentValue),
+ `${plan.performancePercentage.toFixed(2)}%`,
+ `${plan.performancePerAnnoPerformance.toFixed(2)}%`
+ ]);
+
+ doc.autoTable({
+ startY: yPos,
+ head: [['Asset', 'Interval Amount', 'Total Invested', 'Current Value', 'Performance', 'Performance (p.a.)']],
+ body: savingsPlansTableData,
+ });
+ yPos = (doc as any).lastAutoTable.finalY + 15;
+ }
+
+ // Add page break before future projections
+ doc.addPage();
+ yPos = 20;
+
+ // Future Projections
+ doc.setFontSize(16);
+ doc.text('Future Projections', 15, yPos);
+ yPos += 15;
+ doc.setFontSize(12);
+ doc.setTextColor(100);
+ // Future Projections Explanation
+ doc.text('About Future Projections:', 15, yPos);
+ yPos += 7;
+ const projectionText =
+ 'The future projections are calculated using your portfolio\'s historical performance ' +
+ `(${performancePerAnno.toFixed(2)}% p.a.) as a baseline. The chart shows different time horizons ` +
+ 'to help visualize potential growth scenarios. These projections are estimates based on ' +
+ 'historical data and should not be considered guaranteed returns.';
+
+ doc.setTextColor(0);
+ const projectionLines = doc.splitTextToSize(projectionText, 180);
+ doc.text(projectionLines, 20, yPos);
+ yPos += projectionLines.length * 7 - 7;
+
+
+ const years = [10, 15, 20, 30, 40];
+ const chartWidth = 180;
+ const chartHeight = 100;
+
+ // Calculate all projections first
+ const allProjections = await Promise.all(years.map(async year => {
+ const { projection } = await calculateFutureProjection(assets, year, performancePerAnno, {
+ enabled: false,
+ amount: 0,
+ interval: 'monthly',
+ startTrigger: 'date'
+ });
+ return { year, projection };
+ }));
+
+ // Show summary table
+ const projectionSummary = allProjections.map(({ year, projection }) => {
+ const projected = projection[projection.length - 1];
+ return [
+ `${year} Years`,
+ formatEuro(projected.invested),
+ formatEuro(projected.value),
+ `${((projected.value - projected.invested) / projected.invested * 100).toFixed(2)}%`
+ ];
+ });
+
+ doc.autoTable({
+ startY: yPos,
+ head: [['Timeframe', 'Invested Amount', 'Expected Value', '% Gain']],
+ body: projectionSummary,
+ });
+ yPos = (doc as any).lastAutoTable.finalY + 15;
+
+ // Draw combined chart
+ const maxValue = Math.max(...allProjections.flatMap(p => p.projection.map(d => d.value)));
+ const yAxisSteps = 5;
+ const stepSize = maxValue / yAxisSteps;
+ const legendHeight = 40; // Height for legend section
+
+ // Draw axes
+ doc.setDrawColor(200);
+ doc.line(15, yPos, 15, yPos + chartHeight); // Y axis
+ doc.line(15, yPos + chartHeight, 15 + chartWidth, yPos + chartHeight); // X axis
+
+ // Draw Y-axis labels and grid lines
+ doc.setFontSize(8);
+ doc.setDrawColor(230);
+ for (let i = 0; i <= yAxisSteps; i++) {
+ const value = maxValue - (i * stepSize);
+ const y = yPos + (i * (chartHeight / yAxisSteps));
+ doc.text(formatEuro(value), 5, y + 3);
+ doc.line(15, y, 15 + chartWidth, y); // Grid line
+ }
+
+ const colors: [number, number, number][] = [
+ [0, 100, 255], // Blue
+ [255, 100, 0], // Orange
+ [0, 200, 100], // Green
+ [200, 0, 200], // Purple
+ [255, 0, 0], // Red
+ ];
+
+ // Draw lines for each projection
+ allProjections.forEach(({ projection }, index) => {
+ const points = projection.map((p, i) => [
+ 15 + (i * (chartWidth / projection.length)),
+ yPos + chartHeight - (p.value / maxValue * chartHeight)
+ ]);
+
+ doc.setDrawColor(...(colors[index]));
+ doc.setLineWidth(0.5);
+ points.forEach((point, i) => {
+ if (i > 0) {
+ doc.line(points[i - 1][0], points[i - 1][1], point[0], point[1]);
+ }
+ });
+ });
+
+ // Add date labels
+ doc.setFontSize(8);
+ doc.setDrawColor(0);
+
+ // Draw legend at bottom
+ const legendY = yPos + chartHeight + 20;
+ const legendItemWidth = chartWidth / years.length;
+
+ doc.setFontSize(8);
+ allProjections.forEach(({ year }, index) => {
+ const x = 15 + (index * legendItemWidth);
+
+ // Draw color line
+ doc.setDrawColor(...colors[index]);
+ doc.setLineWidth(1);
+ doc.line(x, legendY + 4, x + 15, legendY + 4);
+
+ // Draw text
+ doc.setTextColor(0);
+ doc.text(`${year} Years`, x + 20, legendY + 6);
+ });
+
+ yPos += chartHeight + legendHeight; // Update yPos to include legend space
+
+ // Add footer with link
+ const footerText = 'Built by Tomato6966 - SourceCode';
+ const link = 'https://github.com/Tomato6966/investment-portfolio-simulator';
+
+ doc.setFontSize(10);
+ doc.setTextColor(100);
+ const pageHeight = doc.internal.pageSize.height;
+
+ // Add to all pages
+ // @ts-expect-error - doc.internal.getNumberOfPages() is not typed
+ const totalPages = doc.internal.getNumberOfPages();
+ for (let i = 1; i <= totalPages; i++) {
+ doc.setPage(i);
+
+ // Footer text with link
+ doc.text(footerText, 15, pageHeight - 10);
+
+ // Add link annotation
+ doc.link(15, pageHeight - 15, doc.getTextWidth(footerText), 10, { url: link });
+
+ // Page numbers
+ doc.text(`Page ${i} of ${totalPages}`, doc.internal.pageSize.width - 30, pageHeight - 10);
+ }
+
+ doc.save('portfolio-analysis.pdf');
+};
diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts
index 01ed4b1..4880e92 100644
--- a/src/utils/formatters.ts
+++ b/src/utils/formatters.ts
@@ -1,41 +1,41 @@
-import { formatDate, isValid, parseISO } from "date-fns";
-
-export const formatCurrency = (value: number): string => {
- return `β¬${value.toLocaleString('de-DE', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2
- })}`;
-};
-
-const LIGHT_MODE_COLORS = [
- '#2563eb', '#dc2626', '#059669', '#7c3aed', '#ea580c',
- '#0891b2', '#be123c', '#1d4ed8', '#b91c1c', '#047857',
- '#6d28d9', '#c2410c', '#0e7490', '#9f1239', '#1e40af',
- '#991b1b', '#065f46', '#5b21b6', '#9a3412', '#155e75',
- '#881337', '#1e3a8a', '#7f1d1d', '#064e3b', '#4c1d95'
-];
-
-const DARK_MODE_COLORS = [
- '#60a5fa', '#f87171', '#34d399', '#a78bfa', '#fb923c',
- '#22d3ee', '#fb7185', '#3b82f6', '#ef4444', '#10b981',
- '#8b5cf6', '#f97316', '#06b6d4', '#f43f5e', '#2563eb',
- '#dc2626', '#059669', '#7c3aed', '#ea580c', '#0891b2',
- '#be123c', '#1d4ed8', '#b91c1c', '#047857', '#6d28d9'
-];
-
-export const getHexColor = (usedColors: Set, isDarkMode: boolean): string => {
- const colorPool = isDarkMode ? DARK_MODE_COLORS : LIGHT_MODE_COLORS;
-
- // Find first unused color
- const availableColor = colorPool.find(color => !usedColors.has(color));
-
- if (availableColor) {
- return availableColor;
- }
-
- // Fallback to random color if all predefined colors are used
- return `#${Math.floor(Math.random() * 16777215).toString(16)}`;
-};
-
-export const formatDateToISO = (date: Date) => formatDate(date, 'yyyy-MM-dd');
-export const isValidDate = (dateString: string) => isValid(parseISO(dateString));
+import { formatDate, isValid, parseISO } from "date-fns";
+
+export const formatCurrency = (value: number): string => {
+ return `β¬${value.toLocaleString('de-DE', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ })}`;
+};
+
+const LIGHT_MODE_COLORS = [
+ '#2563eb', '#dc2626', '#059669', '#7c3aed', '#ea580c',
+ '#0891b2', '#be123c', '#1d4ed8', '#b91c1c', '#047857',
+ '#6d28d9', '#c2410c', '#0e7490', '#9f1239', '#1e40af',
+ '#991b1b', '#065f46', '#5b21b6', '#9a3412', '#155e75',
+ '#881337', '#1e3a8a', '#7f1d1d', '#064e3b', '#4c1d95'
+];
+
+const DARK_MODE_COLORS = [
+ '#60a5fa', '#f87171', '#34d399', '#a78bfa', '#fb923c',
+ '#22d3ee', '#fb7185', '#3b82f6', '#ef4444', '#10b981',
+ '#8b5cf6', '#f97316', '#06b6d4', '#f43f5e', '#2563eb',
+ '#dc2626', '#059669', '#7c3aed', '#ea580c', '#0891b2',
+ '#be123c', '#1d4ed8', '#b91c1c', '#047857', '#6d28d9'
+];
+
+export const getHexColor = (usedColors: Set, isDarkMode: boolean): string => {
+ const colorPool = isDarkMode ? DARK_MODE_COLORS : LIGHT_MODE_COLORS;
+
+ // Find first unused color
+ const availableColor = colorPool.find(color => !usedColors.has(color));
+
+ if (availableColor) {
+ return availableColor;
+ }
+
+ // Fallback to random color if all predefined colors are used
+ return `#${Math.floor(Math.random() * 16777215).toString(16)}`;
+};
+
+export const formatDateToISO = (date: Date) => formatDate(date, 'yyyy-MM-dd');
+export const isValidDate = (dateString: string) => isValid(parseISO(dateString));
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 11f02fe..7d0ff9e 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1 +1 @@
-///
+///
diff --git a/tailwind.config.js b/tailwind.config.js
index d95d577..c72449f 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,10 +1,10 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
- darkMode: 'class',
- theme: {
- extend: {},
- },
- plugins: [
- ],
-};
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+ darkMode: 'class',
+ theme: {
+ extend: {},
+ },
+ plugins: [
+ ],
+};
diff --git a/tsconfig.app.json b/tsconfig.app.json
index f0a2350..65d7265 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -1,24 +1,24 @@
-{
- "compilerOptions": {
- "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
- },
- "include": ["src"]
-}
+{
+ "compilerOptions": {
+ "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
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 1ffef60..38e8e3b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,7 @@
-{
- "files": [],
- "references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ]
-}
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
index 0d3d714..d791001 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -1,22 +1,22 @@
-{
- "compilerOptions": {
- "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
- },
- "include": ["vite.config.ts"]
-}
+{
+ "compilerOptions": {
+ "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
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
index 3f58647..c70cf4a 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,29 +1,29 @@
-import { defineConfig, loadEnv } from "vite";
-
-import react from "@vitejs/plugin-react";
-
-// https://vitejs.dev/config/
-export default defineConfig(({ mode }) => {
- const env = loadEnv(mode, process.cwd(), '');
- const isDev = mode === 'development';
-
- return {
- plugins: [react()],
- optimizeDeps: {
- exclude: ['lucide-react'],
- },
- server: isDev ? {
- proxy: {
- '/yahoo': {
- target: 'https://query1.finance.yahoo.com',
- changeOrigin: true,
- rewrite: (path) => path.replace(/^\/yahoo/, ''),
- headers: {
- 'Origin': 'https://finance.yahoo.com'
- }
- }
- }
- } : undefined,
- base: env.VITE_BASE_URL || '/',
- };
-});
+import { defineConfig, loadEnv } from "vite";
+
+import react from "@vitejs/plugin-react";
+
+// https://vitejs.dev/config/
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), '');
+ const isDev = mode === 'development';
+
+ return {
+ plugins: [react()],
+ optimizeDeps: {
+ exclude: ['lucide-react'],
+ },
+ server: isDev ? {
+ proxy: {
+ '/yahoo': {
+ target: 'https://query1.finance.yahoo.com',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/yahoo/, ''),
+ headers: {
+ 'Origin': 'https://finance.yahoo.com'
+ }
+ }
+ }
+ } : undefined,
+ base: env.VITE_BASE_URL || '/',
+ };
+});