diff --git a/.github/workflows/run-eslint.yaml b/.github/workflows/run-eslint.yaml index ff3d66d..93ddc33 100644 --- a/.github/workflows/run-eslint.yaml +++ b/.github/workflows/run-eslint.yaml @@ -1,36 +1,27 @@ name: Run ESLint on: - push: - branches: - - main - - development pull_request: - branches: - - main - - development jobs: eslint: runs-on: ubuntu-latest steps: - # Step 1: Checkout the repository - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - # Step 2: Set up Node.js - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '20' # Adjust the Node.js version as needed + node-version: '20' + cache: 'yarn' + cache-dependency-path: applications/idp-arc/frontend/yarn.lock - # Step 3: Install dependencies using Yarn - name: Install dependencies working-directory: applications/idp-arc/frontend - run: yarn install + run: yarn install --frozen-lockfile - # Step 4: Run ESLint - name: Run ESLint working-directory: applications/idp-arc/frontend run: yarn lint \ No newline at end of file diff --git a/.github/workflows/ui-tests.yaml b/.github/workflows/ui-tests.yaml new file mode 100644 index 0000000..9a1a0db --- /dev/null +++ b/.github/workflows/ui-tests.yaml @@ -0,0 +1,39 @@ +name: UI Tests (Playwright) + +on: + pull_request: + +jobs: + e2e: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + cache-dependency-path: applications/idp-arc/frontend/yarn.lock + + - name: Install dependencies + working-directory: applications/idp-arc/frontend + run: yarn install --frozen-lockfile + + - name: Install Playwright browsers + working-directory: applications/idp-arc/frontend + run: npx playwright install --with-deps chromium + + - name: Run Playwright tests + working-directory: applications/idp-arc/frontend + run: yarn test:e2e + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: applications/idp-arc/frontend/playwright-report/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 72d1950..a8d7318 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ applications/idp-arc/frontend/node_modules applications/idp-arc/frontend/dist applications/idp-arc/frontend/build +applications/idp-arc/frontend/test-results +applications/idp-arc/frontend/playwright-report cloud-harness deployment/helm/ skaffold.yaml .vscode .overrides -.DS_Store \ No newline at end of file +.DS_Store +CLAUDE.md +CLAUDE.local.md diff --git a/.vite/deps/_metadata.json b/.vite/deps/_metadata.json new file mode 100644 index 0000000..f5133ab --- /dev/null +++ b/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "c7d84c57", + "configHash": "56b22b6d", + "lockfileHash": "e3b0c442", + "browserHash": "605d0283", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/.vite/deps/package.json b/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/README.md b/README.md index a56b9f9..8d94c1f 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,16 @@ Preview production build locally: yarn preview ``` +Unit tests (no browser, no backend): + +```bash +yarn test:unit +``` + +Uploads (Scenario 1 of the MAABCD–OSB integration) go from the browser straight to a public bucket +open for anonymous writes, named by `VITE_UPLOAD_BUCKET_URL` (e.g. `https://storage.googleapis.com/maabcd`), +as `uploads////`. The bucket's CORS must allow PUT from this app's origin. + --- ## Docker (Local Build + Run) diff --git a/applications/idp-arc/Dockerfile b/applications/idp-arc/Dockerfile index b545cd3..132d970 100644 --- a/applications/idp-arc/Dockerfile +++ b/applications/idp-arc/Dockerfile @@ -6,6 +6,9 @@ COPY frontend/package.json ./ COPY frontend/yarn.lock ./ RUN yarn install --frozen-lockfile COPY frontend/ ./ +# Baked into the bundle by Vite at build time; the Codefresh pipeline passes it as a build argument. +ARG VITE_UPLOAD_BUCKET_URL +ENV VITE_UPLOAD_BUCKET_URL=${VITE_UPLOAD_BUCKET_URL} RUN yarn build FROM nginxinc/nginx-unprivileged:stable-alpine diff --git a/applications/idp-arc/deploy/values-dev.yaml b/applications/idp-arc/deploy/values-dev.yaml index 27c1e78..235ebec 100644 --- a/applications/idp-arc/deploy/values-dev.yaml +++ b/applications/idp-arc/deploy/values-dev.yaml @@ -1,5 +1,5 @@ harness: - subdomain: idp-arc + subdomain: www secured: false service: diff --git a/applications/idp-arc/deploy/values.yaml b/applications/idp-arc/deploy/values.yaml index 27c1e78..235ebec 100644 --- a/applications/idp-arc/deploy/values.yaml +++ b/applications/idp-arc/deploy/values.yaml @@ -1,5 +1,5 @@ harness: - subdomain: idp-arc + subdomain: www secured: false service: diff --git a/applications/idp-arc/frontend/.devcontainer/README.md b/applications/idp-arc/frontend/.devcontainer/README.md new file mode 100644 index 0000000..61ef84f --- /dev/null +++ b/applications/idp-arc/frontend/.devcontainer/README.md @@ -0,0 +1,49 @@ +# Dev Container Configuration + +This directory contains the configuration for running the frontend application in a VS Code Dev Container. + +## Features + +- **Node.js LTS**: Pre-configured with the latest LTS version of Node.js +- **Yarn**: Yarn package manager pre-installed +- **Playwright**: E2E testing environment with browser dependencies +- **VS Code Extensions**: Essential extensions for React/TypeScript development +- **Auto-start**: Development server automatically starts when container launches +- **Port Forwarding**: Vite dev server (port 5173) automatically forwarded + +## Usage + +1. Open this folder in VS Code +2. When prompted, click "Reopen in Container" +3. Wait for the container to build and dependencies to install +4. The dev server will start automatically on port 5173 + +## Manual Commands + +If you need to run commands manually: + +```bash +# Install dependencies +yarn install + +# Start dev server +yarn dev + +# Run linting +yarn lint + +# Run e2e tests +yarn test:e2e + +# Run e2e tests in UI mode +yarn test:e2e:ui +``` + +## Customization + +You can modify `.devcontainer/devcontainer.json` to: +- Add more VS Code extensions +- Change Node.js version +- Add additional features +- Modify port forwarding +- Customize startup commands diff --git a/applications/idp-arc/frontend/.devcontainer/devcontainer.json b/applications/idp-arc/frontend/.devcontainer/devcontainer.json new file mode 100644 index 0000000..2667a17 --- /dev/null +++ b/applications/idp-arc/frontend/.devcontainer/devcontainer.json @@ -0,0 +1,45 @@ +{ + "name": "IDP ARC Frontend Dev Container", + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", + + "features": { + "ghcr.io/devcontainers/features/node:1": { + "nodeGypDependencies": true, + "version": "lts" + } + }, + + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "ms-playwright.playwright", + "bradlc.vscode-tailwindcss", + "dsznajder.es7-react-js-snippets", + "ms-vscode.vscode-typescript-next" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "typescript.tsdk": "node_modules/typescript/lib" + } + } + }, + + "forwardPorts": [5173], + + "portsAttributes": { + "5173": { + "label": "Vite Dev Server", + "onAutoForward": "notify" + } + }, + + "postCreateCommand": "yarn install && npx playwright install --with-deps", + + "remoteUser": "node" +} diff --git a/applications/idp-arc/frontend/.gitignore b/applications/idp-arc/frontend/.gitignore new file mode 100644 index 0000000..32029ad --- /dev/null +++ b/applications/idp-arc/frontend/.gitignore @@ -0,0 +1,5 @@ +.env.* + +# Local dev TLS certs (yarn dev over HTTPS); never committed. +.certs/ +*.pem diff --git a/applications/idp-arc/frontend/e2e/app.spec.ts b/applications/idp-arc/frontend/e2e/app.spec.ts new file mode 100644 index 0000000..75be6c0 --- /dev/null +++ b/applications/idp-arc/frontend/e2e/app.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test' + +test('page has correct title', async ({ page }) => { + await page.goto('/') + await expect(page).toHaveTitle('MAABCD') +}) + +test('root element is rendered', async ({ page }) => { + await page.goto('/') + const root = page.locator('#root') + await expect(root).toBeAttached() +}) diff --git a/applications/idp-arc/frontend/index.html b/applications/idp-arc/frontend/index.html index 5ad1e3b..3ae7837 100644 --- a/applications/idp-arc/frontend/index.html +++ b/applications/idp-arc/frontend/index.html @@ -2,9 +2,8 @@ - - idp-arc + MAABCD
diff --git a/applications/idp-arc/frontend/package-lock.json b/applications/idp-arc/frontend/package-lock.json new file mode 100644 index 0000000..bab8b44 --- /dev/null +++ b/applications/idp-arc/frontend/package-lock.json @@ -0,0 +1,3440 @@ +{ + "name": "idp-arc-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "idp-arc-frontend", + "version": "0.0.1", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.0.0", + "@mui/material": "^9.0.0", + "i18next": "^26.0.3", + "keycloak-js": "^26.2.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-i18next": "^17.0.2", + "react-router-dom": "^7.14.2" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@playwright/test": "^1.52.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "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, + "license": "Apache-2.0", + "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.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "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.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "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.1", + "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.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "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.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.0.tgz", + "integrity": "sha512-uwQNGkhv0lf7ufxw6QXev77BW6pWbW+7uxYjU5+rfp4lBkFtMEgJCsarTM3Tn+i0lGx6+Ol2u88JdGXr0GDskA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.0.0.tgz", + "integrity": "sha512-oDwyvI6LgjWRC9MBcSGvLkPud9S9ELgSBQFYxa1rYcZn6Br55dn22SyvsPDMsn0G8OndFk53iMT45W5mNqrogw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^9.0.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.0.0.tgz", + "integrity": "sha512-+VP/oQCDhDR87NQQgXnNBG8dwy6GNuQLnenS1pZvkbn2dKFSxRSRMybTpH9xUxXP+316mlYDy5CSbYtusnCWtw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/core-downloads-tracker": "^9.0.0", + "@mui/system": "^9.0.0", + "@mui/types": "^9.0.0", + "@mui/utils": "^9.0.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.4", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.0.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.0.0.tgz", + "integrity": "sha512-JtuZoaiCqwD6vjgYu6Xp3T7DZkrxJlgtDz5yESzhI34fEX5hHMh2VJUbuL9UOg8xrfIFMrq6dcYoH/7Zi4G0RA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/utils": "^9.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.0.0.tgz", + "integrity": "sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.0.0.tgz", + "integrity": "sha512-YnC5Zg6j04IxiLc/boAKs0464jfZlLFVa7mf5E8lF0XOtZVUvG6R6gJK50lgUYdaaLdyLfxF6xR7LaPuEpeT/g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/private-theming": "^9.0.0", + "@mui/styled-engine": "^9.0.0", + "@mui/types": "^9.0.0", + "@mui/utils": "^9.0.0", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.0.0.tgz", + "integrity": "sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.0.0.tgz", + "integrity": "sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/types": "^9.0.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/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, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" + }, + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "dev": true, + "license": "MIT", + "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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "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.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "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.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "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-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "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, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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, + "license": "MIT" + }, + "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/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "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.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.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": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "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.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "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.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "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, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.0.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.3.tgz", + "integrity": "sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.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, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "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==", + "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, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keycloak-js": { + "version": "26.2.3", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.3.tgz", + "integrity": "sha512-widjzw/9T6bHRgEp6H/Se3NCCarU7u5CwFKBcwtu7xfA1IfdZb+7Q7/KGusAnBo34Vtls8Oz9vzSqkQvQ7+b4Q==", + "license": "Apache-2.0", + "workspaces": [ + "test" + ] + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT" + }, + "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/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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/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, + "license": "MIT", + "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/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-i18next": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.2.tgz", + "integrity": "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", + "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", + "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "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/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "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.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "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, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "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/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/applications/idp-arc/frontend/package.json b/applications/idp-arc/frontend/package.json index 19fe2d9..d853039 100644 --- a/applications/idp-arc/frontend/package.json +++ b/applications/idp-arc/frontend/package.json @@ -4,20 +4,35 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "vite", + "dev": "VITE_USE_MOCKS=true vite", + "start:dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test:unit": "node --import ./tests/unit/ts-resolve.mjs --test 'tests/unit/**/*.test.ts'", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.0.0", + "@mui/material": "^9.0.0", + "i18next": "^26.0.3", + "keycloak-js": "^26.2.3", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-i18next": "^17.0.2", + "react-router-dom": "^7.14.2", + "spark-md5": "^3.0.2" }, "devDependencies": { "@eslint/js": "^9.39.1", + "@playwright/test": "^1.52.0", "@types/node": "^24.10.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "@types/spark-md5": "^3.0.5", "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", @@ -27,4 +42,4 @@ "typescript-eslint": "^8.48.0", "vite": "^7.3.1" } -} +} \ No newline at end of file diff --git a/applications/idp-arc/frontend/playwright.config.ts b/applications/idp-arc/frontend/playwright.config.ts new file mode 100644 index 0000000..a538f89 --- /dev/null +++ b/applications/idp-arc/frontend/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? 'github' : 'html', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'yarn dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/applications/idp-arc/frontend/public/Emory.png b/applications/idp-arc/frontend/public/Emory.png new file mode 100644 index 0000000..22ff161 Binary files /dev/null and b/applications/idp-arc/frontend/public/Emory.png differ diff --git a/applications/idp-arc/frontend/public/UCBerkeley.png b/applications/idp-arc/frontend/public/UCBerkeley.png new file mode 100644 index 0000000..8d03d00 Binary files /dev/null and b/applications/idp-arc/frontend/public/UCBerkeley.png differ diff --git a/applications/idp-arc/frontend/public/UCL.png b/applications/idp-arc/frontend/public/UCL.png new file mode 100644 index 0000000..6be6ec0 Binary files /dev/null and b/applications/idp-arc/frontend/public/UCL.png differ diff --git a/applications/idp-arc/frontend/public/UCSantaCruz.png b/applications/idp-arc/frontend/public/UCSantaCruz.png new file mode 100644 index 0000000..ade71fc Binary files /dev/null and b/applications/idp-arc/frontend/public/UCSantaCruz.png differ diff --git a/applications/idp-arc/frontend/public/afterCorrection.png b/applications/idp-arc/frontend/public/afterCorrection.png new file mode 100644 index 0000000..779e402 Binary files /dev/null and b/applications/idp-arc/frontend/public/afterCorrection.png differ diff --git a/applications/idp-arc/frontend/public/beforeCorrection.png b/applications/idp-arc/frontend/public/beforeCorrection.png new file mode 100644 index 0000000..7790a9c Binary files /dev/null and b/applications/idp-arc/frontend/public/beforeCorrection.png differ diff --git a/applications/idp-arc/frontend/public/bgTexture.png b/applications/idp-arc/frontend/public/bgTexture.png new file mode 100644 index 0000000..5588fb7 Binary files /dev/null and b/applications/idp-arc/frontend/public/bgTexture.png differ diff --git a/applications/idp-arc/frontend/public/mainBG.png b/applications/idp-arc/frontend/public/mainBG.png new file mode 100644 index 0000000..a435086 Binary files /dev/null and b/applications/idp-arc/frontend/public/mainBG.png differ diff --git a/applications/idp-arc/frontend/public/protocol1.png b/applications/idp-arc/frontend/public/protocol1.png new file mode 100644 index 0000000..0047dd1 Binary files /dev/null and b/applications/idp-arc/frontend/public/protocol1.png differ diff --git a/applications/idp-arc/frontend/public/protocols_archives/4c.zip b/applications/idp-arc/frontend/public/protocols_archives/4c.zip new file mode 100644 index 0000000..5fbe0ff Binary files /dev/null and b/applications/idp-arc/frontend/public/protocols_archives/4c.zip differ diff --git a/applications/idp-arc/frontend/public/silent-check-sso.html b/applications/idp-arc/frontend/public/silent-check-sso.html new file mode 100644 index 0000000..9435ce6 --- /dev/null +++ b/applications/idp-arc/frontend/public/silent-check-sso.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/applications/idp-arc/frontend/src/App.css b/applications/idp-arc/frontend/src/App.css index b9d355d..328e7bd 100644 --- a/applications/idp-arc/frontend/src/App.css +++ b/applications/idp-arc/frontend/src/App.css @@ -1,42 +1,3 @@ #root { - max-width: 1280px; margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} +} \ No newline at end of file diff --git a/applications/idp-arc/frontend/src/App.tsx b/applications/idp-arc/frontend/src/App.tsx index 3d7ded3..e51ca9a 100644 --- a/applications/idp-arc/frontend/src/App.tsx +++ b/applications/idp-arc/frontend/src/App.tsx @@ -1,34 +1,89 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { createBrowserRouter, RouterProvider } from 'react-router-dom' +import { ThemeProvider } from '@mui/material/styles' +import CssBaseline from '@mui/material/CssBaseline' + +import { AppContext } from './AppContext' +import { authClient } from './app/container' +import { theme } from './theme/components' +import type { AuthState } from './core/types' +import Home from './pages/LandingPage' +import Workspaces from './pages/Workspaces' +import ProtocolsPage from './pages/ProtocolsPage' +import AboutPage from './pages/AboutPage' + +const router = createBrowserRouter([ + { path: '/', element: }, + { path: '/workspaces', element: }, + { path: '/protocols', element: }, + { path: '/about', element: }, +]) function App() { - const [count, setCount] = useState(0) + const { t } = useTranslation() + const [authState, setAuthState] = useState('loading') + const [tokenParsed, setTokenParsed] = useState | null>(null) + const [authError, setAuthError] = useState(null) + const initialized = useRef(false) + + useEffect(() => { + if (initialized.current) return + initialized.current = true + + authClient + .init() + .then((authenticated: boolean) => { + // If running inside the login popup, signal the parent and close. + if (authenticated && window.opener) { + window.opener.postMessage({ type: 'keycloak-login-complete' }, window.location.origin) + window.close() + return + } + setAuthState(authenticated ? 'authenticated' : 'unauthenticated') + if (authenticated && authClient.tokenParsed) { + setTokenParsed(authClient.tokenParsed) + } + }) + .catch((err: Error) => { + // Clear stale auth fragment so a page refresh doesn't retry a failed token exchange. + if (window.location.hash) { + window.history.replaceState(null, '', window.location.pathname + window.location.search) + } + console.error('Keycloak init failed', err) + setAuthError(t('errors.authFailed')) + setAuthState('unauthenticated') + }) + }, [t]) + + // Reload after popup login so check-sso picks up the new Keycloak session. + useEffect(() => { + const handler = (event: MessageEvent) => { + if (event.origin === window.location.origin && event.data?.type === 'keycloak-login-complete') { + window.location.reload() + } + } + window.addEventListener('message', handler) + return () => window.removeEventListener('message', handler) + }, []) + + const username = + (tokenParsed?.preferred_username as string) ?? + (tokenParsed?.email as string) ?? + 'Unknown user' + + const appContextValue = useMemo( + () => ({ authState, tokenParsed, authError, username }), + [authState, tokenParsed, authError, username], + ) return ( - <> - -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- + + + + + + ) } diff --git a/applications/idp-arc/frontend/src/AppContext.ts b/applications/idp-arc/frontend/src/AppContext.ts new file mode 100644 index 0000000..82a2ab5 --- /dev/null +++ b/applications/idp-arc/frontend/src/AppContext.ts @@ -0,0 +1,20 @@ +import { createContext, useContext } from 'react' +import type { AuthState } from './core/types' + +export type AppContextValue = { + authState: AuthState + tokenParsed: Record | null + authError: string | null + username: string +} + +export const AppContext = createContext({ + authState: 'loading', + tokenParsed: null, + authError: null, + username: 'Unknown user', +}) + +export function useAppContext(): AppContextValue { + return useContext(AppContext) +} diff --git a/applications/idp-arc/frontend/src/Icons.tsx b/applications/idp-arc/frontend/src/Icons.tsx new file mode 100644 index 0000000..9bed4a8 --- /dev/null +++ b/applications/idp-arc/frontend/src/Icons.tsx @@ -0,0 +1,106 @@ +import { SvgIcon } from "@mui/material" +import type { SvgIconProps } from "@mui/material" + +export function Logo(props: SvgIconProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/applications/idp-arc/frontend/src/app/container.ts b/applications/idp-arc/frontend/src/app/container.ts new file mode 100644 index 0000000..fa0db2a --- /dev/null +++ b/applications/idp-arc/frontend/src/app/container.ts @@ -0,0 +1,80 @@ +/** + * Composition Root — src/app/container.ts + * + * The ONLY file allowed to import from both src/infra/ and src/core/use-cases/. + * It creates concrete infrastructure instances and wires them into use-cases. + * + * Dependency graph (arrows = "depends on"): + * + * App.tsx ──► container ──► use-cases ──► ports (interfaces) + * ──► infra ──implements──► ports + * + * Rules enforced by convention: + * • core/use-cases/ must NOT import from infra/ + * • infra/ must NOT import from core/use-cases/ + * • App.tsx must NOT import from infra/ directly + * • Only container.ts crosses those layer boundaries. + */ + +import { KeycloakAuthClient } from '../infra/keycloakAuthClient' +import { WorkspaceApiClient } from '../infra/workspaceApiClient' +import { DandiApiClient } from '../infra/dandiApiClient' +import { PublicBucketObjectStore } from '../infra/publicBucketObjectStore' +import type { IObjectStore } from '../core/ports/IObjectStore' +import { createLoadWorkspacesUseCase } from '../core/use-cases/loadWorkspaces' +import { createCreateAndUploadToDandiUseCase } from '../core/use-cases/createAndUploadToDandi' +import { createCreateWorkspaceUseCase } from '../core/use-cases/createWorkspace' +import { createRunProtocolUseCase } from '../core/use-cases/runProtocol' + +// ─── Config ─────────────────────────────────────────────────────────────────── +// All environment-specific URLs live here (or read from import.meta.env in Vite). + +const BASE_DOMAIN = import.meta.env.VITE_OSB_BASE_DOMAIN ?? 'v2dev.opensourcebrain.org' +const PROTOCOL = import.meta.env.VITE_OSB_PROTOCOL ?? 'https' +const WWW_BASE = import.meta.env.DEV ? '/api-proxy' : `${PROTOCOL}://www.${BASE_DOMAIN}` +const WORKSPACES_API = `${WWW_BASE}/proxy/workspaces/api` +const WORKSPACES_LIST_URL = + `${WWW_BASE}/proxy/workspaces/api/workspace?page=1&per_page=24&q=&tags=` +const FRONTEND_BASE = `${PROTOCOL}://www.${BASE_DOMAIN}` + +// ─── Infrastructure singletons ──────────────────────────────────────────────── + +export const authClient = new KeycloakAuthClient({ + url: import.meta.env.VITE_KEYCLOAK_URL ?? 'https://accounts.v2dev.opensourcebrain.org', + realm: import.meta.env.VITE_KEYCLOAK_REALM ?? 'osb2dev', + clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID ?? 'maabcd', +}) + +const workspaceApi = new WorkspaceApiClient(WORKSPACES_API, WORKSPACES_LIST_URL) +// DANDI upload endpoints live in OSB's `workspaces` app (the admin key has to sit wherever +// they run) — same API base as every other workspace call. +const dandiApi = new DandiApiClient(WORKSPACES_API) + +/** Scenario 1: where uploads go before OSB imports them (`gs://maabcd`). Used by the upload flow. */ +export const objectStore: IObjectStore = new PublicBucketObjectStore(import.meta.env.VITE_UPLOAD_BUCKET_URL) + +// ─── Use-cases (injected with their concrete dependencies) ──────────────────── + +/** Refreshes the token then returns the workspace list. */ +export const loadWorkspaces = createLoadWorkspacesUseCase(authClient, workspaceApi) + +/** Creates a new, empty workspace with the given name. */ +export const createWorkspace = createCreateWorkspaceUseCase(authClient, workspaceApi) + +/** Uploads the researcher's file, imports it and the protocol's repository into the workspace + * through OSB, and runs the notebooks in OSB's Argo task (MAABCD–OSB design, Scenario 1). */ +export const runProtocol = createRunProtocolUseCase(authClient, workspaceApi, objectStore) + +/** DANDI-backed upload (Route A, see IDP-43 notes); `finalize` also runs the selected + * protocol's script server-side (jupyter_kernel_client.py in OSBv2's workspaces app). */ +export const createAndUploadToDandi = createCreateAndUploadToDandiUseCase(authClient, dandiApi) + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +/** + * Builds the public URL for a given workspace id. + * Centralised here so no component needs to know BASE_DOMAIN. + */ +export function getWorkspaceUrl(workspaceId: number): string { + return `${FRONTEND_BASE}/workspaces/open/${workspaceId}/jupyter` +} diff --git a/applications/idp-arc/frontend/src/app/mockContainer.ts b/applications/idp-arc/frontend/src/app/mockContainer.ts new file mode 100644 index 0000000..1494bef --- /dev/null +++ b/applications/idp-arc/frontend/src/app/mockContainer.ts @@ -0,0 +1,39 @@ +/** + * Mock Composition Root — src/app/mockContainer.ts + * + * Mirrors container.ts but wires in-memory mock implementations instead of + * real HTTP/Keycloak adapters. Used when VITE_USE_MOCKS=true (i.e. `pnpm dev:mock`). + * + * Exported surface is intentionally identical to container.ts so App.tsx never + * needs to know which container it is talking to. + */ + +import { MockAuthClient } from '../infra/mocks/mockAuthClient' +import { MockWorkspaceApiClient } from '../infra/mocks/mockWorkspaceApiClient' +import { MockDandiApiClient } from '../infra/mocks/mockDandiApiClient' +import { MockObjectStore } from '../infra/mocks/mockObjectStore' +import { createRunProtocolUseCase } from '../core/use-cases/runProtocol' +import type { IObjectStore } from '../core/ports/IObjectStore' +import { createLoadWorkspacesUseCase } from '../core/use-cases/loadWorkspaces' +import { createCreateAndUploadToDandiUseCase } from '../core/use-cases/createAndUploadToDandi' + +// ─── Mock infrastructure singletons ────────────────────────────────────────── + +export const authClient = new MockAuthClient() + +const workspaceApi = new MockWorkspaceApiClient() +const dandiApi = new MockDandiApiClient() + +export const objectStore: IObjectStore = new MockObjectStore() + +// ─── Use-cases (same factory functions, different adapters) ─────────────────── + +export const loadWorkspaces = createLoadWorkspacesUseCase(authClient, workspaceApi) +export const runProtocol = createRunProtocolUseCase(authClient, workspaceApi, objectStore) +export const createAndUploadToDandi = createCreateAndUploadToDandiUseCase(authClient, dandiApi) + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +export function getWorkspaceUrl(workspaceId: number): string { + return `/mock/workspaces/${workspaceId}/jupyter` +} diff --git a/applications/idp-arc/frontend/src/assets/ASST_graphic.jpeg b/applications/idp-arc/frontend/src/assets/ASST_graphic.jpeg new file mode 100644 index 0000000..e9c420a Binary files /dev/null and b/applications/idp-arc/frontend/src/assets/ASST_graphic.jpeg differ diff --git a/applications/idp-arc/frontend/src/assets/BanditFigure_UCLstyleBohan.png b/applications/idp-arc/frontend/src/assets/BanditFigure_UCLstyleBohan.png new file mode 100644 index 0000000..91f5064 Binary files /dev/null and b/applications/idp-arc/frontend/src/assets/BanditFigure_UCLstyleBohan.png differ diff --git a/applications/idp-arc/frontend/src/assets/four_choice_graphic.jpeg b/applications/idp-arc/frontend/src/assets/four_choice_graphic.jpeg new file mode 100644 index 0000000..d3f2cfa Binary files /dev/null and b/applications/idp-arc/frontend/src/assets/four_choice_graphic.jpeg differ diff --git a/applications/idp-arc/frontend/src/components/DataUploadDialog.tsx b/applications/idp-arc/frontend/src/components/DataUploadDialog.tsx new file mode 100644 index 0000000..b3872eb --- /dev/null +++ b/applications/idp-arc/frontend/src/components/DataUploadDialog.tsx @@ -0,0 +1,457 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { + Box, + Button, + CircularProgress, + Dialog, + IconButton, + MenuItem, + Select, + Stack, + Typography, +} from '@mui/material' +import CloudUploadOutlinedIcon from '@mui/icons-material/CloudUploadOutlined' +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutlineOutlined' +import CloseIcon from '@mui/icons-material/Close' +import ArrowForwardIcon from '@mui/icons-material/ArrowForward' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import OpenInNewIcon from '@mui/icons-material/OpenInNewOutlined' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlineOutlined' +import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty' + +import { getWorkspaceUrl, runProtocol } from '../app/container' +import { useAppContext } from '../AppContext' +import type { RunStep } from '../core/types' +import RunChecklist from './RunChecklist' +import { inputFileProblem, inputFormatsFor } from '../core/inputFormats' +import { formatBytes } from '../core/formatBytes' +import { templatesZipFileName, templatesZipHref } from '../core/protocolTemplates' +import { SIGN_IN_AGAIN } from '../core/userMessages' +import protocols from '../data/protocols.json' + +/** Only protocols with an analysis repository can be run, so only those are offered. */ +const runnableProtocols = protocols.filter((p) => 'repoZipUrl' in p && p.repoZipUrl) + +type DialogStep = 'select' | 'upload' | 'uploading' | 'success' | 'stillRunning' | 'failed' + +export interface DataUploadDialogProps { + open: boolean + onClose: () => void + onAuthRequired?: () => void +} + +export default function DataUploadDialog({ open, onClose, onAuthRequired }: DataUploadDialogProps) { + const { tokenParsed } = useAppContext() + + interface FormState { + step: DialogStep + protocol: string + file: File | null + isDragging: boolean + uploadMessage: string + /** ID of the workspace spawned in the current dialog session; drives retry behaviour. */ + spawnedWorkspaceId: number | undefined + /** Where the last run's results are, relative to the workspace root. */ + outputsDir: string + /** The run's checklist (upload, imports, run), as last reported by the use-case. */ + runSteps: RunStep[] + } + + const INITIAL_FORM: FormState = { + step: 'select', + protocol: '', + file: null, + isDragging: false, + uploadMessage: '', + spawnedWorkspaceId: undefined, + outputsDir: '', + runSteps: [], + } + + const [form, setForm] = useState(INITIAL_FORM) + const { step, protocol, file, isDragging, uploadMessage, spawnedWorkspaceId, outputsDir, runSteps } = form + const fileInputRef = useRef(null) + // The run being watched; each run gets its own token, so stopping one never revives another. + const watchRef = useRef<{ current: boolean } | null>(null) + + // The dialog can't be closed from clicking Upload until the notebooks have started in OSB: + // before that, closing would abandon the upload or the imports half way (and the user would + // have to start over). Once they run in OSB's task, closing only stops watching. + const [closeLocked, setCloseLocked] = useState(false) + useEffect(() => { + if (step !== 'uploading') setCloseLocked(false) // finished, failed, or sent back to the form + }, [step]) + + // While a run is being set up, uploaded or watched, ask before the page is reloaded or closed: + // the upload is driven by this tab and would be lost, and the progress view would lose track + // of the run (which itself carries on in the workspace). Browsers show their own generic + // "Leave site?" text. This can't stop sleep, a crash, or the browser discarding the tab. + useEffect(() => { + if (!open || step !== 'uploading') return + const warn = (e: BeforeUnloadEvent) => { + e.preventDefault() + e.returnValue = '' // still needed by some browsers to show the prompt + } + window.addEventListener('beforeunload', warn) + return () => window.removeEventListener('beforeunload', warn) + }, [open, step]) + + useEffect(() => { + if (!open) { + // Stops watching only: the run itself carries on in the workspace. + if (watchRef.current) watchRef.current.current = true + return + } + setForm(INITIAL_FORM) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]) + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault() + const dropped = e.dataTransfer.files[0] + setForm((prev) => ({ ...prev, isDragging: false, file: dropped ?? prev.file })) + }, []) + + const openWorkspaceTab = (wsId: number) => { + window.open(getWorkspaceUrl(wsId), '_blank') + window.focus() + } + + + /** The selected protocol; its `repoZipUrl` in protocols.json is the analysis that runs. */ + const selectedProtocol = runnableProtocols.find((p) => p.name === protocol) + const inputFormats = inputFormatsFor(selectedProtocol?.inputFormats) + const templatesHref = templatesZipHref(selectedProtocol) + const fileProblem = file ? inputFileProblem(file, selectedProtocol?.name ?? 'This protocol', selectedProtocol?.inputFormats) : null + + const handleUpload = async () => { + if (!file || fileProblem || !selectedProtocol) return + setCloseLocked(true) + setForm((prev) => ({ ...prev, step: 'uploading', uploadMessage: '', outputsDir: '', runSteps: [] })) + if (watchRef.current) watchRef.current.current = true + const watch = { current: false } + watchRef.current = watch + + await runProtocol( + { + protocol: selectedProtocol, + file, + // `spawnedWorkspaceId` is set as soon as the run has a workspace, so a retry reuses it + // instead of creating another one. + workspaceId: spawnedWorkspaceId, + workspaceName: selectedProtocol.name, + }, + (state) => { + if (watch.current) return // the dialog was closed, or another run started: no longer this one's + if (state.phase === 'running') setCloseLocked(false) // in OSB's task now: closing only stops watching + if (state.phase === 'failed' && state.error?.toLowerCase().includes(SIGN_IN_AGAIN)) { + setForm((prev) => ({ + ...prev, step: 'upload', uploadMessage: '', spawnedWorkspaceId: state.workspaceId ?? prev.spawnedWorkspaceId, + })) + onAuthRequired?.() + return + } + setForm((prev) => ({ + ...prev, + spawnedWorkspaceId: state.workspaceId ?? prev.spawnedWorkspaceId, + uploadMessage: state.phase === 'failed' ? `${state.message}: ${state.error}` : state.message, + outputsDir: state.outputsDir ?? prev.outputsDir, + runSteps: state.steps, + ...(state.phase === 'succeeded' ? { step: 'success' } : {}), + ...(state.phase === 'stillRunning' ? { step: 'stillRunning' } : {}), + ...(state.phase === 'failed' ? { step: 'failed' } : {}), + })) + }, + watch, + ) + } + + const stepIndex = step === 'select' ? 0 : 1 + const canGoNext = !!protocol + const canUpload = !!file && !fileProblem + + return ( + + {/* ── Header ─────────────────────────────────────────────────────── */} + + + + + {[0, 1].map((i) => ( + + ))} + + + + {tokenParsed && ( + + )} + + + + + + + {/* ── Title & subtitle ───────────────────────────────────────────── */} + {step === 'select' ? ( + + + Select behavioral task / protocol + + + Select a behavioral task / protocol. + + + ) : ( + + + Upload your data + + {step === 'upload' && ( + + Upload your data to contribute to multimodal neurophysiology research and visualize it on the Open Source Brain platform. + + )} + + )} + + {/* ── Content ────────────────────────────────────────────────────── */} + + + {/* Step 1 — select */} + {step === 'select' && ( + + + + Behavioral task / Protocol + + + + + + Protocol docs + + Read how this protocol's analysis works and what it expects as input. + + + + + )} + + {/* Step 2 — file upload */} + {step === 'upload' && ( + + {uploadMessage && ( + + {uploadMessage} + + )} + + + setForm((prev) => ({ ...prev, file: e.target.files?.[0] ?? null }))} + /> + fileInputRef.current?.click()} + onDragOver={(e) => { e.preventDefault(); setForm((prev) => ({ ...prev, isDragging: true })) }} + onDragLeave={() => setForm((prev) => ({ ...prev, isDragging: false }))} + onDrop={handleDrop} + sx={{ + height: '100%', + border: '1px solid', + borderColor: isDragging ? 'primary.main' : 'divider', + borderRadius: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + bgcolor: isDragging ? 'action.hover' : 'transparent', + transition: 'border-color 0.2s, background-color 0.2s', + gap: 1, + }} + > + {file ? ( + <> + + {file.name} ({formatBytes(file.size)}) + + {fileProblem && ( + + {fileProblem} + + )} + + ) : ( + <> + + + Click here or drag file to upload{inputFormats ? ` (${inputFormats.join(', ')})` : ''} + + + )} + + + + + Protocol template + + Download an example input file for this protocol and fill in your data the same way. + + + + + + )} + + {/* Running, finished or failed: the same checklist, with a different header */} + {(step === 'uploading' || step === 'success' || step === 'stillRunning' || step === 'failed') && ( + + + {step === 'uploading' && } + {step === 'success' && } + {step === 'stillRunning' && } + {step === 'failed' && } + + {uploadMessage || (step === 'success' ? 'The analysis finished.' : 'Starting..')} + + {(step === 'success' || step === 'stillRunning') && outputsDir && ( + + {step === 'stillRunning' ? 'Results will be in' : 'Results are in'} {outputsDir}/ in your workspace. + + )} + {step !== 'uploading' && spawnedWorkspaceId !== undefined && ( + + )} + + {runSteps.length > 0 && } + + )} + + + {/* ── Footer ─────────────────────────────────────────────────────── */} + {step === 'select' && ( + + + + )} + {step === 'upload' && ( + + + + + )} + {step === 'failed' && ( + + + + + )} + + ) +} diff --git a/applications/idp-arc/frontend/src/components/PageLayout.tsx b/applications/idp-arc/frontend/src/components/PageLayout.tsx new file mode 100644 index 0000000..bec254b --- /dev/null +++ b/applications/idp-arc/frontend/src/components/PageLayout.tsx @@ -0,0 +1,485 @@ +import ArrowForwardIcon from '@mui/icons-material/ArrowForward' +import CloseIcon from '@mui/icons-material/Close' +import MenuIcon from '@mui/icons-material/Menu' +import { + AppBar, + Box, + Button, + CircularProgress, + Container, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Drawer, + IconButton, + Menu, + MenuItem, + Stack, + Tooltip, + Toolbar, + Typography, + useScrollTrigger, +} from '@mui/material' +import type { ReactNode } from 'react' +import { useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useLocation, useNavigate } from 'react-router-dom' +import { authClient } from '../app/container' +import { useAppContext } from '../AppContext' +import { Logo } from '../Icons' +import DataUploadDialog from './DataUploadDialog' +import { registerUploadOpener } from './UploadContext' + +const ArrowIcon = () => + +export interface PageLayoutProps { + children: ReactNode + title: ReactNode + image?: string + height?: number + cta?: ReactNode + showDivider?: boolean +} + +export default function PageLayout({ + children, + title, + image, + height = 477, + cta, +}: PageLayoutProps) { + const { t } = useTranslation('landingPage') + const scrolled = useScrollTrigger({ disableHysteresis: true, threshold: 50 }) + const [drawerOpen, setDrawerOpen] = useState(false) + const [avatarAnchor, setAvatarAnchor] = useState(null) + const avatarMenuOpen = Boolean(avatarAnchor) + const [waitingForLogin, setWaitingForLogin] = useState(false) + const popupRef = useRef(null) + const [uploadDialogOpen, setUploadDialogOpen] = useState(false) + + async function handleLogin() { + let loginUrl: string + try { + loginUrl = await authClient.getLoginUrl(`${window.location.origin}/`) + } catch { + // keycloak-js 26 + PKCE can throw if internal state is unset (e.g. after + // token expiry). Fall back to a full-page redirect via kc.login(). + authClient.login() + return + } + const w = 480, h = 600 + const left = Math.round((screen.width - w) / 2) + const top = Math.round((screen.height - h) / 2) + popupRef.current = window.open(loginUrl, 'kc-login', `width=${w},height=${h},left=${left},top=${top},toolbar=no,menubar=no`) + setWaitingForLogin(true) + + const poll = setInterval(() => { + if (popupRef.current?.closed) { + clearInterval(poll) + setWaitingForLogin(false) + } + }, 500) + } + + function cancelLogin() { + popupRef.current?.close() + setWaitingForLogin(false) + } + const navigate = useNavigate() + const location = useLocation() + const { authState, username } = useAppContext() + const isAuthenticated = authState === 'authenticated' + + useEffect(() => { + registerUploadOpener(() => + isAuthenticated ? setUploadDialogOpen(true) : void handleLogin() + ) + }, [isAuthenticated]) + + const navItems = [ + { label: t('nav.protocols'), path: '/protocols' }, + { label: t('nav.about'), path: '/about' }, + ...(isAuthenticated ? [{ label: t('nav.myWorkspaces'), path: 'https://www.v2dev.opensourcebrain.org/', external: true }] : []), + ] + + const isActive = (path: string) => location.pathname === path + + const styles = { + root: { + bgcolor: 'var(--mui-palette-background-default)', + minHeight: '100vh', + position: 'relative', + }, + gutterWrapper: { + position: 'absolute', + inset: 0, + overflow: 'hidden', + pointerEvents: 'none', + zIndex: 0, + }, + gutterLine: (offset: string) => ({ + position: 'absolute', + top: 0, + left: `calc(50% + ${offset})`, + width: '1px', + height: '100%', + bgcolor: 'divider', + opacity: 0.4, + pointerEvents: 'none', + zIndex: 0, + }), + appBar: { + zIndex: 10, + transition: 'background 0.3s, border-color 0.3s', + background: scrolled ? 'var(--mui-palette-background-default)' : 'transparent', + borderBottom: scrolled ? '1px solid var(--mui-palette-white-200)' : 'none', + }, + toolbar: { padding: '0 !important' }, + logoStack: { alignItems: 'center', gap: 0.5, cursor: 'pointer', flexShrink: 0 }, + logo: { width: 32, height: 32 }, + desktopNav: { + alignItems: 'center', + gap: 0.5, + display: { xs: 'none', md: 'flex' }, + ml: 'auto', + }, + navButton: (path: string) => ({ + bgcolor: isActive(path) ? 'var(--mui-palette-white-200)' : 'transparent', + '&:hover': { bgcolor: 'var(--mui-palette-white-300)' }, + }), + menuIconButton: { + display: { xs: 'flex', md: 'none' }, + ml: 'auto', + color: 'inherit', + }, + drawerPaper: { width: 240, background: 'var(--mui-palette-background-default)', color: 'var(--mui-palette-text-primary)' }, + drawerStack: { p: 2 }, + drawerCloseButton: { alignSelf: 'flex-end', color: 'inherit' }, + drawerNavStack: { gap: 1, mt: 1 }, + drawerNavButton: { justifyContent: 'flex-start' }, + pageBanner: { + position: 'relative', + height: height, + overflow: 'hidden', + flexShrink: 0, + }, + pageBannerOverlay: { position: 'absolute', inset: 0, pointerEvents: 'none' }, + pageBannerGradientH: { + position: 'absolute', + inset: 0, + background: + 'linear-gradient(to right, rgba(97,62,56,0.4), rgba(57,43,34,0.4) 50%, rgba(29,35,24,0.4))', + }, + pageBannerTexture: { + position: 'absolute', + inset: 0, + backgroundImage: `url('/bgTexture.png')`, + backgroundSize: '1024px 1024px', + opacity: 0.05, + }, + pageBannerImage: { + position: 'absolute', + inset: 0, + width: '100%', + height: '100%', + objectFit: 'cover', + objectPosition: 'top center', + opacity: 0.6, + }, + pageBannerGradientV: { + position: 'absolute', + inset: 0, + background: 'linear-gradient(to bottom, rgba(0,0,0,0) 10%, #1a1a1a 99%)', + }, + pageBannerContainer: { + position: 'relative', + zIndex: 1, + height: '100%', + display: 'flex', + flexDirection: 'column', + pt: image ? '185px' : '193px', + gap: 4, + }, + pageBannerTitle: { maxWidth: { xs: '100%', lg: '75%' } }, + content: { position: 'relative', zIndex: 1 }, + footer: { + // marginTop: 58, + position: 'relative', + zIndex: 1, + borderTop: '1px solid', + borderColor: 'var(--mui-palette-white-200)', + }, + footerStack: { + justifyContent: 'space-between', + alignItems: 'center', + py: 6, + }, + footerLinks: { gap: 1.5 }, + footerLink: { cursor: 'pointer', '&:hover': { color: 'var(--mui-palette-text-primary)' } }, + avatar: { + width: '2rem', + height: '2rem', + borderRadius: '6.25rem', + flexShrink: 0, + marginLeft: 1, + cursor: 'pointer', + border: 'none', + padding: 0, + backdropFilter: 'blur(2px)', + background: + 'linear-gradient(90deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.3) 100%), linear-gradient(131.69deg, #ffff00 2.94%, #ffa900 26.66%, #ff5300 50.37%, #802980 74.08%, #0000ff 97.79%)', + '&:hover': { opacity: 0.85 }, + }, + avatarMenu: { + background: 'var(--mui-palette-background-default)', + border: '1px solid var(--mui-palette-white-200)', + minWidth: 160, + }, + } + + return ( + + + {['-860px', '860px'].map((offset) => ( + + ))} + + + + + + navigate('/')} + > + + {t('nav.logoTitle')} + + + + {navItems.map(({ label, path, external }) => ( + + ))} + { + !isAuthenticated && + } + + {isAuthenticated && ( + <> + + setAvatarAnchor(e.currentTarget)} + sx={styles.avatar} + aria-label={username} + /> + + setAvatarAnchor(null)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + slotProps={{ paper: { sx: styles.avatarMenu } }} + > + { setAvatarAnchor(null); authClient.logout() }}> + {t('nav.logout')} + + + + )} + + + setDrawerOpen(true)} + sx={styles.menuIconButton} + > + + + + + + + setDrawerOpen(false)} + slotProps={{ + paper: { sx: styles.drawerPaper }, + }} + > + + setDrawerOpen(false)} + sx={styles.drawerCloseButton} + > + + + + {navItems.map(({ label, path, external }) => ( + + ))} + {isAuthenticated ? ( + + ) : ( + + )} + + + + + + + + + + + + {image && ( + + )} + + + + + + {typeof title === 'string' ? ( + + {title} + + ) : ( + title + )} + {cta} + + + + {children} + + + + + + {t('footer.copyright')} + + + {( + [ + { key: 'legal', label: t('footer.links.legal') }, + { key: 'contact', label: t('footer.links.contact') }, + ] as const + ).map(({ key, label }) => ( + + {label} + + ))} + + + + + + setUploadDialogOpen(false)} onAuthRequired={handleLogin} /> + + + {t('nav.signingIn')} + + + + {t('nav.completeSignIn')} + + + + + + + + ) +} diff --git a/applications/idp-arc/frontend/src/components/RunChecklist.tsx b/applications/idp-arc/frontend/src/components/RunChecklist.tsx new file mode 100644 index 0000000..5bb851a --- /dev/null +++ b/applications/idp-arc/frontend/src/components/RunChecklist.tsx @@ -0,0 +1,47 @@ +import { Box, CircularProgress, Stack, Typography } from '@mui/material' +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutlineOutlined' +import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlineOutlined' +import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutlineOutlined' + +import type { RunStep, StepState } from '../core/types' + +const STEP_ICON_SX = { fontSize: 20, flexShrink: 0, mt: '1px' } + +function StepIcon({ state }: { state: StepState }) { + switch (state) { + case 'succeeded': + return + case 'running': + return + case 'failed': + return + case 'skipped': + return + default: + return + } +} + +/** One line per stage of the run: workspace, storage, server, then the run's own stages and notebooks. */ +export default function RunChecklist({ steps }: { steps: RunStep[] }) { + return ( + + {steps.map((s) => ( + + + + + {s.label} + + {s.detail && ( + + {s.detail} + + )} + + + ))} + + ) +} diff --git a/applications/idp-arc/frontend/src/components/UploadContext.ts b/applications/idp-arc/frontend/src/components/UploadContext.ts new file mode 100644 index 0000000..e4c75a6 --- /dev/null +++ b/applications/idp-arc/frontend/src/components/UploadContext.ts @@ -0,0 +1,12 @@ +/** + * Module-level upload trigger. + * + * PageLayout registers the real opener (which checks auth and shows the dialog). + * Any page component — including ones that render PageLayout and therefore can + * never be *inside* a context Provider it creates — can call openUpload() to + * trigger the dialog or login flow. + */ +let _opener: () => void = () => {} + +export const registerUploadOpener = (fn: () => void) => { _opener = fn } +export const openUpload = () => _opener() diff --git a/applications/idp-arc/frontend/src/core/dandiEtag.ts b/applications/idp-arc/frontend/src/core/dandiEtag.ts new file mode 100644 index 0000000..a081825 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/dandiEtag.ts @@ -0,0 +1,56 @@ +import SparkMD5 from 'spark-md5' + +/** + * Part size DANDI splits uploads into. Must match theirs exactly or the ETag won't agree. + * + * DANDI validates AssetBlob.etag against `^[0-9a-f]{32}-\d{1,5}$`, so the multipart form is + * required even for a single small part — it can't be skipped below the part size. + */ +export const DANDI_ETAG_PART_SIZE = 64 * 1024 * 1024 // 64 MiB + +export interface PartPlan { + partNumber: number + start: number + end: number + size: number +} + +/** Splits a file size into DANDI's fixed-size parts. Always returns at least one part, even for a 0-byte file. */ +export function planParts(fileSize: number, partSize = DANDI_ETAG_PART_SIZE): PartPlan[] { + const parts: PartPlan[] = [] + let start = 0 + let partNumber = 1 + while (start < fileSize) { + const end = Math.min(start + partSize, fileSize) + parts.push({ partNumber, start, end, size: end - start }) + start = end + partNumber += 1 + } + if (parts.length === 0) parts.push({ partNumber: 1, start: 0, end: 0, size: 0 }) + return parts +} + +/** + * Computes the dandi-etag: MD5 of each part, concatenate the raw (binary) digests, MD5 that + * concatenation, hex-encode, append `-`. Standard S3 multipart ETag algorithm. + */ +export async function computeDandiEtag( + file: File, + partSize = DANDI_ETAG_PART_SIZE, +): Promise<{ etag: string; parts: PartPlan[] }> { + const parts = planParts(file.size, partSize) + let concatenatedRawDigests = '' + + for (const part of parts) { + const buf = await file.slice(part.start, part.end).arrayBuffer() + const hasher = new SparkMD5.ArrayBuffer() + hasher.append(buf) + concatenatedRawDigests += hasher.end(true) // raw binary string — 16 bytes per part + } + + const outer = new SparkMD5() + outer.appendBinary(concatenatedRawDigests) + const etag = `${outer.end()}-${parts.length}` + + return { etag, parts } +} diff --git a/applications/idp-arc/frontend/src/core/formatBytes.ts b/applications/idp-arc/frontend/src/core/formatBytes.ts new file mode 100644 index 0000000..1f11ee7 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/formatBytes.ts @@ -0,0 +1,6 @@ +/** A file size for people: KB below 1 MB, then MB, then GB. */ +export function formatBytes(bytes: number): string { + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + if (bytes < 1024 ** 3) return `${(bytes / 1024 / 1024).toFixed(1)} MB` + return `${(bytes / 1024 ** 3).toFixed(2)} GB` +} diff --git a/applications/idp-arc/frontend/src/core/inputFormats.ts b/applications/idp-arc/frontend/src/core/inputFormats.ts new file mode 100644 index 0000000..b0cd142 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/inputFormats.ts @@ -0,0 +1,32 @@ +/** + * Which files a protocol can take as input. + * + * Any file is accepted unless the protocol lists the extensions its analysis code actually reads + * (`inputFormats` in protocols.json). There is no size cap here: the upload goes in chunks, so the + * only limit is the space on the workspace's volume. + */ + +export function extensionOf(fileName: string): string { + const dot = fileName.lastIndexOf('.') + return dot === -1 ? '' : fileName.slice(dot).toLowerCase() +} + +/** The protocol's accepted extensions, or null when it takes any file. */ +export function inputFormatsFor(protocolFormats?: string[]): string[] | null { + return protocolFormats?.length ? protocolFormats.map((f) => f.toLowerCase()) : null +} + +/** Why the protocol can't take this file, or null if it can. */ +export function inputFileProblem( + file: { name: string; size: number }, + protocolName: string, + protocolFormats?: string[], +): string | null { + if (file.size === 0) return `${file.name} is empty.` + const formats = inputFormatsFor(protocolFormats) + const ext = extensionOf(file.name) + if (formats && !formats.includes(ext)) { + return `${protocolName} reads ${formats.join(', ')} files; ${file.name} (${ext || 'no extension'}) isn't one of them.` + } + return null +} diff --git a/applications/idp-arc/frontend/src/core/ports/IAuthClient.ts b/applications/idp-arc/frontend/src/core/ports/IAuthClient.ts new file mode 100644 index 0000000..9024cbd --- /dev/null +++ b/applications/idp-arc/frontend/src/core/ports/IAuthClient.ts @@ -0,0 +1,26 @@ +/** + * IAuthClient — abstraction over any authentication provider (Keycloak, Auth0, etc.) + * + * DIP rule: business logic (use-cases) imports ONLY this interface. + * The concrete keycloak-js adapter lives in src/infra/ and is injected at + * the composition root — the use-cases never know which provider is used. + */ +export interface IAuthClient { + /** Initialises the auth provider. Returns true when the user is already authenticated. */ + init(): Promise + + /** Returns the current bearer token, refreshing it if it will expire within `minValidity` seconds. */ + getToken(minValidity?: number): Promise + + /** Returns the IdP login URL so callers can open it in a popup rather than redirecting. */ + getLoginUrl(redirectUri: string): Promise + + /** Redirects the user to the IdP login page. */ + login(): void + + /** Logs the user out and clears the session. */ + logout(): void + + /** Parsed payload of the current token, or null when unauthenticated. */ + readonly tokenParsed: Record | null +} diff --git a/applications/idp-arc/frontend/src/core/ports/IDandiApi.ts b/applications/idp-arc/frontend/src/core/ports/IDandiApi.ts new file mode 100644 index 0000000..77ab635 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/ports/IDandiApi.ts @@ -0,0 +1,56 @@ +/** Abstraction over the OSB endpoints that broker the DANDI upload. */ + +export interface UploadPart { + partNumber: number + url: string +} + +export interface UploadInitResult { + /** Absent when the content was already in DANDI — nothing was uploaded. */ + uploadId?: string + path: string + /** Empty when deduplicated; the client then skips the S3 upload entirely. */ + parts: UploadPart[] + /** Set only on the deduplicated path — the existing blob to attach a new asset to. */ + blobId?: string +} + +export interface UploadedPart { + partNumber: number + size: number + etag: string +} + +export interface UploadFinalizeResult { + assetPath: string + dandisetUrl: string + workspaceId: number + /** Everything the protocol script printed, or why it didn't run. */ + scriptOutput?: string +} + +export interface FinalizeUploadInput { + /** Absent on the deduplicated path — nothing was uploaded, so there is nothing to complete. */ + uploadId?: string + path: string + parts: UploadedPart[] + /** Existing workspace to attach to; omit to have the backend create one. */ + workspaceId?: number + workspaceName?: string + blobId?: string + /** Analysis script to run. The backend fetches it, so it must be reachable from OSB's + * cluster — not a local address. */ + scriptUrl?: string + /** Filename the script should land under in the workspace. */ + scriptName?: string +} + +export interface IDandiApi { + initUpload(token: string, taskId: string, filename: string, size: number, dandiEtag: string): Promise + + /** PUTs one part straight to S3 via its presigned URL. Returns S3's ETag for the part, + * read from the response header — requires the bucket to expose it via CORS. */ + putPart(url: string, blob: Blob): Promise + + finalizeUpload(token: string, input: FinalizeUploadInput): Promise +} diff --git a/applications/idp-arc/frontend/src/core/ports/IObjectStore.ts b/applications/idp-arc/frontend/src/core/ports/IObjectStore.ts new file mode 100644 index 0000000..96a8f73 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/ports/IObjectStore.ts @@ -0,0 +1,25 @@ +/** + * Where a researcher's upload comes to rest before OSB imports it into the workspace (MAABCD–OSB + * Integration design, "Upload port + adapter"). Scenario 1 is a public bucket; Scenario 2 swaps + * in an EMBER-DANDI adapter behind this same port, so the upload flow doesn't change. + */ + +export interface ObjectUploadInput { + file: File + /** The protocol's stable id (protocols.json): groups uploads by protocol. */ + protocolId: string + /** The uploader's Keycloak id (`sub`): groups uploads by contributor. */ + userSub: string +} + +export interface StoredObject { + /** Where OSB's workspace import fetches the file from (`workspaceresource` `origin.path`). */ + url: string + /** The object's name in its store, for messages and logs. */ + key: string +} + +export interface IObjectStore { + /** Stores the file. `onProgress` gets the bytes sent so far, repeatedly while it uploads. */ + put(input: ObjectUploadInput, onProgress?: (sentBytes: number, totalBytes: number) => void): Promise +} diff --git a/applications/idp-arc/frontend/src/core/ports/IWorkspaceApi.ts b/applications/idp-arc/frontend/src/core/ports/IWorkspaceApi.ts new file mode 100644 index 0000000..e7a8bed --- /dev/null +++ b/applications/idp-arc/frontend/src/core/ports/IWorkspaceApi.ts @@ -0,0 +1,71 @@ +import type { Workspace } from '../types' + +/** A resource of a workspace as `GET /workspace/{id}` lists it (only what the run needs). */ +export interface WorkspaceResourceState { + /** -1 for OSB's "Importing resources" placeholder, listed while an import is still running. */ + id: number + name: string + /** `p` pending (being imported), `a` available, `e` error. */ + status?: 'p' | 'a' | 'e' + /** Where it is on the volume, e.g. `idp//repo//notebooks/01_load.ipynb`. */ + path?: string +} + +export interface ImportResourceInput { + workspaceId: number + /** Shown in OSB. */ + name: string + /** Where OSB's import task fetches it from. */ + url: string + /** Volume folder it lands in (a zip is unpacked there), e.g. `idp//data`. */ + folder: string + /** OSB resource type: `e` data, `g` a repository. */ + resourceType: 'e' | 'g' +} + +/** POST /workspace/{id}/run: what to run, how to set it up, and where its files go (all paths relative). */ +export interface StartRunInput { + /** The repository on the volume; `discard` removes it once the run has copied it. */ + repo: { dir: string; discard?: boolean } + /** Set-up paths in the repository; OSB skips the ones it doesn't have. */ + setup?: { requirements?: string; pythonPath?: string[]; install?: string[] } + /** The notebooks to run, relative to repo.dir, in this order. */ + notebooks: string[] + /** The notebooks' INPUT_DIR parameter: the run's input on the volume. */ + inputDir?: string + /** The notebooks' OUTPUT_DIR parameter: where they write their results on the volume. */ + outputDir: string + /** Where the executed notebooks and the log go on the volume. */ + results: { notebooks: string; log: string } + /** The image the notebooks run in: an OSB application's name or an image reference. OSB's JupyterLab without it. */ + image?: string +} + +/** + * IWorkspaceApi — abstraction over the OSB workspace REST API. + * + * DIP rule: use-cases depend on this interface, not on fetch() or any HTTP client. + * The real HTTP implementation lives in src/infra/workspaceApiClient.ts. + * In tests you pass a plain object that satisfies this shape — no vi.mock() needed. + */ +export interface IWorkspaceApi { + /** Fetches the paginated list of workspaces for the current user. */ + listWorkspaces(token: string): Promise + + /** + * Creates a new workspace and returns its numeric id (its volume exists when this returns). + * @param token Bearer token for authorisation. + * @param name Display name for the new workspace. + * @param tags OSB tags, e.g. `maabcd:` so the protocol's workspace can be found again. + */ + createWorkspace(token: string, name: string, tags?: string[]): Promise + + /** The workspace's resources (`GET /workspace/{id}`). */ + getWorkspaceResources(token: string, workspaceId: number): Promise + + /** Imports a URL into the workspace (`POST /workspaceresource`); OSB copies it asynchronously. */ + importResource(token: string, input: ImportResourceInput): Promise + + /** Runs notebooks in the workspace (`POST /workspace/{id}/run`); returns at once. */ + startRun(token: string, workspaceId: number, input: StartRunInput): Promise<{ workflow: string }> +} diff --git a/applications/idp-arc/frontend/src/core/ports/index.ts b/applications/idp-arc/frontend/src/core/ports/index.ts new file mode 100644 index 0000000..1359619 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/ports/index.ts @@ -0,0 +1,5 @@ +export type { IAuthClient } from './IAuthClient' +export type { IWorkspaceApi } from './IWorkspaceApi' +export type { IDandiApi } from './IDandiApi' +export type { IObjectStore, ObjectUploadInput, StoredObject } from './IObjectStore' +export type { ImportResourceInput, StartRunInput, WorkspaceResourceState } from './IWorkspaceApi' diff --git a/applications/idp-arc/frontend/src/core/protocolRepo.ts b/applications/idp-arc/frontend/src/core/protocolRepo.ts new file mode 100644 index 0000000..3310702 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/protocolRepo.ts @@ -0,0 +1,27 @@ +/** + * A protocol's repository, from its `repoZipUrl` in protocols.json (GitHub's codeload archive, as + * the MAABCD–OSB design names it), e.g. + * `https://codeload.github.com/maracbaylis/four-choice-example/zip/refs/heads/main`. + */ +export interface ProtocolRepo { + owner: string + repo: string + /** Branch, tag or commit. */ + ref: string + /** + * The archive's single top-level folder, which OSB's import unpacks the zip into: + * `-`, GitHub dropping a tag's leading `v` (four-choice-example-main). + */ + folder: string +} + +const CODELOAD = /^https:\/\/codeload\.github\.com\/([^/]+)\/([^/]+)\/zip\/(?:refs\/(heads|tags)\/)?(.+)$/ + +/** The repository a `repoZipUrl` points at, or null if it isn't a GitHub codeload archive URL. */ +export function parseRepoZipUrl(url: string | undefined): ProtocolRepo | null { + const m = url ? CODELOAD.exec(url) : null + if (!m) return null + const [, owner, repo, kind, ref] = m + const folderRef = kind === 'tags' && /^v\d/.test(ref) ? ref.slice(1) : ref + return { owner, repo, ref, folder: `${repo}-${folderRef.replace(/\//g, '-')}` } +} diff --git a/applications/idp-arc/frontend/src/core/protocolTemplates.ts b/applications/idp-arc/frontend/src/core/protocolTemplates.ts new file mode 100644 index 0000000..d6e013f --- /dev/null +++ b/applications/idp-arc/frontend/src/core/protocolTemplates.ts @@ -0,0 +1,17 @@ +/** + * Where to download a protocol's templates archive (`templatesZipUrl` in protocols.json). + * + * A relative path points into public/ and is served under the app's base URL; an absolute URL is + * used as-is. Returns null when the protocol has no archive, so its download buttons are disabled. + */ +export function templatesZipHref(protocol: object | undefined, baseUrl: string = import.meta.env.BASE_URL): string | null { + if (!protocol || !('templatesZipUrl' in protocol)) return null + const url = protocol.templatesZipUrl + if (typeof url !== 'string' || !url) return null + return /^[a-z]+:\/\//i.test(url) ? url : `${baseUrl.replace(/\/?$/, '/')}${url.replace(/^\//, '')}` +} + +/** File name the browser saves the archive under (`-templates.zip`). */ +export function templatesZipFileName(protocol: { id: string }): string { + return `${protocol.id}-templates.zip` +} diff --git a/applications/idp-arc/frontend/src/core/runSettings.ts b/applications/idp-arc/frontend/src/core/runSettings.ts new file mode 100644 index 0000000..14a64b3 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/runSettings.ts @@ -0,0 +1,35 @@ +/** + * Every tunable number of a protocol run, in one place. + * + * Used by the run (core/use-cases/runProtocol.ts). Change a value here and nowhere else. + */ +export const RUN_SETTINGS = { + // ─── Uploading and importing ───────────────────────────────────────────────── + + /** At most how often upload progress is reported (the browser reports it many times a second). */ + uploadProgressMs: 250, + + /** How often `GET /workspace/{id}` is polled while OSB's imports run. */ + importPollMs: 5_000, + + /** How long the imports may take (copy + scan for each resource). */ + importTimeoutMs: 10 * 60_000, + + // ─── The run ───────────────────────────────────────────────────────────────── + + /** How often `GET /workspace/{id}` is polled while the run runs, and while its results are checked. */ + runPollMs: 5_000, + + /** Stop watching if OSB hasn't shown the run as running by then (it may already be over). */ + runStartTimeoutMs: 2 * 60_000, + + /** Give up watching after this long. Above the run task's own 1 h deadline (OSB workflow.py). */ + runTimeoutMs: 70 * 60_000, + + /** + * How long after the run's workflow has ended its scan may take to show in the listing (it + * reports through an event queue). Neither notebooks/ nor notebooks.failed/ listed by then, the + * run stopped before any notebook ran. + */ + runResultTimeoutMs: 15_000, +} diff --git a/applications/idp-arc/frontend/src/core/types.ts b/applications/idp-arc/frontend/src/core/types.ts new file mode 100644 index 0000000..2a9e2e4 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/types.ts @@ -0,0 +1,85 @@ +/** + * Core domain types — shared by ports, use-cases, and the UI layer. + * + * Rule: nothing in this file may import from src/infra/, src/app/, or React. + * These types represent the language of the problem domain, not the solution. + */ + +// ─── Auth ───────────────────────────────────────────────────────────────────── + +export type AuthState = 'loading' | 'authenticated' | 'unauthenticated' + +// ─── Workspace ──────────────────────────────────────────────────────────────── + +export interface Workspace { + id: string | number + name: string + description?: string + timestamp_created?: string + thumbnail?: string +} + +// ─── Upload workflow ────────────────────────────────────────────────────────── + +export type UploadPhase = + | 'idle' + | 'hashing' + | 'initializing' + | 'uploading' + | 'registering' + | 'done' + | 'error' + +export interface UploadState { + phase: UploadPhase + message: string + error?: string + workspaceId?: number + scriptOutput?: string +} + +/** Human-readable labels for each upload phase, used by both the use-case and the UI. */ +export const PHASE_LABELS: Record = { + idle: '', + hashing: '1 / 4 — Computing checksum…', + initializing: '2 / 4 — Preparing upload…', + uploading: '3 / 4 — Uploading file…', + // finalize also spawns the workspace and runs the protocol script synchronously — hence "can + // take a few minutes" (see jupyter_kernel_client.py in OSBv2's workspaces app). + registering: '4 / 4 — Registering asset, creating workspace & running script (can take a few minutes)…', + done: 'Done!', + error: 'Error', +} + +// ─── Protocol run (upload, OSB imports, notebooks in an Argo task) ─────────── + +export type RunPhase = + | 'uploading' + | 'workspace' + | 'importing' + | 'running' + /** Still running in the workspace past the watch limit: no longer followed here. */ + | 'stillRunning' + | 'succeeded' + | 'failed' + +export type StepState = 'pending' | 'running' | 'succeeded' | 'skipped' | 'failed' + +/** One line of the run's checklist in the UI. */ +export interface RunStep { + id: string + label: string + state: StepState + detail?: string +} + +export interface RunState { + phase: RunPhase + message: string + /** Every step of the run, in order, for the checklist. */ + steps: RunStep[] + workspaceId?: number + /** This run's results folder, relative to the workspace root. */ + outputsDir?: string + error?: string +} diff --git a/applications/idp-arc/frontend/src/core/use-cases/createAndUploadToDandi.ts b/applications/idp-arc/frontend/src/core/use-cases/createAndUploadToDandi.ts new file mode 100644 index 0000000..a4c7afb --- /dev/null +++ b/applications/idp-arc/frontend/src/core/use-cases/createAndUploadToDandi.ts @@ -0,0 +1,105 @@ +import type { IAuthClient } from '../ports/IAuthClient' +import type { IDandiApi } from '../ports/IDandiApi' +import type { UploadState } from '../types' +import { PHASE_LABELS } from '../types' +import { computeDandiEtag } from '../dandiEtag' + +export type OnProgress = (state: UploadState) => void + +export interface CreateAndUploadToDandiInput { + taskId: string + file: File + /** Existing workspace to attach the asset to; omit to create a new one. */ + workspaceId?: number + workspaceName?: string + /** The selected protocol's analysis script, resolved from protocols.json by the caller. + * Delivered into the workspace alongside the data so it is ready to run. */ + scriptUrl?: string + scriptName?: string +} + +/** + * Uploads a file to DANDI and attaches it to an OSB workspace. + * + * 1. Hash the file in the browser into a dandi-etag — the content digest DANDI identifies a + * blob by. It has to be sent up front, before any bytes move, because DANDI answers with + * "already have this" and skips the upload entirely when the digest matches an existing blob. + * 2. POST /dandi/upload/init — the backend calls DANDI with the admin key and passes back one + * presigned S3 URL per part. Each URL carries its own signature and expiry, which is what + * lets the browser write to a bucket it has no credentials for. + * 3. PUT each part's bytes to its presigned URL — a plain unauthenticated PUT, browser straight + * to S3. The file never passes through OSB, so size costs the server nothing. + * 4. POST /dandi/upload/finalize — registers the asset, attaches the workspace, runs the script + */ +export function createCreateAndUploadToDandiUseCase( + auth: Pick, + dandiApi: IDandiApi, +) { + return async function createAndUploadToDandi( + input: CreateAndUploadToDandiInput, + onProgress: OnProgress, + abortRef: { current: boolean }, + ): Promise { + const { taskId, file, workspaceId, workspaceName, scriptUrl, scriptName } = input + + try { + // ── Step 1: hash the file into DANDI's content digest ───────────────── + onProgress({ phase: 'hashing', message: PHASE_LABELS.hashing }) + const { etag, parts: partPlan } = await computeDandiEtag(file) + if (abortRef.current) return null + + // ── Step 2: initialize ──────────────────────────────────────────────── + onProgress({ phase: 'initializing', message: PHASE_LABELS.initializing }) + const initToken = await auth.getToken(30) + const init = await dandiApi.initUpload(initToken, taskId, file.name, file.size, etag) + if (abortRef.current) return null + + // ── Step 3: PUT each part to its presigned URL ──────────────────────── + // init returns no parts when DANDI already has this exact content — just a blob_id to + // attach a new asset to, so there is nothing to upload. + const uploadedParts = [] + if (init.parts.length > 0) { + onProgress({ phase: 'uploading', message: PHASE_LABELS.uploading }) + for (const part of init.parts) { + if (abortRef.current) return null + const plan = partPlan.find((p) => p.partNumber === part.partNumber) + if (!plan) throw new Error(`No local part plan for part ${part.partNumber}`) + const blob = file.slice(plan.start, plan.end) + const s3Etag = await dandiApi.putPart(part.url, blob) + uploadedParts.push({ partNumber: part.partNumber, size: plan.size, etag: s3Etag }) + } + } else { + onProgress({ phase: 'uploading', message: 'Already in DANDI — skipping upload…' }) + } + if (abortRef.current) return null + + // finalize also spawns the workspace and runs the script, so it can block for minutes. + // The token is used at the very end of that too, so it must outlive the whole call — + // hence 600s rather than the usual short floor. + onProgress({ phase: 'registering', message: PHASE_LABELS.registering }) + const finalizeToken = await auth.getToken(600) + const result = await dandiApi.finalizeUpload(finalizeToken, { + uploadId: init.uploadId, + path: init.path, + parts: uploadedParts, + workspaceId, + workspaceName, + blobId: init.blobId, + scriptUrl, + scriptName, + }) + + onProgress({ + phase: 'done', + message: PHASE_LABELS.done, + workspaceId: result.workspaceId, + scriptOutput: result.scriptOutput, + }) + return result.workspaceId + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err) + onProgress({ phase: 'error', message: PHASE_LABELS.error, error: msg }) + return null + } + } +} diff --git a/applications/idp-arc/frontend/src/core/use-cases/createWorkspace.ts b/applications/idp-arc/frontend/src/core/use-cases/createWorkspace.ts new file mode 100644 index 0000000..29af84b --- /dev/null +++ b/applications/idp-arc/frontend/src/core/use-cases/createWorkspace.ts @@ -0,0 +1,13 @@ +import type { IAuthClient } from '../ports/IAuthClient' +import type { IWorkspaceApi } from '../ports/IWorkspaceApi' + +/** Creates a new, empty workspace with the given name and returns its id. */ +export function createCreateWorkspaceUseCase( + auth: Pick, + workspaceApi: Pick, +) { + return async function createWorkspace(name: string): Promise { + const token = await auth.getToken(30) + return workspaceApi.createWorkspace(token, name) + } +} diff --git a/applications/idp-arc/frontend/src/core/use-cases/index.ts b/applications/idp-arc/frontend/src/core/use-cases/index.ts new file mode 100644 index 0000000..46d82b6 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/use-cases/index.ts @@ -0,0 +1 @@ +export { createLoadWorkspacesUseCase } from './loadWorkspaces' diff --git a/applications/idp-arc/frontend/src/core/use-cases/loadWorkspaces.ts b/applications/idp-arc/frontend/src/core/use-cases/loadWorkspaces.ts new file mode 100644 index 0000000..a39e5cb --- /dev/null +++ b/applications/idp-arc/frontend/src/core/use-cases/loadWorkspaces.ts @@ -0,0 +1,25 @@ +import type { IAuthClient } from '../ports/IAuthClient' +import type { IWorkspaceApi } from '../ports/IWorkspaceApi' +import type { Workspace } from '../types' + +/** + * loadWorkspaces use-case + * + * Orchestrates: refresh token → fetch workspaces list. + * + * DI: all dependencies are injected — no direct imports of fetch or keycloak. + * Testing: pass fake { getToken, listWorkspaces } plain objects; no mocking framework needed. + * + * @example + * const load = createLoadWorkspacesUseCase(authClient, workspaceApi) + * const workspaces = await load() + */ +export function createLoadWorkspacesUseCase( + auth: Pick, + api: Pick, +) { + return async function loadWorkspaces(): Promise { + const token = await auth.getToken(30) + return api.listWorkspaces(token) + } +} diff --git a/applications/idp-arc/frontend/src/core/use-cases/runProgress.ts b/applications/idp-arc/frontend/src/core/use-cases/runProgress.ts new file mode 100644 index 0000000..8a94eb0 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/use-cases/runProgress.ts @@ -0,0 +1,128 @@ +import type { RunPhase, RunState, RunStep, StepState } from '../types' +import { UserFacingError, userMessage } from '../userMessages' + +export type OnRunState = (state: RunState) => void + +/** A failure that belongs to one step, so only that row is marked failed. */ +export class StepError extends Error { + readonly step: string + readonly original: unknown + constructor(step: string, cause: unknown) { + super(cause instanceof Error ? cause.message : String(cause)) + this.step = step + this.original = cause + } +} + +/** + * The run's checklist: one row per step, reported in full after every change. + * Each method updates a row and reports, so the run itself reads as a list of steps. + */ +export class RunProgress { + private steps: RunStep[] + private phase: RunPhase = 'uploading' + private readonly report: OnRunState + /** Set once the run has failed or finished: nothing reports over the outcome after that. */ + private ended = false + workspaceId?: number + outputsDir?: string + + constructor(report: OnRunState, steps: { id: string; label: string }[], workspaceId?: number) { + this.report = report + this.steps = steps.map((s) => ({ ...s, state: 'pending' })) + this.workspaceId = workspaceId + } + + /** A step starts; `phase` is what the whole run is doing now. */ + start(id: string, phase: RunPhase, message: string, detail?: string) { + if (this.ended) return + this.phase = phase + this.set(id, 'running', detail) + this.emit(message) + } + + /** A running step reports progress (upload bytes, run state). */ + update(id: string, detail: string, message: string) { + if (this.ended) return + this.set(id, 'running', detail) + this.emit(message) + } + + done(id: string, detail?: string, message?: string) { + if (this.ended) return + this.set(id, 'succeeded', detail) + if (message) this.emit(message) + } + + skip(id: string, detail?: string) { + this.set(id, 'skipped', detail) + } + + /** The run ends well. */ + finish(message: string) { + if (this.ended) return + this.ended = true + this.phase = 'succeeded' + this.emit(message) + } + + /** Still running past the watch limit: stops following it; the run carries on in the workspace. */ + stopWatching(id: string, message: string) { + if (this.ended) return + this.ended = true + this.phase = 'stillRunning' + this.steps = this.steps.map((s) => (s.id === id ? { ...s, state: 'pending', detail: 'Still running in the workspace' } : s)) + this.emit(message) + } + + /** + * Marks the step that failed (a StepError's, else the one running) and reports the error in + * words for the user; the technical error goes to the console. + */ + fail(err: unknown) { + if (this.ended) return + this.ended = true + const failed = err instanceof StepError ? err.step : this.steps.find((s) => s.state === 'running')?.id + const cause = err instanceof StepError ? err.original : err + console.error(cause instanceof UserFacingError && cause.details ? cause.details : cause) + this.steps = this.steps.map((s) => + s.id === failed ? { ...s, state: 'failed' } : s.state === 'running' ? { ...s, state: 'pending', detail: undefined } : s) + this.phase = 'failed' + this.emit('The analysis did not complete', userMessage(cause, failed)) + } + + emit(message: string, error?: string) { + const { phase, steps, workspaceId, outputsDir } = this + this.report({ phase, message, steps, workspaceId, outputsDir, ...(error ? { error } : {}) }) + } + + private set(id: string, state: StepState, detail?: string) { + // A running step keeps its last detail unless given a new one. + this.steps = this.steps.map((s) => + s.id === id ? { ...s, state, detail: detail ?? (state === 'running' ? s.detail : undefined) } : s) + } +} + +/** Runs one step, so a failure is attributed to that step's row (not to another one running in + * parallel, like the upload). */ +export async function inStep(step: string, run: () => Promise): Promise { + try { + return await run() + } catch (err) { + throw err instanceof StepError ? err : new StepError(step, err) + } +} + +/** Calls `check` until it returns true, every `everyMs`. Resolves to how it ended. */ +export async function pollUntil( + check: () => Promise, + { everyMs, timeoutMs, stopped }: { everyMs: number; timeoutMs: number; stopped: () => boolean }, +): Promise<'done' | 'timeout' | 'stopped'> { + const deadline = Date.now() + timeoutMs + for (;;) { + if (stopped()) return 'stopped' + if (await check()) return 'done' + if (Date.now() > deadline) return 'timeout' + await new Promise((resolve) => setTimeout(resolve, everyMs)) + } +} diff --git a/applications/idp-arc/frontend/src/core/use-cases/runProtocol.ts b/applications/idp-arc/frontend/src/core/use-cases/runProtocol.ts new file mode 100644 index 0000000..2823026 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/use-cases/runProtocol.ts @@ -0,0 +1,294 @@ +import type { IAuthClient } from '../ports/IAuthClient' +import type { IObjectStore, StoredObject } from '../ports/IObjectStore' +import type { IWorkspaceApi, WorkspaceResourceState } from '../ports/IWorkspaceApi' +import { formatBytes } from '../formatBytes' +import { inputFileProblem } from '../inputFormats' +import { parseRepoZipUrl } from '../protocolRepo' +import { RUN_SETTINGS } from '../runSettings' +import { UserFacingError } from '../userMessages' +import { workspaceLayout } from '../workspaceLayout' +import { inStep, pollUntil, RunProgress, type OnRunState } from './runProgress' + +export type { OnRunState } from './runProgress' + +/** The protocols.json fields a run needs. */ +export interface RunProtocolDefinition { + /** Stable id: tags the workspace, names the upload's prefix and the results folder. */ + id: string + name: string + /** GitHub codeload zip of the protocol's repository. */ + repoZipUrl?: string + /** Repository-relative folder of the notebooks. */ + notebooksDir?: string + /** Overrides REPOSITORY_SETUP's requirements file, PYTHONPATH folders and install candidates. */ + requirements?: string + pythonPath?: string[] + install?: string[] + inputFormats?: string[] + /** The image the notebooks run in (their environment): an OSB application's name (e.g. `netpyne`) + * or an image reference from a registry OSB allows. Without it, OSB's default (JupyterLab). */ + image?: string +} + +export interface RunProtocolInput { + protocol: RunProtocolDefinition + /** The researcher's data; without it the notebooks run on the repository's example data. */ + file?: File | null + /** Existing workspace to use; a new one, tagged with the protocol, is created when undefined. */ + workspaceId?: number + /** Name for the workspace created when `workspaceId` is undefined. */ + workspaceName: string +} + +/** + * How a protocol repository sets up its environment, as agreed with the protocol authors (30 Sep): + * a requirements file at the root, `scripts/` on PYTHONPATH, and `scripts/` installed by its + * install.py, else as a package. OSB applies whichever of these the repository has; a protocol can + * override each in protocols.json. + */ +const REPOSITORY_SETUP = { + requirements: 'requirements.txt', + pythonPath: ['scripts'], + /** Candidates; OSB uses the first the repository has. */ + install: ['scripts/install.py', 'scripts/setup.py', 'scripts/pyproject.toml'], +} + +const STEPS = [ + { id: 'upload', label: 'Upload your file' }, + { id: 'workspace', label: 'Get the workspace ready' }, + { id: 'repo', label: 'Import the analysis code' }, + { id: 'data', label: 'Import your data' }, + { id: 'imports', label: 'Wait for the imports to finish' }, + { id: 'run', label: 'Run the notebooks' }, +] + +/** + * Runs a protocol on the researcher's data, per the MAABCD–OSB design (Scenario 1). Before anything + * moves, the protocol and the file are checked; then, one step per entry in STEPS: + * + * 1. Upload: browser → public bucket (IObjectStore); its URL is what OSB imports. It goes first + * (the design runs it in parallel with the workspace side): nothing is created in OSB until + * the file is safely in the bucket, so a failed upload leaves no empty workspace. + * 2. Workspace: the selected one, or a new one tagged `maabcd:`. + * 3. Import the repository and + * 4. import the data, both through OSB (`POST /workspaceresource`) into this run's own folder + * (workspaceLayout): the code fresh for every run, the upload in its data/ (a zip is unpacked + * there). + * 5. Wait for the imports: done when `GET /workspace/{id}` lists no pending resource in this + * run's folder and no "Importing resources" placeholder. (Not "every resource is `a`": OSB's scan only indexes + * .nwb/.npjson/.ipynb, and drops other resources once copied.) They worked if the scan then + * lists notebooks in this run's notebooks folder; the data file isn't listed, so it can't be + * checked this way. Those notebooks, in byte order of their names, are what step 6 runs. + * 6. Run the notebooks in OSB's Argo task (`POST /workspace/{id}/run`): IDP says which, in what + * order, where the input goes, how to set up the environment (REPOSITORY_SETUP) and which + * folders are the results; OSB only carries it out. The results go to the run's folder, and + * the run task removes the code once it has copied it (discard_repo). It is followed, like the + * imports, through `GET /workspace/{id}`'s placeholder: that shows when it ends. Whether it + * succeeded is in which folder its executed notebooks are then listed. + * + * No lab server is started: the imports and the run are OSB's own Argo workflows, and their + * affinity to the workspace is met by their own pods (verified 5 Oct on a never-opened workspace). + */ +export function createRunProtocolUseCase( + auth: Pick, + workspaceApi: IWorkspaceApi, + objectStore: IObjectStore, +) { + const token = (minValidity = 30) => auth.getToken(minValidity) + + return async function runProtocol(input: RunProtocolInput, report: OnRunState, abortRef?: { current: boolean }): Promise { + const { protocol, file } = input + const progress = new RunProgress(report, STEPS, input.workspaceId) + const stopped = () => !!abortRef?.current + + try { + // Before anything moves: the protocol and the file. + const { repo, userId } = checkBeforeStarting(input) + // This run's folder in the workspace, and where the repository lands in it (the zip + // unpacks to -/). + const layout = workspaceLayout(protocol.id, new Date()) + const repoDir = `${layout.run}/${repo.folder}` + if (!file) { + progress.skip('upload') + progress.skip('data') + } + progress.emit('Starting…') + + // 1. Upload the file straight to the bucket. Nothing else starts until it has landed. + const stored = file ? await inStep('upload', () => uploadFile(file, userId)) : null + if (stopped()) return + + // 2. Get the workspace: the selected one, or a new one. + const workspaceId = await inStep('workspace', prepareWorkspace) + if (stopped()) return + + // 3. Import the analysis code (the protocol's repository). + await inStep('repo', () => importRepo(workspaceId, repo, layout.run)) + + // 4. Import the uploaded data. + if (stored && file) await inStep('data', () => importData(workspaceId, file, stored, layout.inputs)) + + // 5. Wait for the imports, and find the notebooks to run. + if (stopped()) return + const notebooks = await inStep('imports', () => waitForImports(workspaceId, layout.run, repoDir)) + if (!notebooks) return + + // 6. Run the notebooks. + await inStep('run', () => runNotebooks(workspaceId, repoDir, notebooks, layout, !!stored)) + } catch (err) { + progress.fail(err) + } + + // ── The steps ──────────────────────────────────────────────────────────────────────── + + /** Before anything moves: a file the protocol can't read would only fail minutes later. */ + function checkBeforeStarting({ protocol, file }: RunProtocolInput): { repo: { folder: string }; userId: string } { + const folder = parseRepoZipUrl(protocol.repoZipUrl)?.folder + if (!protocol.repoZipUrl || !folder || !protocol.notebooksDir) { + throw new UserFacingError(`The analysis for "${protocol.name}" isn't available yet.`) + } + const repo = { folder } + const fileProblem = file && inputFileProblem(file, protocol.name, protocol.inputFormats) + if (fileProblem) throw new UserFacingError(fileProblem) + const userId = auth.tokenParsed?.sub + if (typeof userId !== 'string') throw new UserFacingError('Not signed in; please sign in again') + return { repo, userId } + } + + /** Step 1. */ + async function uploadFile(file: File, userId: string): Promise { + const message = `Uploading ${file.name}…` + progress.start('upload', 'uploading', message, `0 B of ${formatBytes(file.size)}`) + let lastReport = 0 + const stored = await objectStore.put({ file, protocolId: protocol.id, userSub: userId }, (sent, total) => { + if (Date.now() - lastReport < RUN_SETTINGS.uploadProgressMs && sent < total) return + lastReport = Date.now() + const pct = total ? Math.round((sent / total) * 100) : 100 + progress.update('upload', `${formatBytes(sent)} of ${formatBytes(total)} (${pct}%)`, message) + }) + progress.done('upload', undefined, 'Upload finished') + return stored + } + + /** Step 2. */ + async function prepareWorkspace(): Promise { + progress.start('workspace', 'workspace', 'Getting the workspace ready…') + if (input.workspaceId !== undefined) { + progress.done('workspace', undefined, 'Workspace ready') + return input.workspaceId + } + const id = await workspaceApi.createWorkspace(await token(), input.workspaceName, [`maabcd:${protocol.id}`]) + progress.workspaceId = id + progress.done('workspace', undefined, 'Workspace ready') + return id + } + + /** Step 3. */ + async function importRepo(workspaceId: number, repo: { folder: string }, folder: string) { + progress.start('repo', 'importing', 'Importing the analysis code…') + await workspaceApi.importResource(await token(), { + workspaceId, name: repo.folder, url: protocol.repoZipUrl!, folder, resourceType: 'g', + }) + progress.done('repo') + } + + /** Step 4. */ + async function importData(workspaceId: number, file: File, stored: StoredObject, folder: string) { + progress.start('data', 'importing', 'Importing your data…') + await workspaceApi.importResource(await token(), { + workspaceId, name: file.name, url: stored.url, folder, resourceType: 'e', + }) + progress.done('data') + } + + /** + * Step 5. Returns the notebooks to run, relative to the repository, in byte order of their + * names (as the contract says: zero-padded prefixes); null if the dialog was closed meanwhile. + */ + async function waitForImports(workspaceId: number, runDir: string, repoDir: string): Promise { + progress.start('imports', 'importing', 'Waiting for the imports to finish…') + let resources: WorkspaceResourceState[] = [] + // Only this run's imports count: a reused workspace can hold older resources that failed or + // never finished, and those must not fail or stall every later run. + const ours = (r: WorkspaceResourceState) => !!r.path?.startsWith(`${runDir}/`) + const ended = await pollUntil(async () => { + resources = await workspaceApi.getWorkspaceResources(await token(), workspaceId) + const failed = resources.find((r) => ours(r) && r.status === 'e') + if (failed) throw new Error(`OSB could not import ${failed.name}`) + return !resources.some((r) => r.id === -1 || (ours(r) && r.status === 'p')) + }, { everyMs: RUN_SETTINGS.importPollMs, timeoutMs: RUN_SETTINGS.importTimeoutMs, stopped }) + if (ended === 'timeout') throw new UserFacingError('Copying the files into the workspace is taking too long. Please try again later.', + `The imports did not finish within ${RUN_SETTINGS.importTimeoutMs / 60_000} minutes`) + if (ended !== 'done') return null + // The visible notebooks directly in the notebooks folder, relative to the repository. + const folderInRepo = `${protocol.notebooksDir}/` + const notebooks = resources + .map((r) => (r.path?.startsWith(`${repoDir}/`) ? r.path.slice(repoDir.length + 1) : '')) + .filter((path) => { + const name = path.slice(folderInRepo.length) + return path.startsWith(folderInRepo) && name.endsWith('.ipynb') && !name.includes('/') && !name.startsWith('.') + }) + .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) + if (!notebooks.length) throw new UserFacingError('The analysis code has no notebooks to run. Please let the protocol\'s maintainers know.', + `No notebooks in ${repoDir}/${folderInRepo} after the import`) + progress.done('imports') + return notebooks + } + + /** Step 6. */ + async function runNotebooks(workspaceId: number, repoDir: string, notebooks: string[], + layout: ReturnType, hasData: boolean) { + progress.start('run', 'running', 'Starting the notebooks…', 'Submitting') + await workspaceApi.startRun(await token(), workspaceId, { + // Removed once copied: every run gets the repository as it is now. + repo: { dir: repoDir, discard: true }, + setup: { + requirements: protocol.requirements ?? REPOSITORY_SETUP.requirements, + pythonPath: protocol.pythonPath ?? REPOSITORY_SETUP.pythonPath, + install: protocol.install ?? REPOSITORY_SETUP.install, + }, + notebooks, + // The notebooks' INPUT_DIR and OUTPUT_DIR parameters: they read the upload and write their + // results in the run's folder. Without an upload they use their own example data. + inputDir: hasData ? layout.inputs : undefined, + outputDir: layout.outputs, + results: { notebooks: layout.notebooks, log: layout.log }, + ...(protocol.image ? { image: protocol.image } : {}), + }) + progress.outputsDir = layout.run + // Followed through `GET /workspace/{id}`, as the imports are: while a workflow for the + // workspace runs, OSB lists a placeholder resource (id -1). Seen, then gone: the run is over. + let seen = false + const submitted = Date.now() + const ended = await pollUntil(async () => { + const running = (await workspaceApi.getWorkspaceResources(await token(), workspaceId)).some((r) => r.id === -1) + if (running && !seen) progress.update('run', 'Running the notebooks', 'Running the notebooks…') + seen ||= running + // Never seen: a run that ends within seconds can finish between two polls. + return seen ? !running : Date.now() - submitted > RUN_SETTINGS.runStartTimeoutMs + }, { everyMs: RUN_SETTINGS.runPollMs, timeoutMs: RUN_SETTINGS.runTimeoutMs, stopped }) + if (ended === 'timeout') progress.stopWatching('run', 'Still running in the workspace; open it to follow the results') + if (ended !== 'done') return + + // Whether it succeeded. The run task leaves the executed notebooks in notebooks/ if every one + // passed, else in notebooks.failed/ (the failed one last), and its workflow ends with a scan + // either way, so one of the two is listed once the placeholder is gone (after a short lag: + // the scan reports through an event queue). + const executed = notebooks.map((nb) => `${layout.notebooks}/${nb.split('/').pop()}`) + const failedDir = `${layout.failedNotebooks}/` + let failedAt: string | undefined + const listed = await pollUntil(async () => { + const paths = (await workspaceApi.getWorkspaceResources(await token(), workspaceId)).flatMap((r) => r.path ?? []) + failedAt = paths.filter((path) => path.startsWith(failedDir)).sort().pop() + return !!failedAt || executed.every((path) => paths.includes(path)) + }, { everyMs: RUN_SETTINGS.runPollMs, timeoutMs: RUN_SETTINGS.runResultTimeoutMs, stopped }) + if (listed === 'stopped') return + // The user reads the run step's plain sentence; which notebook failed goes to the console. + if (failedAt) throw new Error(`${failedAt} failed; see ${layout.log}`) + // Neither: it stopped before any notebook ran (e.g. installing the requirements). + if (listed === 'timeout') throw new Error(`The run ended with no executed notebooks in ${layout.notebooks}/ or ${failedDir}; see ${layout.log}`) + progress.done('run', `Results in ${layout.run}/`) + progress.finish('The analysis has finished') + } + } +} diff --git a/applications/idp-arc/frontend/src/core/userMessages.ts b/applications/idp-arc/frontend/src/core/userMessages.ts new file mode 100644 index 0000000..e093f31 --- /dev/null +++ b/applications/idp-arc/frontend/src/core/userMessages.ts @@ -0,0 +1,36 @@ +/** + * What the user reads when a run fails: one plain sentence per step. Technical details (HTTP + * errors, OSB's messages) go to the browser console; the run's own log is in its results folder. + */ + +/** An error whose message is written for the user; `details` is for the console. */ +export class UserFacingError extends Error { + readonly details?: string + constructor(message: string, details?: string) { + super(message) + this.details = details + } +} + +const STEP_FAILED: Record = { + upload: 'Your file could not be uploaded. Check your connection and try again.', + workspace: 'The workspace could not be prepared. Please try again.', + repo: 'The analysis code could not be added to the workspace. Please try again.', + data: 'Your file could not be added to the workspace. Please try again.', + imports: 'The files could not be copied into the workspace. Please try again.', + run: 'The analysis did not finish. Open the workspace to see what happened.', +} + +/** Says the session has ended; the dialog sends the user to sign in when the text contains it. */ +export const SIGN_IN_AGAIN = 'sign in again' + +/** The user's text for an error thrown during `step`. */ +export function userMessage(err: unknown, step?: string): string { + if (err instanceof UserFacingError) return err.message + // The auth client's "Please sign in again." can come from any step that asks for a token: kept, + // so the dialog can send the user to sign in instead of suggesting a retry that fails the same way. + if (err instanceof Error && err.message.toLowerCase().includes(SIGN_IN_AGAIN)) { + return `Your session has expired. Please ${SIGN_IN_AGAIN}.` + } + return (step && STEP_FAILED[step]) || 'Something went wrong. Please try again.' +} diff --git a/applications/idp-arc/frontend/src/core/workspaceLayout.ts b/applications/idp-arc/frontend/src/core/workspaceLayout.ts new file mode 100644 index 0000000..f3c206a --- /dev/null +++ b/applications/idp-arc/frontend/src/core/workspaceLayout.ts @@ -0,0 +1,37 @@ +/** + * Where a run's files go in the workspace: the only place these folder names are set. OSB uses the + * paths it is given. + * + * /run--/ + * -/ the protocol's code, imported for this run and removed by the run task + * inputs/ the upload; the notebooks' INPUT_DIR + * outputs/ what the notebooks write; their OUTPUT_DIR + * notebooks/ the executed notebooks, only if all passed ┐ + * (else notebooks.failed/) │ written by the run task + * run.log ┘ + */ + +/** The upload; passed to the notebooks as INPUT_DIR. */ +const INPUTS_DIR = 'inputs' +/** What the notebooks write; passed to them as OUTPUT_DIR. */ +const OUTPUTS_DIR = 'outputs' +/** The executed notebooks: the run task leaves them here only if every notebook passed. */ +const NOTEBOOKS_DIR = 'notebooks' +/** Assumed from OSB's run task (run.sh): a failed run's executed notebooks are in `.failed`. */ +const FAILED_SUFFIX = '.failed' +/** The run task's log. */ +const LOG_FILE = 'run.log' + +export function workspaceLayout(protocolId: string, startedAt: Date) { + // No `:` (not allowed in file names on Windows or in macOS Finder); sorts in run order. + const stamp = startedAt.toISOString().replace(/\.\d+Z$/, 'Z').replace(/:/g, '-') + const run = `${protocolId}/run-${protocolId}-${stamp}` + return { + run, + inputs: `${run}/${INPUTS_DIR}`, + outputs: `${run}/${OUTPUTS_DIR}`, + notebooks: `${run}/${NOTEBOOKS_DIR}`, + failedNotebooks: `${run}/${NOTEBOOKS_DIR}${FAILED_SUFFIX}`, + log: `${run}/${LOG_FILE}`, + } +} diff --git a/applications/idp-arc/frontend/src/data/README.md b/applications/idp-arc/frontend/src/data/README.md new file mode 100644 index 0000000..700a2e7 --- /dev/null +++ b/applications/idp-arc/frontend/src/data/README.md @@ -0,0 +1,102 @@ +# Protocols (`protocols.json`) + +One entry per behavioral task / protocol. It drives the Protocols page and the upload dialog, and +it tells IDP which analysis to run on a researcher's data and what input that analysis can read. +Field names follow the MAABCD–OSB Integration design (`id`, `repoZipUrl`, `notebooksDir`). + +## Fields + +| Field | Used by | Meaning | +|---|---|---| +| `id` | run | **Stable id, never derived from the name** (`four-choice-reversal`). Tags the workspace (`maabcd:`), prefixes the upload in the bucket and names the workspace folder its runs go in (`/run--/`). Don't change it once uploads exist. | +| `name` | everywhere | Display name. Free to edit. | +| `description`, `imageUrl`, `videoUrl` | Protocols page | Text and media shown for the protocol. | +| `repoZipUrl` | run | GitHub archive of the analysis repository, `https://codeload.github.com///zip/refs/heads/` (or `refs/tags/`, or `/zip/`). A **branch** follows the latest fixes; a **commit** freezes the code. An empty `repoZipUrl` = no analysis yet: the upload dialog doesn't offer the protocol. | +| `notebooksDir` | run | Repository-relative folder of the notebooks (`notebooks`). | +| `requirements`, `pythonPath`, `install` | run | Override the repository setup below (defaults in `REPOSITORY_SETUP`, runProtocol.ts: `requirements.txt`; `["scripts"]`; `["scripts/install.py", "scripts/setup.py", "scripts/pyproject.toml"]`). OSB applies only the ones the repository has. Leave them out for a repository that follows the contract. | +| `templatesZipUrl` | Protocols page, dialog | Archive of the protocol's templates (data sheets, instructions), downloaded by the **Download** buttons on the Protocols page and in the dialog's upload step. A path relative to `public/` (`protocols_archives/4c.zip`) or an absolute URL. Missing or empty = both buttons disabled. | +| `inputFormats` | dialog, run | Extensions the analysis code can actually read. The dialog refuses other files before anything starts. **Without it, any file is accepted** (except an empty one, always refused). | + +## What happens on "Upload and run" (core/use-cases/runProtocol.ts) + +The button stays disabled until a file is chosen, and while that file is empty or of a format the +protocol doesn't read. + +1. The file goes from the browser straight to the bucket (`VITE_UPLOAD_BUCKET_URL`), as + `uploads////`. Once it has landed: the workspace (the selected one, or + a new one tagged `maabcd:`), then the repository import. No lab server is started; OSB's + import and run tasks don't need one. +2. OSB imports both into this run's folder (`POST /workspaceresource`; names in + core/workspaceLayout.ts): `/run--/-/` for the code, fresh for + every run, and `…/inputs/` for the upload (a `.zip` is unpacked there). +3. When the imports are done, IDP lists the notebooks OSB found in `notebooksDir` and sorts them. + It then asks OSB to run exactly those, in that order (`POST /workspace/{id}/run`, papermill in + an Argo task), with the run's `inputs/` and `outputs/` as the notebooks' `INPUT_DIR` and + `OUTPUT_DIR` parameters. OSB sets up `requirements.txt` and `scripts/` first; which notebooks run + and where they read and write is IDP's decision. +4. Results, in the same run folder (e.g. `four-choice-reversal/run-four-choice-reversal-2026-10-05T10-54-43Z/`): + `inputs/` (the upload), `outputs/` (what the notebooks wrote), the executed notebooks and `run.log`. The + run task removes the code once it has copied it, so the next run gets the current code. +5. **Success or failure.** The run task writes the executed notebooks to `notebooks.running/` and + renames that folder once, at the end: to `notebooks/` if every notebook passed, to + `notebooks.failed/` if one failed (it holds the ones that ran, the failed one last; later ones + never ran). So `notebooks/` exists only after a fully successful run. The run's workflow ends + with OSB's scan whatever the outcome, so once OSB's placeholder (id -1) is gone, the listing + says how it went: every executed notebook in `notebooks/` = success; anything in + `notebooks.failed/` = failure; neither + (re-checked for 15 s, as the scan reports through an event queue) = it stopped before any + notebook ran (e.g. installing the requirements). The cause is in `run.log`. + `outputs/` is written either way, so after a failure it may hold partial results. + +## What a protocol repository must look like + +Agreed with the protocol authors on 30 Sep 2026. These conventions live in IDP (`REPOSITORY_SETUP` +and `notebooksDir`): IDP tells OSB which notebooks to run, in what order, and which setup files to +use; OSB uses the ones the repository has and assumes nothing else. + +1. `requirements.txt` at the root — **optional**, installed first. Pin versions: the task image + pre-installs pandas, numpy, openpyxl and matplotlib, and an unpinned requirement is satisfied by + whatever version that is (four-choice broke on matplotlib 3.11, which removed `boxplot(labels=)`). +2. `scripts/` — **optional**: `scripts/install.py` is run if present, else it's pip-installed if it + is a package (`setup.py`/`pyproject.toml`); it's always on `PYTHONPATH`. A failure here doesn't + stop the run. +3. `notebooks/` — **required**. Every visible `*.ipynb` directly in it runs, one after another, in + byte order of the file name (**zero-pad the numbers**: `01_…`, `02_…`; `10_x` sorts before + `2_x`), each with its own folder as working directory. The first failing notebook stops the run. +4. **Input and output: a cell tagged `parameters`** (papermill's convention) in every notebook, setting + `INPUT_DIR` and `OUTPUT_DIR` to its own defaults (e.g. its example data). The notebooks read from + `INPUT_DIR` and write everything they produce to `OUTPUT_DIR`; for a run, IDP passes the run's + `inputs/` and `outputs/`. A notebook without that cell isn't run. + +## Per protocol + +### Four-choice reversal digging task — `maracbaylis/four-choice-example` + +- **Accepted: `.xlsx`, `.zip`.** + - `.xlsx`: one filled **ARC Four Choice TABS workbook** (one animal, 6–8 sheets). Google Sheets: + *File → Download → Microsoft Excel (.xlsx)*. + - `.zip`: several such workbooks (e.g. one per animal), analysed together; files at the top level + or inside one folder (what Finder/Explorer "Compress" makes). +- **Not accepted: `.csv`, `.tsv`, `.xls`, `.ods`**: the pipeline only reads `*.xlsx` with openpyxl. +- **Needs a `parameters` cell (not there yet):** its notebooks set `INPUT_DIR` / `OUTDIR` in their + first cell without the tag, and 02/03 call `example_input_dir()` again. Until that cell is tagged + and used (asked of the authors, 5 Oct 2026), runs stop with "has no cell tagged parameters". +- **One workbook is one animal:** PCA and the group figures need at least two. +- **Without an upload** (not offered by the dialog) the notebooks run on the repository's own 11 + example workbooks. +- Verified 2 Oct 2026 with the task image in Docker: all three notebooks in ~9 s on one workbook. + +### Two arm bandit, ASST digging + +No analysis repository yet (`repoZipUrl` is empty): they are shown on the site, but the upload +dialog doesn't offer them. When a repository exists, fill in `repoZipUrl` and add `notebooksDir` +and — after checking what its code reads — `inputFormats`. + +## Adding or changing a protocol + +1. Check what the analysis code really reads (file types, single file vs folder, where it looks) + and set `inputFormats` to match, and check its notebooks have the `parameters` cell — don't list formats the code ignores; researchers + would only find out minutes into a run. +2. Run it once from the dialog with the sample file. + +Tunable timings of the run (time limits, polling) are in `../core/runSettings.ts`, not here. diff --git a/applications/idp-arc/frontend/src/data/protocols.json b/applications/idp-arc/frontend/src/data/protocols.json new file mode 100644 index 0000000..13306ee --- /dev/null +++ b/applications/idp-arc/frontend/src/data/protocols.json @@ -0,0 +1,41 @@ +[ + { + "id": "four-choice-reversal", + "name": "Four-choice reversal digging task", + "repoZipUrl": "https://codeload.github.com/maracbaylis/four-choice-example/zip/refs/heads/main", + "notebooksDir": "notebooks", + "inputFormats": [".xlsx", ".zip"], + "templatesZipUrl": "protocols_archives/4c.zip", + "description": "In the four choice reversal digging task mice must identify the single rewarded option and update their choice when a previously unrewarded option becomes rewarded. The reversal phase in this task is sensitive to orbitofrontal and dorsomedial prefrontal cortex function and the 2-day version used here provides a metric of recall on the 2nd day of testing before reversal.", + "imageUrls": ["assets/four_choice_graphic.jpeg"], + "videoUrl": "https://storage.googleapis.com/maabcd/tutorial.mp4", + "references": [ + "Johnson, C., & Wilbrecht, L. (2011). Juvenile mice show greater flexibility in multiple choice reversal learning than adults. Developmental cognitive neuroscience, 1(4), 540-551.", + "Thomas, A. W., Caporale, N., Wu, C., & Wilbrecht, L. (2016). Early maternal separation impacts cognitive flexibility at the age of first independence in mice. Developmental cognitive neuroscience, 18, 49-56.", + "Lu, J., Tjia, M., Mullen, B., Cao, B., Lukasiewicz, K., Shah-Morales, S., ... & Zuo, Y. (2021). An analog of psychedelics restores functional neural circuits disrupted by unpredictable stress. Molecular Psychiatry, 26(11), 6237-6252.", + "Delevich, K., Hoshal, B., Zhou, L.Z., Zhang, Y., Vedula, S., Lin, W.C., Chase, J., Collins, A.G. and Wilbrecht, L., 2022. Activation, but not inhibition, of the indirect pathway disrupts choice rejection in a freely moving, multiple-choice foraging task. Cell Reports, 40(4). DOI: 10.1016/j.celrep.2022.111129", + "Lin, W.C., Liu, C., Kosillo, P., Tai, L.H., Galarce, E., Bateup, H.S., Lammel, S. and Wilbrecht, L., 2022. Transient food insecurity during the juvenile-adolescent period affects adult weight, cognitive flexibility, and dopamine neurobiology. Current Biology, 32(17), pp.3690-3703. 10.1016/j.cub.2022.06.089", + "Cording, K.R., Tu, E.M., Wang, H., Agopyan-Miu, A.H. and Bateup, H.S., 2025. Cntnap2 loss drives striatal neuron hyperexcitability and behavioral inflexibility. elife, 13, p.RP100162. https://doi.org/10.7554/eLife.100162.3" + ] + }, + { + "id": "asst-digging", + "name": "ASST digging task", + "repoZipUrl": "", + "description": "The attentional set-shifting task (ASST) is a rodent analogue of the Cambridge Neuropsychological Test Automated Battery (CANTAB) IED task. It assesses the ability to shift attention between perceptual dimensions of compound stimuli. The task requires animals to learn sequential discriminations, measuring the cost of shifting attention from one perceptual dimension to another.", + "imageUrls": ["assets/ASST_graphic.jpeg"], + "videoUrl": "", + "references": [ + "Ma, S., Wang, K. H., & Zuo, Y. (2026). Targeting insulo-frontal pathway to reduce stress-evoked cognitive rigidity. Nature Communications." + ] + }, + { + "id": "two-arm-bandit", + "name": "Two arm bandit task", + "repoZipUrl": "", + "description": "The two-armed bandit task is a classic paradigm in behavioral neuroscience used to study decision-making under uncertainty. In this task, participants are presented with two options, each associated with a different probability of reward. By repeatedly choosing between the two options, participants learn to exploit the option with the higher reward probability while also exploring the other option to ensure that they are not missing out on a potentially better source of reward. This task has been used to investigate the neural mechanisms underlying reinforcement learning, exploration-exploitation trade-offs, and the role of different brain regions in decision-making.", + "imageUrls": ["assets/BanditFigure_UCLstyleBohan.png"], + "videoUrl": "", + "references": [] + } +] diff --git a/applications/idp-arc/frontend/src/i18n.ts b/applications/idp-arc/frontend/src/i18n.ts new file mode 100644 index 0000000..d418c5d --- /dev/null +++ b/applications/idp-arc/frontend/src/i18n.ts @@ -0,0 +1,22 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' + +import en from './locales/en/common.json' +import enAbout from './locales/en/about.json' +import enLandingPage from './locales/en/landingPage.json' + +i18n + .use(initReactI18next) + .init({ + resources: { + en: { translation: en, common: en, landingPage: enLandingPage, about: enAbout }, + }, + lng: 'en', + fallbackLng: 'en', + interpolation: { + // React already escapes values, so no need for i18next to do it. + escapeValue: false, + }, + }) + +export default i18n diff --git a/applications/idp-arc/frontend/src/index.css b/applications/idp-arc/frontend/src/index.css index 08a3ac9..e69de29 100644 --- a/applications/idp-arc/frontend/src/index.css +++ b/applications/idp-arc/frontend/src/index.css @@ -1,68 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/applications/idp-arc/frontend/src/infra/dandiApiClient.ts b/applications/idp-arc/frontend/src/infra/dandiApiClient.ts new file mode 100644 index 0000000..ef0e4f8 --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/dandiApiClient.ts @@ -0,0 +1,80 @@ +import type { FinalizeUploadInput, IDandiApi, UploadFinalizeResult, UploadInitResult } from '../core/ports/IDandiApi' + +/** + * DandiApiClient — talks to idp-arc's OWN backend (same origin, `/api/dandi/...`), which is + * what actually holds the EMBER-DANDI admin key and calls DANDI/OSB on the browser's behalf. + */ +export class DandiApiClient implements IDandiApi { + constructor(private readonly baseApiUrl: string) {} + + async initUpload(token: string, taskId: string, filename: string, size: number, dandiEtag: string): Promise { + const res = await fetch(`${this.baseApiUrl}/dandi/upload/init`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ task_id: taskId, filename, size, dandi_etag: dandiEtag }), + }) + if (!res.ok) { + throw new Error(`Upload init failed: ${res.status} ${res.statusText} — ${await res.text().catch(() => '')}`) + } + const data = await res.json() as { + upload_id?: string + path: string + parts: { part_number: number; url: string }[] + blob_id?: string + } + return { + uploadId: data.upload_id, + path: data.path, + blobId: data.blob_id, + parts: (data.parts ?? []).map((p) => ({ partNumber: p.part_number, url: p.url })), + } + } + + async putPart(url: string, blob: Blob): Promise { + const res = await fetch(url, { method: 'PUT', body: blob }) + if (!res.ok) { + throw new Error(`Part upload to S3 failed: ${res.status} ${res.statusText}`) + } + const etag = res.headers.get('ETag') + if (!etag) { + throw new Error( + 'S3 did not expose an ETag header on the part upload response — the bucket needs ' + + 'Access-Control-Expose-Headers: ETag in its CORS config.', + ) + } + return etag.replaceAll('"', '') + } + + async finalizeUpload(token: string, input: FinalizeUploadInput): Promise { + const { uploadId, path, parts, workspaceId, workspaceName, blobId, scriptUrl, scriptName } = input + const res = await fetch(`${this.baseApiUrl}/dandi/upload/finalize`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + path, + ...(uploadId ? { upload_id: uploadId } : {}), + ...(blobId ? { blob_id: blobId } : {}), + parts: parts.map((p) => ({ part_number: p.partNumber, size: p.size, etag: p.etag })), + ...(workspaceId !== undefined ? { workspace_id: workspaceId } : {}), + ...(workspaceName ? { workspace_name: workspaceName } : {}), + ...(scriptUrl ? { script_url: scriptUrl } : {}), + ...(scriptName ? { script_name: scriptName } : {}), + }), + }) + if (!res.ok) { + throw new Error(`Upload finalize failed: ${res.status} ${res.statusText} — ${await res.text().catch(() => '')}`) + } + const data = await res.json() as { + asset_path: string + dandiset_url: string + workspace_id: number + script_output?: string + } + return { + assetPath: data.asset_path, + dandisetUrl: data.dandiset_url, + workspaceId: data.workspace_id, + scriptOutput: data.script_output, + } + } +} diff --git a/applications/idp-arc/frontend/src/infra/index.ts b/applications/idp-arc/frontend/src/infra/index.ts new file mode 100644 index 0000000..ccacbfc --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/index.ts @@ -0,0 +1,2 @@ +export { KeycloakAuthClient } from './keycloakAuthClient' +export { WorkspaceApiClient } from './workspaceApiClient' diff --git a/applications/idp-arc/frontend/src/infra/keycloakAuthClient.ts b/applications/idp-arc/frontend/src/infra/keycloakAuthClient.ts new file mode 100644 index 0000000..99a3b27 --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/keycloakAuthClient.ts @@ -0,0 +1,61 @@ +import Keycloak from 'keycloak-js' +import type { IAuthClient } from '../core/ports/IAuthClient' + +/** + * KeycloakAuthClient — concrete implementation of IAuthClient backed by keycloak-js. + * + * This is the ONLY file in the codebase that imports keycloak-js. + * Replacing Keycloak with Auth0 means writing a new class here — nothing else changes. + */ +export class KeycloakAuthClient implements IAuthClient { + private readonly kc: Keycloak + + constructor(config: { url: string; realm: string; clientId: string }) { + this.kc = new Keycloak(config) + } + + init(): Promise { + return this.kc.init({ + onLoad: 'check-sso', + // Without this, check-sso does its login-status check via a full top-window + // redirect to the Keycloak server and back — if that server is unreachable, + // the whole app is replaced by the dead auth server instead of just failing + // to log in. Routing the check through a hidden iframe (this static page, + // which was already shipped in public/ but never wired up) keeps the app + // on-screen even when Keycloak is down. + silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', + pkceMethod: 'S256', + checkLoginIframe: false, + scope: 'openid profile email administrator-scope', + }) + } + + async getToken(minValidity = 30): Promise { + try { + await this.kc.updateToken(minValidity) + } catch { + // No refresh token available (check-sso without offline session). + // Fall through and use the current token if it still exists. + } + if (!this.kc.token) { + throw new Error('No access token available. Please sign in again.') + } + return this.kc.token + } + + getLoginUrl(redirectUri: string): Promise { + return this.kc.createLoginUrl({ redirectUri }) + } + + login(): void { + void this.kc.login() + } + + logout(): void { + void this.kc.logout({ redirectUri: window.location.origin + '/' }) + } + + get tokenParsed(): Record | null { + return (this.kc.tokenParsed as Record) ?? null + } +} diff --git a/applications/idp-arc/frontend/src/infra/mocks/index.ts b/applications/idp-arc/frontend/src/infra/mocks/index.ts new file mode 100644 index 0000000..898815b --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/mocks/index.ts @@ -0,0 +1,2 @@ +export { MockAuthClient } from './mockAuthClient' +export { MockWorkspaceApiClient } from './mockWorkspaceApiClient' diff --git a/applications/idp-arc/frontend/src/infra/mocks/mockAuthClient.ts b/applications/idp-arc/frontend/src/infra/mocks/mockAuthClient.ts new file mode 100644 index 0000000..049151f --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/mocks/mockAuthClient.ts @@ -0,0 +1,40 @@ +import type { IAuthClient } from '../../core/ports/IAuthClient' + +/** + * MockAuthClient — in-memory stub for IAuthClient. + * + * • init() resolves immediately as "authenticated" + * • getToken() returns a static fake JWT payload + * • login/logout do nothing (or log to console) + * • tokenParsed returns a fake user payload + */ +export class MockAuthClient implements IAuthClient { + readonly tokenParsed: Record | null = { + sub: 'mock-user-id', + preferred_username: 'mock-user', + email: 'mock@example.com', + given_name: 'Mock', + family_name: 'User', + } + + async getLoginUrl(_redirectUri: string): Promise { + return '/' + } + + async init(): Promise { + console.info('[MockAuthClient] init() → authenticated') + return true + } + + async getToken(_minValidity?: number): Promise { + return 'mock-access-token' + } + + login(): void { + console.info('[MockAuthClient] login() called — no-op in mock mode') + } + + logout(): void { + console.info('[MockAuthClient] logout() called — no-op in mock mode') + } +} diff --git a/applications/idp-arc/frontend/src/infra/mocks/mockDandiApiClient.ts b/applications/idp-arc/frontend/src/infra/mocks/mockDandiApiClient.ts new file mode 100644 index 0000000..112aaee --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/mocks/mockDandiApiClient.ts @@ -0,0 +1,44 @@ +import type { FinalizeUploadInput, IDandiApi, UploadFinalizeResult, UploadInitResult } from '../../core/ports/IDandiApi' + +let nextWorkspaceId = 900 +let nextUploadId = 1 + +/** MockDandiApiClient — in-memory stub for IDandiApi. No real DANDI/OSB calls. */ +export class MockDandiApiClient implements IDandiApi { + async initUpload(_token: string, taskId: string, filename: string, size: number, _dandiEtag: string): Promise { + await delay(400) + const uploadId = `mock-upload-${nextUploadId++}` + const path = `task-${taskId}/sub-mockuser/${filename}` + console.info(`[MockDandiApiClient] initUpload(${filename}, ${size}B) → ${uploadId} @ ${path}`) + return { uploadId, path, parts: [{ partNumber: 1, url: `blob:mock-part-url` }] } + } + + async putPart(_url: string, _blob: Blob): Promise { + await delay(500) + return 'mock-s3-etag' + } + + async finalizeUpload(_token: string, input: FinalizeUploadInput): Promise { + const { uploadId, path, workspaceId, workspaceName, scriptUrl, scriptName } = input + // The real backend blocks here for as long as spawning + running the script takes — mimic + // that shape (a longer delay) rather than resolving instantly, so this feels representative. + await delay(scriptUrl ? 1500 : 500) + const wsId = workspaceId ?? nextWorkspaceId++ + console.info( + `[MockDandiApiClient] finalizeUpload(${uploadId}) → workspace ${wsId} ` + + `(${workspaceName ?? 'existing'}), script ${scriptName ?? 'none'}`, + ) + return { + assetPath: path, + dandisetUrl: 'https://dandi.emberarchive.org/dandiset/000533/draft', + workspaceId: wsId, + scriptOutput: scriptUrl + ? 'Fetched 1818 bytes from DANDI\n\nProtocol : two-arm-bandit\nTrials : 20\nReward : 13/20 (65%)\n\nWrote ./cumulative_reward.png\n' + : undefined, + } + } +} + +function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/applications/idp-arc/frontend/src/infra/mocks/mockObjectStore.ts b/applications/idp-arc/frontend/src/infra/mocks/mockObjectStore.ts new file mode 100644 index 0000000..441b6e6 --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/mocks/mockObjectStore.ts @@ -0,0 +1,21 @@ +import type { IObjectStore, ObjectUploadInput, StoredObject } from '../../core/ports/IObjectStore' +import { objectKey } from '../publicBucketObjectStore' + +/** MockObjectStore — in-memory stub for IObjectStore. Nothing leaves the browser. */ +export class MockObjectStore implements IObjectStore { + async put(input: ObjectUploadInput, onProgress?: (sentBytes: number, totalBytes: number) => void): Promise { + const { file } = input + // Ten steps, so the mock dialog shows the same streaming progress as the real upload. + for (let i = 1; i <= 10; i++) { + await delay(80) + onProgress?.(Math.round((file.size * i) / 10), file.size) + } + const key = objectKey(input.protocolId, input.userSub, `mock-${Date.now()}`, file.name) + console.info(`[MockObjectStore] put(${key}, ${file.size}B)`) + return { url: `https://storage.googleapis.com/mock-bucket/${key}`, key } + } +} + +function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/applications/idp-arc/frontend/src/infra/mocks/mockWorkspaceApiClient.ts b/applications/idp-arc/frontend/src/infra/mocks/mockWorkspaceApiClient.ts new file mode 100644 index 0000000..a7a1e16 --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/mocks/mockWorkspaceApiClient.ts @@ -0,0 +1,115 @@ +import type { + ImportResourceInput, + IWorkspaceApi, + StartRunInput, + WorkspaceResourceState, +} from '../../core/ports/IWorkspaceApi' +import type { Workspace } from '../../core/types' + +const MOCK_WORKSPACES: Workspace[] = [ + { + id: 1, + name: 'Mock Workspace Alpha', + description: 'A sample workspace for local development', + timestamp_created: '2024-01-15T10:00:00Z', + thumbnail: undefined, + }, + { + id: 2, + name: 'Mock Workspace Beta', + description: 'Another workspace with a longer description to test truncation', + timestamp_created: '2024-02-20T14:30:00Z', + thumbnail: undefined, + }, + { + id: 3, + name: 'Neuroscience Project', + description: 'Simulated neuroscience data workspace', + timestamp_created: '2024-03-05T09:15:00Z', + thumbnail: undefined, + }, +] + +let nextId = 4 + +/** + * MockWorkspaceApiClient — in-memory stub for IWorkspaceApi. + * + * • listWorkspaces() returns the static MOCK_WORKSPACES list after a short delay + * • createWorkspace() appends a new workspace and returns its id + */ +export class MockWorkspaceApiClient implements IWorkspaceApi { + private workspaces: Workspace[] = [...MOCK_WORKSPACES] + + async listWorkspaces(_token: string): Promise { + await delay(400) + console.info('[MockWorkspaceApiClient] listWorkspaces()', this.workspaces) + return [...this.workspaces] + } + + async createWorkspace(_token: string, name: string, tags: string[] = []): Promise { + await delay(600) + const id = nextId++ + this.workspaces.push({ + id, + name, + description: 'Created in mock mode', + timestamp_created: new Date().toISOString(), + }) + console.info(`[MockWorkspaceApiClient] createWorkspace("${name}", [${tags.join(', ')}]) → id=${id}`) + return id + } + + // Imports: pending for a few polls, then gone (as OSB's scan drops non-indexed files). + private imports: { name: string; pendingPolls: number }[] = [] + /** What OSB's scan would list once the imports are done: the repository's notebooks. */ + private notebooks: WorkspaceResourceState[] = [] + /** Polls left showing a run as running (OSB's placeholder while its workflow runs). */ + private runPolls = 0 + private runCount = 0 + + async getWorkspaceResources(_token: string, _workspaceId: number): Promise { + await delay(200) + this.imports.forEach((i) => { i.pendingPolls -= 1 }) + this.imports = this.imports.filter((i) => i.pendingPolls > 0) + if (this.imports.length) { + return [{ id: -1, name: 'Importing resources into workspace' }, ...this.imports.map((i, n) => ({ id: 100 + n, name: i.name, status: 'p' as const }))] + } + if (this.runPolls > 0) { + this.runPolls -= 1 + if (this.runPolls === 0) this.notebooks = this.executed + return [{ id: -1, name: 'Refreshing resources' }, ...this.notebooks] + } + return this.notebooks + } + + async importResource(_token: string, input: ImportResourceInput): Promise { + await delay(300) + console.info(`[MockWorkspaceApiClient] importResource(${input.name} → ${input.folder}/)`) + this.imports.push({ name: input.name, pendingPolls: 2 }) + if (input.resourceType === 'g') { + this.notebooks = ['01_load.ipynb', '02_analysis.ipynb'].map((nb, n) => ({ + id: 200 + n, name: nb, status: 'a' as const, path: `${input.folder}/${input.name}/notebooks/${nb}`, + })) + } + } + + /** A run's executed notebooks: listed once its workflow (and scan) is over; every run succeeds here. */ + private executed: WorkspaceResourceState[] = [] + + async startRun(_token: string, workspaceId: number, input: StartRunInput): Promise<{ workflow: string }> { + await delay(300) + const workflow = `osb-run-notebooks-job-mock${++this.runCount}` + this.runPolls = 3 + this.executed = input.notebooks.map((nb, n) => { + const name = nb.split('/').pop()! + return { id: 300 + n, name, status: 'a' as const, path: `${input.results.notebooks}/${name}` } + }) + console.info(`[MockWorkspaceApiClient] startRun(#${workspaceId}, ${input.repo.dir}: ${input.notebooks.join(', ')}) → ${workflow}`) + return { workflow } + } +} + +function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/applications/idp-arc/frontend/src/infra/publicBucketObjectStore.ts b/applications/idp-arc/frontend/src/infra/publicBucketObjectStore.ts new file mode 100644 index 0000000..b3245de --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/publicBucketObjectStore.ts @@ -0,0 +1,57 @@ +import type { IObjectStore, ObjectUploadInput, StoredObject } from '../core/ports/IObjectStore' + +/** + * Scenario 1: PUTs straight from the browser to a bucket open for anonymous writes (`gs://maabcd`, + * `https://storage.googleapis.com/maabcd`). No Authorization header; the object is readable at the + * same URL afterwards, which is what OSB's import fetches. The bucket's CORS must allow PUT from + * MAABCD's origin. + * + * `allUsers` has object-create but not object-admin, so an upload can't overwrite an existing + * object: a key collision comes back as 412. Keys carry a fresh upload id, so that's rare. + * + * XMLHttpRequest, not fetch, for upload progress. One request per file (GCS takes up to 5 TB). + */ +export class PublicBucketObjectStore implements IObjectStore { + private readonly baseUrl: string + + /** `baseUrl`: e.g. `https://storage.googleapis.com/maabcd` (VITE_UPLOAD_BUCKET_URL). */ + constructor(baseUrl: string | undefined) { + this.baseUrl = (baseUrl ?? '').replace(/\/+$/, '') + } + + put(input: ObjectUploadInput, onProgress?: (sentBytes: number, totalBytes: number) => void): Promise { + if (!this.baseUrl) return Promise.reject(new Error('No upload bucket is configured (VITE_UPLOAD_BUCKET_URL)')) + const key = objectKey(input.protocolId, input.userSub, crypto.randomUUID(), input.file.name) + const url = `${this.baseUrl}/${key.split('/').map(encodeURIComponent).join('/')}` + const { file } = input + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.open('PUT', url) + xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream') + xhr.upload.onprogress = (e) => onProgress?.(e.loaded, file.size) + xhr.onload = () => { + if (xhr.status === 412) { + reject(new Error(`An upload named ${key} already exists in the bucket; try again`)) + } else if (xhr.status < 200 || xhr.status >= 300) { + reject(new Error(`Upload to the bucket failed: ${xhr.status} ${xhr.statusText}`.trim())) + } else { + onProgress?.(file.size, file.size) + resolve({ url, key }) + } + } + xhr.onerror = () => reject(new Error('Upload to the bucket failed: network error (check the bucket’s CORS)')) + xhr.onabort = () => reject(new Error('Upload to the bucket was aborted')) + xhr.send(file) + }) + } +} + +/** `uploads////` (design R1.4), every segment path-safe. */ +export function objectKey(protocolId: string, userSub: string, uploadId: string, filename: string): string { + return ['uploads', protocolId, userSub, uploadId, filename].map(pathSafe).join('/') +} + +/** One path segment: no `/`, no `..`, no spaces. */ +function pathSafe(value: string): string { + return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[-._]+|[-._]+$/g, '') || 'unknown' +} diff --git a/applications/idp-arc/frontend/src/infra/workspaceApiClient.ts b/applications/idp-arc/frontend/src/infra/workspaceApiClient.ts new file mode 100644 index 0000000..29e1996 --- /dev/null +++ b/applications/idp-arc/frontend/src/infra/workspaceApiClient.ts @@ -0,0 +1,137 @@ +import type { + ImportResourceInput, + IWorkspaceApi, + StartRunInput, + WorkspaceResourceState, +} from '../core/ports/IWorkspaceApi' +import type { Workspace } from '../core/types' + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +/** + * WorkspaceApiClient — concrete IWorkspaceApi implementation. + * + * All fetch() calls to the OSB workspace REST API live here. + * Use-cases never see fetch — only the IWorkspaceApi interface. + */ +export class WorkspaceApiClient implements IWorkspaceApi { + constructor( + private readonly baseApiUrl: string, // e.g. "https://www.v2dev.opensourcebrain.org/proxy/workspaces/api" + private readonly listUrl: string, // full workspaces list URL with pagination params + ) {} + + async listWorkspaces(token: string): Promise { + const res = await fetch(this.listUrl, { + headers: { Authorization: `Bearer ${token}` }, + }) + if (!res.ok) { + throw new Error(`Workspace list API responded ${res.status} ${res.statusText}`) + } + const data: unknown = await res.json() + if (Array.isArray(data)) return data as Workspace[] + const obj = data as Record + return (obj.results ?? obj.items ?? obj.workspaces ?? []) as Workspace[] + } + + async createWorkspace(token: string, name: string, tags: string[] = []): Promise { + const RETRYABLE = new Set([502, 503, 504]) + const MAX_ATTEMPTS = 3 + let lastError: Error = new Error('Workspace creation failed') + + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + if (attempt > 1) await sleep(attempt * 2_000) + + const res = await fetch(`${this.baseApiUrl}/workspace`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: name.trim(), + description: name.trim(), + ...(tags.length ? { tags: tags.map((tag) => ({ tag })) } : {}), + }), + }) + + if (res.ok) { + const ws = (await res.json()) as { id: number } + return ws.id + } + + let detail = '' + try { + const body = await res.json() as Record + detail = (body.description ?? body.message ?? body.detail ?? '') as string + } catch { /* non-JSON body */ } + + lastError = new Error( + res.status === 405 + ? `Not allowed to create a new workspace on this platform (HTTP 405).${detail ? ` ${detail}` : ''}` + : RETRYABLE.has(res.status) + ? `Workspace service unavailable (HTTP ${res.status}), attempt ${attempt}/${MAX_ATTEMPTS}.` + : `Failed to create workspace: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`, + ) + + if (!RETRYABLE.has(res.status)) break + } + + throw lastError + } + + async getWorkspaceResources(token: string, workspaceId: number): Promise { + const res = await fetch(`${this.baseApiUrl}/workspace/${workspaceId}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + if (!res.ok) throw new Error(`Reading workspace ${workspaceId} failed: ${res.status} ${res.statusText}`) + const ws = (await res.json()) as { resources?: { id: number; name: string; status?: 'p' | 'a' | 'e'; path?: string }[] } + return (ws.resources ?? []).map(({ id, name, status, path }) => ({ id, name, status, path })) + } + + async importResource(token: string, input: ImportResourceInput): Promise { + const folder = `${input.folder.replace(/\/+$/, '')}/` + const res = await fetch(`${this.baseApiUrl}/workspaceresource`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + workspace_id: input.workspaceId, + name: input.name, + resource_type: input.resourceType, + origin: { path: input.url }, + // A folder path (trailing slash): OSB copies the download into it (a zip is unpacked there). + path: folder, + // Sent as well because OSB's WorkspaceresourceService.to_dao only renames `path` to the + // database's `folder` when a `folder` key is present (`if 'folder' in ws_dict`); with + // `path` alone the create fails with MalformedModelDictionaryError. + folder, + }), + }) + if (!res.ok) { + throw new Error(`Importing ${input.name} into the workspace failed: ${res.status} ${res.statusText} ${await res.text().catch(() => '')}`.trim()) + } + } + + async startRun(token: string, workspaceId: number, input: StartRunInput): Promise<{ workflow: string }> { + const { repo, setup, notebooks, inputDir, outputDir, results, image } = input + const res = await fetch(`${this.baseApiUrl}/workspace/${workspaceId}/run`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + repo, + ...(setup ? { setup: { requirements: setup.requirements, python_path: setup.pythonPath, install: setup.install } } : {}), + notebooks, + ...(inputDir ? { input_dir: inputDir } : {}), + output_dir: outputDir, + results, + ...(image ? { image } : {}), + }), + }) + if (!res.ok) { + throw new Error(`Starting the notebooks failed: ${res.status} ${res.statusText} ${await res.text().catch(() => '')}`.trim()) + } + const body = (await res.json()) as { workflow: string } + return { workflow: body.workflow } + } +} diff --git a/applications/idp-arc/frontend/src/locales/en/about.json b/applications/idp-arc/frontend/src/locales/en/about.json new file mode 100644 index 0000000..23b879d --- /dev/null +++ b/applications/idp-arc/frontend/src/locales/en/about.json @@ -0,0 +1,68 @@ +{ + "pageTitle": "About", + "sections": [ + { "id": "overview", "label": "Overview" }, + { "id": "objectives", "label": "Project Objectives" }, + { "id": "phenotypes", "label": "Behavioral Phenotypes as Multivariate Systems" }, + { "id": "quantifying", "label": "Quantifying Behavioral Effects" }, + { "id": "resilience", "label": "Resilience as a Continuous Dimension" }, + { "id": "standardization", "label": "Standardization Across Cohorts" }, + { "id": "platform", "label": "Toward a Shared Behavioral Analysis Platform" }, + { "id": "collaborators", "label": "Collaborators" } + ], + "overview": { + "title": "Overview", + "body1": "The Modeling Adolescent Adversity Brain and Cognitive Development Platform (MAABCD) develops a statistical framework for analyzing how early-life adversity shapes behavioral phenotypes. Behavioral outcomes arise from the interaction of multiple cognitive and motivational processes and are therefore inherently multidimensional. However, behavioral data are often analyzed using methods that treat individual measures independently, which limits the ability to detect coordinated patterns of behavioral change.", + "body2": "The project addresses this challenge by modeling behavior as a multivariate phenotype embedded within a latent behavioral space. By applying dimensionality-reduction methods to high-dimensional behavioral datasets, the framework allows structured patterns of behavioral variation to be identified and compared across experimental groups." + }, + "objectives": { + "title": "Project Objectives", + "items": [ + "Determine whether food insecurity (FI) induces a reproducible displacement in behavioral phenotype space.", + "Test whether distinct adversity paradigms (FI, FR, ALG) produce shared versus distinct shifts.", + "Define resilience as continuous variation along an adversity axis.", + "Establish a framework scalable to multi-laboratory standardization." + ], + "conclusion": "By addressing these objectives within a unified analytical framework, the project aims to establish systematic methods for quantifying the behavioral consequences of early-life adversity." + }, + "phenotypes": { + "title": "Behavioral Phenotypes as Multivariate Systems", + "subTitle": "Modeling Behavior in Latent Space", + "body1": "Behavior cannot be fully characterized through isolated measurements. Instead, it reflects the interaction of multiple behavioral processes that collectively shape observable phenotypes.", + "body2_before": "To capture this structure, the framework analyzes large sets of behavioral features simultaneously and embeds them into a ", + "body2_highlight": "latent behavioral space", + "body2_after": " using dimensionality-reduction methods such as principal component analysis (PCA). This representation preserves the dominant structure of behavioral variation while allowing complex datasets to be interpreted geometrically.", + "figurePlaceholder": "Figure: Latent behavioral geometry framework", + "figureCaption_label": "Figure 1: ", + "figureCaption_title": "Latent behavioral geometry framework", + "figureCaption_body": ". Behavioral features are first cohort-corrected, then embedded into a low-dimensional latent space (PCA). Group effects are expressed as centroid displacement vectors relative to ALS controls. Individual resilience is defined as projection onto the FI displacement axis, enabling continuous within-group variation and that lets us create a scalable framework for cross-laboratory comparison." + }, + "quantifying": { + "title": "Quantifying Behavioral Effects", + "subTitle": "Geometric Representation of Treatment Effects", + "body1_before": "Within the latent behavioral space, the effects of experimental manipulations can be represented as ", + "body1_highlight": "displacement vectors relative to control populations", + "body1_after": ".", + "body2": "These vectors summarize both the magnitude and direction of behavioral change, allowing researchers to identify which behavioral systems are most strongly affected by adversity. This geometric representation provides a systematic method for comparing behavioral phenotypes across experiments and adversity paradigms." + }, + "resilience": { + "title": "Resilience as a Continuous Dimension", + "body1": "A central objective of the framework is to move beyond binary classifications of behavioral outcomes.", + "body2_before": "Rather than categorizing individuals as simply affected or unaffected, resilience is defined as a ", + "body2_highlight": "continuous axis within the latent behavioral space", + "body2_after": ". Each individual's behavioral phenotype can be projected onto this axis, providing a quantitative measure of vulnerability or resilience relative to control populations.", + "body3": "This formulation allows individual variability in behavioral response to be measured directly and compared across populations." + }, + "standardization": { + "title": "Standardization Across Cohorts", + "body1": "Behavioral data are often collected across different cohorts and experimental conditions, introducing sources of technical variability that complicate comparisons between studies.", + "body2_before": "To address this challenge, the framework incorporates ", + "body2_highlight": "cohort correction procedures", + "body2_after": " that remove systematic baseline differences before multivariate analysis. This allows behavioral datasets from different cohorts or laboratories to be analyzed within a consistent statistical framework." + }, + "platform": { + "title": "Toward a Shared Behavioral Analysis Platform", + "body1": "The analytical framework developed through the Adversity Project establishes the conceptual basis for a shared behavioral analysis infrastructure.", + "body2": "By combining standardized behavioral feature sets, cohort-corrected datasets, and latent-space representations of behavioral phenotypes, the system enables behavioral data from multiple laboratories to be analyzed within a unified framework. This approach supports scalable analysis pipelines and collaborative datasets for studying the behavioral consequences of developmental adversity." + } +} diff --git a/applications/idp-arc/frontend/src/locales/en/common.json b/applications/idp-arc/frontend/src/locales/en/common.json new file mode 100644 index 0000000..5ac5c99 --- /dev/null +++ b/applications/idp-arc/frontend/src/locales/en/common.json @@ -0,0 +1,48 @@ +{ + "app": { + "title": "MAABCD" + }, + "auth": { + "initialising": "Initialising authentication…", + "notLoggedIn": "You are not logged in.", + "signIn": "Sign in with OSB", + "signOut": "Sign out", + "signedInAs": "Signed in as" + }, + "errors": { + "authFailed": "Failed to initialise authentication. See console for details.", + "workspacesLoad": "Error: {{message}}" + }, + "workspaces": { + "title": "OSB Workspaces", + "loading": "Loading workspaces…", + "empty": "No workspaces found.", + "newButton": "+ New workspace", + "table": { + "id": "ID", + "name": "Name", + "description": "Description", + "created": "Created", + "noDescription": "—", + "noDate": "—" + } + }, + "modal": { + "title": "New workspace", + "workspaceNameLabel": "Workspace name", + "workspaceNamePlaceholder": "My new workspace", + "cancel": "Cancel", + "create": "Create", + "creationFailed": "Failed to create workspace: {{message}}" + }, + "collaborators": { + "sectionTitle": "Collaborators", + "body": "ARC Platform is built by Adversity and Resilience Consortium, a consortium consisting of 4 different institutions.", + "items": [ + { "alt": "UC Berkeley", "src": "/UCBerkeley.png" }, + { "alt": "Emory University", "src": "/Emory.png" }, + { "alt": "UC Santa Cruz", "src": "/UCSantaCruz.png" }, + { "alt": "UCL", "src": "/UCL.png" } + ] + } +} diff --git a/applications/idp-arc/frontend/src/locales/en/landingPage.json b/applications/idp-arc/frontend/src/locales/en/landingPage.json new file mode 100644 index 0000000..ee7b936 --- /dev/null +++ b/applications/idp-arc/frontend/src/locales/en/landingPage.json @@ -0,0 +1,64 @@ +{ + "banner": { + "title": "Modeling Adolescent Adversity Brain and Cognitive Development Platform (MAABCD)", + "ctaProtocols": "Protocols", + "ctaDataUpload": "Data upload" + }, + "features": [ + { + "title": "Download protocols and templates", + "desc": "Download the standardized protocol and analysis code required to perform experiments according to MAABCD standards. These protocols considered not only likelihood of success, but also ethological relevance, and ease of access to both low and high budget research labs.", + "cta": "Protocols", + "ctaPath": "/protocols" + }, + { + "title": "Upload data in EMBER-DANDI", + "desc": "Share your experimental results with the research community. Sign in to upload dandisets to the Ember-Dandi platform.", + "cta": "Data upload", + "ctaPath": "/workspaces" + }, + { + "title": "Use your data in Open Source Brain", + "desc": "Explore and analyze community data and your uploaded data with our integration with Open Source Brain (OSB). OSB lets you use interactive visualizations and powerful analysis tools with Jupyterlab, NetPyNE and NWB Explorer.", + "cta": "Go to Open Source Brain", + "ctaPath": "https://www.opensourcebrain.org/" + } + ], + "protocols": { + "sectionTitle": "Protocols", + "downloadZip": "Download .zip", + "items": [ + { "name": "Four-Choice Reversal Digging Task", "desc": "Measures odor discrimination and reversal learning." }, + { "name": "Attentional Set Shifting Task (ASST)", "desc": "Measures attentional set formation and shifting, and cognitive flexibility." }, + { "name": "Two-Armed Bandit Task (2ABT)", "desc": "Measures reward-guided choice and flexibility." } + ] + }, + "about": { + "sectionTitle": "About MAABCD Platform", + "body1": "Welcome to our site. MAABCD stands for Model Adolescent Adversity Brain and Cognitive Development. Many researchers are working hard to learn more and more about how experience of adversity can alter brain and behavioral development. One challenge of this work comes from the fact that it is hard to compare data between studies. This prevents us from learning which experiences have common or separable effects. Null results may also not often reach publication and can be lost to the community. To gain a more complete picture of the impacts of adversity, we sought to build a tool that could help our research community harmonize data collection across sites so that we can begin to compare the effects of different treatments on behavioral outcomes. We were inspired by the goals of the human ABCD projects to study adolescent development and the success of the International Brain lab, which harmonized a behavioral data collection from mice across 7 global sites.", + "body2": "In the long term we are interested in understanding a) if there are different dimensions of adversity which have separable effects on brain and behavior and b) if there are sensitive periods for exposure. We hope to disseminate these protocols so many labs can similarly harmonize their data collection enabling comparisons of a wide variety of treatments on the same behavioral outcome metrics. Our project is supported by the BBQS R34.", + "learnMore": "Learn more" + }, + "nav": { + "logoTitle": "MAABCD Platform", + "protocols": "Protocols", + "about": "About", + "myWorkspaces": "My workspaces", + "login": "Login", + "logout": "Logout", + "dataUpload": "Data upload", + "loggedInAs": "OSB logged in as", + "signingIn": "Signing in", + "completeSignIn": "Complete sign in in the popup window.", + "cancel": "Cancel" + }, + "footer": { + "copyright": "Copyright © 2026 MAABCD Platform by MetaCell", + "links": { + "legal": "Legal", + "docs": "Docs", + "about": "About", + "contact": "Contact" + } + } +} diff --git a/applications/idp-arc/frontend/src/main.tsx b/applications/idp-arc/frontend/src/main.tsx index bef5202..cd81cee 100644 --- a/applications/idp-arc/frontend/src/main.tsx +++ b/applications/idp-arc/frontend/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' +import './i18n' import App from './App.tsx' createRoot(document.getElementById('root')!).render( diff --git a/applications/idp-arc/frontend/src/pages/AboutPage.tsx b/applications/idp-arc/frontend/src/pages/AboutPage.tsx new file mode 100644 index 0000000..38f27a9 --- /dev/null +++ b/applications/idp-arc/frontend/src/pages/AboutPage.tsx @@ -0,0 +1,240 @@ +import { Box, Container, Stack, Typography } from '@mui/material' +import { useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import PageLayout from '../components/PageLayout' + +const styles = { + section: (maxWidth = '50%') => ({ + gap: 3, + scrollMarginTop: 80, + maxWidth: { xs: '100%', lg: maxWidth }, + }), + collaboratorsRow: { + alignItems: 'center', + justifyContent: 'space-between', + flexWrap: 'wrap' as const, + gap: 4, + }, + fixedNav: { + display: { xs: 'none', lg: 'flex' }, + position: 'fixed' as const, + top: '50%', + right: { lg: 'max(2rem, calc((100vw - 1536px) / 2 + 2rem))' }, + transform: 'translateY(-50%)', + zIndex: 10, + }, + navItem: { + alignItems: 'center', + gap: 1, + py: 0.75, + cursor: 'pointer', + textDecoration: 'none', + '&:hover': { + '& .MuiTypography-caption': { color: '#fff' }, + '.nav-underline': { width: 24 }, + }, + }, + navLabel: (isActive: boolean) => ({ + fontSize: '0.875rem', + fontWeight: isActive ? 500 : 400, + color: isActive ? 'var(--mui-palette-text-primary)' : 'var(--mui-palette-content-secondary)', + transition: 'color 0.2s', + whiteSpace: 'nowrap', + }), + navUnderline: (isActive: boolean) => ({ + width: isActive ? 24 : 12, + height: '2px', + bgcolor: isActive ? 'var(--mui-palette-primary-main)' : 'var(--mui-palette-stroke-default)', + transition: 'width 0.2s, background-color 0.2s', + flexShrink: 0, + }), +} + +interface SectionProps { + id: string + registerRef: (id: string) => (el: HTMLElement | null) => void + children: React.ReactNode + maxWidth?: string +} + +function Section({ id, registerRef, children, maxWidth }: SectionProps) { + return ( + + {children} + + ) +} + +export default function AboutPage() { + const { t } = useTranslation('about') + const { t: tCommon } = useTranslation('common') + const sections = t('sections', { returnObjects: true }) as Array<{ id: string; label: string }> + const objectives = t('objectives.items', { returnObjects: true }) as string[] + const collaborators = tCommon('collaborators.items', { returnObjects: true }) as Array<{ alt: string; src: string }> + + const [activeSection, setActiveSection] = useState('overview') + const sectionRefs = useRef>({}) + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) setActiveSection(entry.target.id) + } + }, + { rootMargin: '-40% 0px -55% 0px', threshold: 0 }, + ) + Object.values(sectionRefs.current).forEach((el) => el && observer.observe(el)) + return () => observer.disconnect() + }, []) + + const registerRef = (id: string) => (el: HTMLElement | null) => { + sectionRefs.current[id] = el + } + + return ( + + + +
+ {t('overview.title')} + {t('overview.body1')} + {t('overview.body2')} +
+
+ {t('objectives.title')} + + {objectives.map((text, i) => ( + + + {String(i + 1).padStart(2, '0')} + + {text} + + ))} + + {t('objectives.conclusion')} +
+ +
+ {t('phenotypes.title')} + + {t('phenotypes.subTitle')} + {t('phenotypes.body1')} + + {t('phenotypes.body2_before')} + {t('phenotypes.body2_highlight')} + {t('phenotypes.body2_after')} + + + + + + + + {t('phenotypes.figureCaption_label')} + {t('phenotypes.figureCaption_title')} + {t('phenotypes.figureCaption_body')} + +
+ +
+ {t('quantifying.title')} + + {t('quantifying.subTitle')} + + {t('quantifying.body1_before')} + {t('quantifying.body1_highlight')} + {t('quantifying.body1_after')} + + {t('quantifying.body2')} + +
+ +
+ {t('resilience.title')} + {t('resilience.body1')} + + {t('resilience.body2_before')} + {t('resilience.body2_highlight')} + {t('resilience.body2_after')} + + {t('resilience.body3')} +
+ +
+ {t('standardization.title')} + {t('standardization.body1')} + + {t('standardization.body2_before')} + {t('standardization.body2_highlight')} + {t('standardization.body2_after')} + +
+ +
+ {t('platform.title')} + {t('platform.body1')} + {t('platform.body2')} +
+ +
+ {tCommon('collaborators.sectionTitle')} + {tCommon('collaborators.body')} + + {collaborators.map(({ src, alt }) => ( + + ))} + +
+ +
+ + + + {sections.map(({ id, label }) => { + const isActive = activeSection === id + return ( + { + e.preventDefault() + sectionRefs.current[id]?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }} + sx={styles.navItem} + > + + {label} + + + + ) + })} + + + +
+
+ ) +} diff --git a/applications/idp-arc/frontend/src/pages/LandingPage.tsx b/applications/idp-arc/frontend/src/pages/LandingPage.tsx new file mode 100644 index 0000000..04209db --- /dev/null +++ b/applications/idp-arc/frontend/src/pages/LandingPage.tsx @@ -0,0 +1,138 @@ +import ArrowForwardIcon from '@mui/icons-material/ArrowForward' +import FileDownloadIcon from '@mui/icons-material/FileDownload' +import { + Box, + Button, + Container, + List, + ListItem, + ListItemText, + Stack, + Typography, +} from '@mui/material' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import PageLayout from '../components/PageLayout' +import { openUpload } from '../components/UploadContext' + +const ArrowIcon = () => + +export default function LandingPage() { + const { t } = useTranslation('landingPage') + const { t: tCommon } = useTranslation('common') + + const navigate = useNavigate() + + const features = t('features', { returnObjects: true }) as Array<{ title: string; desc: string; cta: string; ctaPath: string }> + + const protocols = t('protocols.items', { returnObjects: true }) as Array<{ name: string; desc: string }> + + const collaborators = tCommon('collaborators.items', { returnObjects: true }) as Array<{ alt: string; src: string }> + + const cta = ( + + + + + ) + + return ( + + + + + {features.map(({ title, desc, cta, ctaPath }) => ( + + + + {title} + + + {desc} + + + + + + + ))} + + + {t('protocols.sectionTitle')} + + {protocols.map(({ name, desc }, index) => ( + navigate('/protocols')} + > + {String(index + 1).padStart(2, '0')} + + + + ))} + + + + + {t('about.sectionTitle')} + {t('about.body1')} + {t('about.body2')} + + + + + {tCommon('collaborators.sectionTitle')} + {tCommon('collaborators.body')} + + {collaborators.map(({ src, alt }) => ( + + ))} + + + + + + ) +} diff --git a/applications/idp-arc/frontend/src/pages/ProtocolsPage.tsx b/applications/idp-arc/frontend/src/pages/ProtocolsPage.tsx new file mode 100644 index 0000000..fedbe38 --- /dev/null +++ b/applications/idp-arc/frontend/src/pages/ProtocolsPage.tsx @@ -0,0 +1,193 @@ +import ArrowForwardIcon from '@mui/icons-material/ArrowForward' +import { + Box, + Button, + Container, + List, + ListItem, + ListItemText, + Stack, + Typography, +} from '@mui/material' +import { useState } from 'react' +import PageLayout from '../components/PageLayout' +import protocols from '../data/protocols.json' +import { templatesZipFileName, templatesZipHref } from '../core/protocolTemplates' + +// imageUrls entries starting with "assets/" point into src/assets and must go through Vite +// to get a hashed build URL; anything else is served as-is from public/. +const assetUrls = import.meta.glob('../assets/*.{png,jpg,jpeg,svg,webp}', { eager: true, query: '?url', import: 'default' }) as Record +const resolveImageUrl = (url: string) => (url.startsWith('assets/') ? assetUrls[`../${url}`] ?? url : url) + +const ArrowIcon = () => + + +// Shared card styles consistent with the rest of the design system +const protocolCardSx = { + width: '100%', + border: '1px solid', + borderColor: 'var(--mui-palette-stroke-default)', + borderRadius: '12px', + background: `#1f1d1d`, + overflow: 'hidden', + padding: '3.125rem 6.25rem' +} + +export default function ProtocolsPage() { + const [activeProtocol, setActiveProtocol] = useState(0) + const active = protocols[activeProtocol] + const templatesHref = templatesZipHref(active) + + return ( + + + + + + + {protocols.map(({ name }, i) => { + const isActive = i === activeProtocol + return ( + setActiveProtocol(i)} + sx={{ + maxHeight: '4rem', + cursor: 'pointer', + borderBottom: '1px solid var(--mui-palette-white-200)', + transition: 'border-color 0.2s, padding 0.2s', + borderLeft: isActive ? '1px solid var(--mui-palette-primary-main)' : '1px solid transparent', + '& .MuiTypography-root': { + color: isActive ? 'text.primary' : 'text.secondary', + fontWeight: isActive ? 500 : 400, + }, + '&:hover': { + borderBottomColor: 'var(--mui-palette-stroke-hover)', + backgroundColor: 'transparent', + pl: 6, + }, + }} + > + {String(i + 1).padStart(2, '0')} + + + ) + })} + + + + "Can't find what you're looking for?" +
+ + Contact us + +
+ +
+ + + + {active.name} + + + + + + + +
+ + + + + {active.name} + + + {active.imageUrls.map((url) => ( + + + + ))} + + {active.description && ( + + {active.description} + + )} + + {active.videoUrl && ( + + Protocol video + + {/* Tutorials are long and narrated: native controls for sound, seeking and fullscreen; + preload="metadata" avoids pulling the whole file on page load. key resets playback + when switching protocol. */} + + + + )} + + {active.references.length > 0 && ( + + References + + {active.references.map((ref, i) => ( + + {ref} + + ))} + + + )} + + +
+
+
+ ) +} + diff --git a/applications/idp-arc/frontend/src/pages/Workspaces.tsx b/applications/idp-arc/frontend/src/pages/Workspaces.tsx new file mode 100644 index 0000000..2db413d --- /dev/null +++ b/applications/idp-arc/frontend/src/pages/Workspaces.tsx @@ -0,0 +1,175 @@ +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { authClient, loadWorkspaces, createWorkspace } from '../app/container' +import type { Workspace } from '../core/types' +import { useAppContext } from '../AppContext' + +export default function Workspaces() { + const { t } = useTranslation() + const { authState, authError, username } = useAppContext() + const [workspaces, setWorkspaces] = useState(null) + const [workspacesError, setWorkspacesError] = useState(null) + const [workspacesLoading, setWorkspacesLoading] = useState(false) + + // Modal state + const [modalOpen, setModalOpen] = useState(false) + const [workspaceName, setWorkspaceName] = useState('') + const [creating, setCreating] = useState(false) + const [createError, setCreateError] = useState(null) + + const loadWorkspaceList = useCallback(() => { + setWorkspacesLoading(true) + setWorkspacesError(null) + loadWorkspaces() + .then((list) => { + console.log('[IDP-ARC] Workspaces loaded:', list) + setWorkspaces(list) + }) + .catch((err: Error) => { + console.error('Failed to fetch workspaces', err) + setWorkspacesError(err.message) + }) + .finally(() => setWorkspacesLoading(false)) + }, []) + + useEffect(() => { + if (authState !== 'authenticated') return + void (async () => { loadWorkspaceList() })() + }, [authState, loadWorkspaceList]) + + function openModal() { + setWorkspaceName('') + setCreateError(null) + setModalOpen(true) + } + + function closeModal() { + setModalOpen(false) + } + + async function handleCreate() { + if (!workspaceName.trim()) return + setCreating(true) + setCreateError(null) + try { + await createWorkspace(workspaceName.trim()) + setModalOpen(false) + loadWorkspaceList() + } catch (err: unknown) { + setCreateError(err instanceof Error ? err.message : String(err)) + } finally { + setCreating(false) + } + } + + if (authState === 'loading') { + return

{t('auth.initialising')}

+ } + + if (authError) { + return

{authError}

+ } + + if (authState === 'unauthenticated') { + return ( +
+

{t('app.title')}

+

{t('auth.notLoggedIn')}

+ +
+ ) + } + + return ( +
+
+

{t('app.title')}

+

{t('auth.signedInAs')} {username}

+ +
+ +
+
+

{t('workspaces.title')}

+ +
+ {workspacesLoading &&

{t('workspaces.loading')}

} + {workspacesError && ( +

{t('errors.workspacesLoad', { message: workspacesError })}

+ )} + {workspaces && workspaces.length === 0 && ( +

{t('workspaces.empty')}

+ )} + {workspaces && workspaces.length > 0 && ( + + + + + + + + + + + {workspaces.map((ws) => ( + + + + + + + ))} + +
{t('workspaces.table.id')}{t('workspaces.table.name')}{t('workspaces.table.description')}{t('workspaces.table.created')}
{ws.id}{ws.name}{ws.description ?? t('workspaces.table.noDescription')} + {ws.timestamp_created + ? new Date(ws.timestamp_created).toLocaleDateString() + : t('workspaces.table.noDate')} +
+ )} +
+ + {/* ── New workspace modal ───────────────────────────────────────────── */} + {modalOpen && ( +
{ if (e.target === e.currentTarget) closeModal() }} + > +
+

{t('modal.title')}

+ + + + {createError && ( +

+ {t('modal.creationFailed', { message: createError })} +

+ )} + +
+ + +
+
+
+ )} +
+ ) +} diff --git a/applications/idp-arc/frontend/src/theme/components.ts b/applications/idp-arc/frontend/src/theme/components.ts new file mode 100644 index 0000000..90f053e --- /dev/null +++ b/applications/idp-arc/frontend/src/theme/components.ts @@ -0,0 +1,608 @@ +import { createTheme } from '@mui/material/styles'; +import { arcPalette } from './palette'; + +export const fontFamilies = { + body: ['Inter', 'sans-serif'].join(', '), + display: ['"Adriane Text"', 'serif'].join(', '), +} as const; + +export const theme = createTheme({ + cssVariables: true, + palette: { + ...arcPalette, + }, + typography: { + fontFamily: fontFamilies.body, + h1: { + fontFamily: fontFamilies.display, + fontSize: '3rem', + fontWeight: 400, + lineHeight: 'normal', + color: 'var(--mui-palette-text-primary)', + letterSpacing: 0, + }, + h2: { + fontFamily: fontFamilies.display, + fontSize: '2rem', + fontWeight: 400, + lineHeight: 'normal', + color: 'var(--mui-palette-text-primary)', + letterSpacing: '0.04rem', + }, + h3: { + fontFamily: fontFamilies.body, + fontSize: '1.5rem', + fontWeight: 400, + lineHeight: '1.6rem', + color: 'var(--mui-palette-text-primary)', + }, + h4: { + fontFamily: fontFamilies.body, + fontSize: '1.25rem', + fontWeight: 400, + lineHeight: '1.6rem', + color: 'var(--mui-palette-text-primary)', + }, + body1: { + fontFamily: fontFamilies.body, + fontSize: '1rem', + fontWeight: 400, + lineHeight: '1.6rem', + color: 'var(--mui-palette-text-secondary)', + }, + body2: { + fontFamily: fontFamilies.body, + fontSize: '1rem', + fontWeight: 400, + lineHeight: '1.5rem', + color: 'var(--mui-palette-text-secondary)', + }, + caption: { + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 400, + lineHeight: '1.375rem', + color: 'var(--mui-palette-text-secondary)', + }, + button: { + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 400, + lineHeight: 'normal', + letterSpacing: 0, + textTransform: 'none' as const, + }, + overline: { + fontFamily: fontFamilies.body, + fontSize: '1rem', + fontWeight: 300, + lineHeight: 'normal', + letterSpacing: 0, + textTransform: 'none' as const, + color: 'var(--mui-palette-text-primary)', + }, + subtitle1: { + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 300, + lineHeight: '1.125rem', + color: 'var(--mui-palette-text-primary)', + }, + subtitle2: { + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 500, + lineHeight: '1.375rem', + color: 'var(--mui-palette-text-primary)', + }, + }, + components: { + + MuiCssBaseline: { + styleOverrides: ` + html { + scroll-behavior: smooth; + } + + body { + background-color: var(--mui-palette-background-default); + color: var(--mui-palette-text-primary); + font-family: ${fontFamilies.body}; + } + + a { + text-decoration: none; + } + + b, strong { + font-weight: 500; + } + + *::-webkit-scrollbar { + width: 0.25rem; + } + + *::-webkit-scrollbar-button { + height: 0; + } + + *::-webkit-scrollbar-thumb { + border-radius: 6.25rem; + background: var(--mui-palette-divider); + } + + *::-webkit-scrollbar-track { + background: transparent; + } + + `, + }, + + MuiButtonBase: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: { + textTransform: 'none', + }, + }, + }, + + MuiButton: { + defaultProps: { + disableElevation: true, + disableRipple: true, + }, + styleOverrides: { + root: { + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 400, + lineHeight: 'normal', + textTransform: 'none' as const, + letterSpacing: 0, + borderRadius: 0, + height: 32, + minWidth: 0, + padding: '0.5rem 0.75rem', + gap: 6, + '&.Mui-disabled': { + opacity: 0.5, + }, + }, + contained: { + backgroundColor: 'var(--mui-palette-text-primary)', + color: 'var(--mui-palette-background-default)', + boxShadow: 'none !important', + '&:hover': { + backgroundColor: 'var(--mui-palette-text-secondary)', + }, + }, + outlined: { + color: 'var(--mui-palette-text-primary)', + borderColor: 'var(--mui-palette-divider)', + '&:hover': { + backgroundColor: 'var(--mui-palette-background-paper)', + borderColor: 'var(--mui-palette-text-secondary)', + }, + }, + text: { + color: 'var(--mui-palette-text-primary)', + '&:hover': { + backgroundColor: 'transparent', + color: 'var(--mui-palette-text-primary)', + }, + }, + }, + }, + + MuiIconButton: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: { + color: 'var(--mui-palette-text-primary)', + borderRadius: 100, + border: `1px solid ${'var(--mui-palette-divider)'}`, + backgroundColor: 'var(--mui-palette-background-paper)', + transition: 'opacity 0.2s ease-in-out', + '&:hover': { + backgroundColor: 'var(--mui-palette-background-paper)', + }, + '&.Mui-disabled': { + opacity: 0.5, + }, + }, + }, + }, + + MuiTypography: { + defaultProps: { + variantMapping: { + h1: 'h1', + h2: 'h2', + h3: 'h3', + h4: 'h4', + body1: 'p', + body2: 'p', + caption: 'span', + overline: 'span', + subtitle1: 'span', + subtitle2: 'span', + }, + }, + }, + + MuiLink: { + defaultProps: { + underline: 'none' as const, + }, + styleOverrides: { + root: { + color: 'var(--mui-palette-text-primary)', + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 400, + '&:hover': { + color: 'var(--mui-palette-text-secondary)', + }, + }, + }, + }, + + MuiAppBar: { + defaultProps: { + elevation: 0, + }, + styleOverrides: { + root: { + backgroundColor: 'transparent', + backgroundImage: 'none', + boxShadow: 'none', + border: 'none', + '--AppBar-color': 'transparent !important', + }, + }, + }, + + MuiToolbar: { + styleOverrides: { + root: { + minHeight: 72, + paddingLeft: 100, + paddingRight: 100, + justifyContent: 'space-between', + }, + }, + }, + + MuiCard: { + styleOverrides: { + root: { + backgroundColor: 'var(--mui-palette-background-paper)', + border: `1px solid ${'var(--mui-palette-divider)'}`, + borderRadius: 12, + backgroundImage: 'none', + boxShadow: 'none', + transition: 'border-color 0.2s', + '&:hover': { + borderColor: 'var(--mui-palette-divider)', + }, + }, + }, + }, + + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + backgroundColor: 'var(--mui-palette-background-paper)', + border: `1px solid ${'var(--mui-palette-divider)'}`, + borderRadius: 0, + }, + rounded: { + borderRadius: 12, + }, + }, + }, + + MuiDivider: { + styleOverrides: { + root: { + borderColor: 'var(--mui-palette-background-paper)', + }, + }, + }, + + MuiListItem: { + styleOverrides: { + root: { + borderBottom: `1px solid ${'var(--mui-palette-background-paper)'}`, + padding: 24, + gap: 12, + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'var(--mui-palette-background-paper)', + }, + }, + }, + }, + + MuiListItemText: { + styleOverrides: { + primary: { + fontFamily: fontFamilies.body, + fontSize: '1rem', + fontWeight: 400, + color: 'var(--mui-palette-text-primary)', + }, + secondary: { + fontFamily: fontFamilies.body, + fontSize: '1rem', + fontWeight: 400, + color: 'var(--mui-palette-text-secondary)', + }, + }, + }, + + MuiListItemIcon: { + styleOverrides: { + root: { + color: 'var(--mui-palette-text-secondary)', + minWidth: 'auto', + marginRight: 12, + }, + }, + }, + + MuiTabs: { + styleOverrides: { + root: { + minHeight: 32, + }, + indicator: { + backgroundColor: 'var(--mui-palette-primary-main)', + height: 2, + }, + }, + }, + + MuiTab: { + styleOverrides: { + root: { + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 400, + color: 'var(--mui-palette-text-primary)', + textTransform: 'none' as const, + minHeight: 32, + padding: '0.5rem 0.75rem', + borderRadius: 0, + '&.Mui-selected': { + color: 'var(--mui-palette-text-primary)', + backgroundColor: 'var(--mui-palette-background-paper)', + }, + '&:hover': { + backgroundColor: 'var(--mui-palette-background-paper)', + }, + }, + }, + }, + + MuiChip: { + styleOverrides: { + root: { + backgroundColor: 'var(--mui-palette-background-paper)', + color: 'var(--mui-palette-text-primary)', + border: `1px solid ${'var(--mui-palette-divider)'}`, + borderRadius: 0, + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 400, + height: 32, + }, + label: { + color: 'inherit', + padding: '0 0.5rem', + }, + }, + }, + + MuiTooltip: { + styleOverrides: { + tooltip: { + backgroundColor: 'var(--mui-palette-background-default)', + border: `1px solid ${'var(--mui-palette-divider)'}`, + color: 'var(--mui-palette-text-primary)', + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 400, + borderRadius: 0, + maxWidth: '30rem', + }, + arrow: { + color: 'var(--mui-palette-background-default)', + }, + }, + }, + + MuiMenu: { + styleOverrides: { + paper: { + backgroundColor: 'var(--mui-palette-background-default)', + border: `1px solid ${'var(--mui-palette-divider)'}`, + borderRadius: 0, + backgroundImage: 'none', + padding: '0.25rem 0', + }, + }, + }, + + MuiMenuItem: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: { + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + fontWeight: 400, + color: 'var(--mui-palette-text-primary)', + padding: '0.5rem 0.75rem', + '&:hover': { + backgroundColor: 'var(--mui-palette-background-paper)', + }, + '&.Mui-selected': { + backgroundColor: 'var(--mui-palette-background-paper)', + '&:hover': { + backgroundColor: 'var(--mui-palette-divider)', + }, + }, + }, + }, + }, + + MuiDialog: { + styleOverrides: { + paper: { + backgroundColor: 'var(--mui-palette-background-default)', + border: `1px solid ${'var(--mui-palette-divider)'}`, + borderRadius: 12, + backgroundImage: 'none', + }, + root: { + '& .MuiBackdrop-root': { + backdropFilter: 'blur(0.25rem)', + background: 'var(--mui-palette-black-100)', + }, + '& .MuiDialogTitle-root': { + borderBottom: `1px solid ${'var(--mui-palette-background-paper)'}`, + fontSize: '1.5rem', + display: 'flex', + alignItems: 'center', + }, + '& .MuiDialogContent-root': { + padding: 24, + }, + '& .MuiDialogActions-root': { + padding: '1rem 1.5rem', + borderTop: `1px solid ${'var(--mui-palette-background-paper)'}`, + }, + '& .MuiDivider-root': { + borderColor: 'var(--mui-palette-background-paper)', + }, + }, + }, + }, + + // ── MuiOutlinedInput ───────────────────── + MuiOutlinedInput: { + styleOverrides: { + root: { + fontFamily: fontFamilies.body, + color: 'var(--mui-palette-text-primary)', + borderRadius: 0, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: 'var(--mui-palette-divider)', + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: 'var(--mui-palette-text-secondary)', + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: 'var(--mui-palette-primary-main)', + }, + }, + input: { + padding: '0.5rem 0.75rem', + }, + }, + }, + + MuiInputLabel: { + styleOverrides: { + root: { + fontFamily: fontFamilies.body, + fontWeight: 400, + fontSize: '0.875rem', + color: 'var(--mui-palette-text-secondary)', + '&.Mui-focused': { + color: 'var(--mui-palette-text-primary)', + }, + }, + }, + }, + + MuiFormHelperText: { + styleOverrides: { + root: { + fontFamily: fontFamilies.body, + fontSize: '0.75rem', + lineHeight: '1.125rem', + fontWeight: 400, + color: 'var(--mui-palette-text-secondary)', + '&.Mui-error': { + color: '#f44336', + }, + }, + }, + }, + + MuiCircularProgress: { + defaultProps: { + color: 'primary' as const, + }, + styleOverrides: { + root: { + color: 'var(--mui-palette-primary-main)', + }, + }, + }, + + MuiLinearProgress: { + styleOverrides: { + root: { + backgroundColor: 'var(--mui-palette-background-paper)', + borderRadius: 0, + }, + bar: { + backgroundColor: 'var(--mui-palette-primary-main)', + }, + }, + }, + + MuiAlert: { + styleOverrides: { + root: { + borderRadius: 0, + fontFamily: fontFamilies.body, + fontSize: '0.875rem', + }, + }, + }, + + MuiSvgIcon: { + styleOverrides: { + root: { + color: 'inherit', + }, + fontSizeSmall: { + width: '1rem', + height: '1rem', + fontSize: '1rem', + }, + fontSizeMedium: { + width: '1.25rem', + height: '1.25rem', + fontSize: '1.25rem', + }, + fontSizeLarge: { + width: '2rem', + height: '2rem', + fontSize: '2rem', + }, + }, + }, + }, +}); diff --git a/applications/idp-arc/frontend/src/theme/palette.ts b/applications/idp-arc/frontend/src/theme/palette.ts new file mode 100644 index 0000000..299ea4f --- /dev/null +++ b/applications/idp-arc/frontend/src/theme/palette.ts @@ -0,0 +1,81 @@ + +import { alpha } from '@mui/material'; + +export const arcPalette = { + // Standard MUI palette keys + background: { + default: '#1A1A1A', + paper: alpha('#ffffff', 0.05), + }, + primary: { + main: '#613e38', + light: '#8a5e57', + dark: '#3d2522', + contrastText: '#ffffff', + }, + secondary: { + main: '#ffffff', + contrastText: '#1A1A1A', + }, + text: { + primary: '#ffffff', + secondary: alpha('#ffffff', 0.6), + disabled: alpha('#ffffff', 0.1), + }, + divider: alpha('#ffffff', 0.1), + + // ── Custom keys — all become var(--mui-palette--) ────────────── + stroke: { + default: alpha('#ffffff', 0.1), + active: alpha('#ffffff', 0.2), + hover: alpha('#ffffff', 0.15), + }, + content: { + primary: '#ffffff', + secondary: alpha('#ffffff', 0.6), + disabled: alpha('#ffffff', 0.1), + inverse: '#1A1A1A', + }, + white: { + 50: alpha('#ffffff', 0.02), + 100: alpha('#ffffff', 0.03), + 200: alpha('#ffffff', 0.05), + 300: alpha('#ffffff', 0.08), + }, + brand: { + 100: alpha('#613e38', 0.05), // rgba(97,62,56,0.05) + 200: alpha('#613e38', 0.1), // rgba(97,62,56,0.10) + 400: alpha('#613e38', 0.4), // rgba(97,62,56,0.40) + }, + brandGradient: { + warm: alpha('#613e38', 0.4), // brand primary tint — rgba(97,62,56,0.4) + midtone: alpha('#392b22', 0.4), // mid dark-brown tint — rgba(57,43,34,0.4) + cool: alpha('#1d2318', 0.4), // dark olive tint — rgba(29,35,24,0.4) + }, + // Black tints + black: { + 100: alpha('#000000', 0.6), // rgba(0,0,0,0.60) + 200: alpha('#000000', 0.75), // rgba(0,0,0,0.75) — modal backdrop + }, +} as const; + +declare module '@mui/material/styles' { + interface Palette { + stroke: typeof arcPalette.stroke; + content: typeof arcPalette.content; + white: typeof arcPalette.white; + brand: typeof arcPalette.brand; + black: typeof arcPalette.black; + brandGradient: typeof arcPalette.brandGradient; + } + interface PaletteOptions { + stroke?: Partial; + content?: Partial; + white?: Partial; + brand?: Partial; + black?: Partial; + brandGradient?: Partial; + } +} + + diff --git a/applications/idp-arc/frontend/tests/unit/objectStore.test.ts b/applications/idp-arc/frontend/tests/unit/objectStore.test.ts new file mode 100644 index 0000000..8f1df99 --- /dev/null +++ b/applications/idp-arc/frontend/tests/unit/objectStore.test.ts @@ -0,0 +1,18 @@ +// The Scenario 1 object store (infra/publicBucketObjectStore.ts). Run: yarn test:unit +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { objectKey } from '../../src/infra/publicBucketObjectStore' + +test('object key follows uploads////', () => { + assert.equal( + objectKey('four-choice-reversal', '1a2b-3c', 'u-1', 'Tac184_8-25-17.nwb'), + 'uploads/four-choice-reversal/1a2b-3c/u-1/Tac184_8-25-17.nwb', + ) +}) + +test('object key segments are path-safe: no traversal, no slashes, no spaces', () => { + assert.equal( + objectKey('four choice', 'user@lab.org', 'u-1', '../../my cohort.zip'), + 'uploads/four-choice/user-lab.org/u-1/my-cohort.zip', + ) +}) diff --git a/applications/idp-arc/frontend/tests/unit/protocolRepo.test.ts b/applications/idp-arc/frontend/tests/unit/protocolRepo.test.ts new file mode 100644 index 0000000..3c9d9e6 --- /dev/null +++ b/applications/idp-arc/frontend/tests/unit/protocolRepo.test.ts @@ -0,0 +1,20 @@ +// protocols.json's repoZipUrl → the repository and the folder OSB's import unpacks it into. +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { parseRepoZipUrl } from '../../src/core/protocolRepo' + +test('a branch archive unpacks to -', () => { + assert.deepEqual(parseRepoZipUrl('https://codeload.github.com/maracbaylis/four-choice-example/zip/refs/heads/main'), { + owner: 'maracbaylis', repo: 'four-choice-example', ref: 'main', folder: 'four-choice-example-main', + }) +}) + +test('a tag drops its leading v; a commit is used as is', () => { + assert.equal(parseRepoZipUrl('https://codeload.github.com/o/r/zip/refs/tags/v1.2.0')?.folder, 'r-1.2.0') + assert.equal(parseRepoZipUrl('https://codeload.github.com/o/r/zip/72cbc1e7b0d6')?.folder, 'r-72cbc1e7b0d6') +}) + +test('anything else is not a repository', () => { + assert.equal(parseRepoZipUrl(undefined), null) + assert.equal(parseRepoZipUrl('https://github.com/o/r'), null) +}) diff --git a/applications/idp-arc/frontend/tests/unit/protocolTemplates.test.ts b/applications/idp-arc/frontend/tests/unit/protocolTemplates.test.ts new file mode 100644 index 0000000..7170b6b --- /dev/null +++ b/applications/idp-arc/frontend/tests/unit/protocolTemplates.test.ts @@ -0,0 +1,30 @@ +// Protocol templates archive links (core/protocolTemplates.ts). Run: yarn test:unit +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { existsSync, readFileSync } from 'node:fs' +import { templatesZipHref } from '../../src/core/protocolTemplates' + +test('relative archive path is served under the app base URL', () => { + assert.equal(templatesZipHref({ templatesZipUrl: 'protocols_archives/4c.zip' }, '/'), '/protocols_archives/4c.zip') + assert.equal(templatesZipHref({ templatesZipUrl: '/protocols_archives/4c.zip' }, '/idp'), '/idp/protocols_archives/4c.zip') +}) + +test('absolute archive URL is used as-is', () => { + const url = 'https://storage.googleapis.com/maabcd/4c.zip' + assert.equal(templatesZipHref({ templatesZipUrl: url }, '/idp/'), url) +}) + +test('no archive (missing, empty, or no protocol) gives null', () => { + assert.equal(templatesZipHref({}, '/'), null) + assert.equal(templatesZipHref({ templatesZipUrl: '' }, '/'), null) + assert.equal(templatesZipHref(undefined, '/'), null) +}) + +test('every relative templatesZipUrl in protocols.json exists in public/', () => { + const protocols = JSON.parse(readFileSync(new URL('../../src/data/protocols.json', import.meta.url), 'utf8')) + for (const p of protocols) { + const href = templatesZipHref(p, '/') + if (!href || /^[a-z]+:\/\//i.test(href)) continue + assert.ok(existsSync(new URL(`../../public${href}`, import.meta.url)), `${p.id}: public${href} is missing`) + } +}) diff --git a/applications/idp-arc/frontend/tests/unit/runProtocol.test.ts b/applications/idp-arc/frontend/tests/unit/runProtocol.test.ts new file mode 100644 index 0000000..d6179a6 --- /dev/null +++ b/applications/idp-arc/frontend/tests/unit/runProtocol.test.ts @@ -0,0 +1,299 @@ +// The Scenario 1 run (core/use-cases/runProtocol.ts) against in-memory ports. Run: yarn test:unit +import { test, beforeEach } from 'node:test' +import assert from 'node:assert/strict' +import { createRunProtocolUseCase } from '../../src/core/use-cases/runProtocol' +import { RUN_SETTINGS } from '../../src/core/runSettings' +import type { IWorkspaceApi, ImportResourceInput, StartRunInput, WorkspaceResourceState } from '../../src/core/ports/IWorkspaceApi' +import type { IObjectStore } from '../../src/core/ports/IObjectStore' +import type { RunState } from '../../src/core/types' + +beforeEach(() => { + RUN_SETTINGS.runResultTimeoutMs = 20 + RUN_SETTINGS.runStartTimeoutMs = 2 * 60_000 + RUN_SETTINGS.importPollMs = 1 + RUN_SETTINGS.runPollMs = 1 + RUN_SETTINGS.runStartTimeoutMs = 20 + RUN_SETTINGS.runTimeoutMs = 70 * 60_000 +}) + +const FOUR_CHOICE = { + id: 'four-choice-reversal', + name: 'Four-choice reversal digging task', + repoZipUrl: 'https://codeload.github.com/maracbaylis/four-choice-example/zip/refs/heads/main', + notebooksDir: 'notebooks', + inputFormats: ['.xlsx', '.zip'], +} +const auth = { getToken: async () => 'token', tokenParsed: { sub: 'user-1' } } +const file = () => new File(['x'], 'animal_01.xlsx') + +/** + * `runPolls`: how many polls after the run is submitted show OSB's placeholder (its workflow running). + * `runFails`: `notebook`, a notebook fails (its executed notebooks listed in notebooks.failed/); + * `setup`, the run stops before any notebook (nothing listed). + */ +function fakes(opts: { runPolls?: number; importPolls?: number; failUpload?: boolean; resources?: WorkspaceResourceState[][]; runFails?: 'notebook' | 'setup' } = {}) { + const calls: string[] = [] + const imports: ImportResourceInput[] = [] + let runInput: StartRunInput | undefined + let polls = 0 + let runPollsLeft = 0 + const api: IWorkspaceApi = { + async listWorkspaces() { return [] }, + async createWorkspace(_t, name, tags) { calls.push(`create:${name}:${tags?.join(',')}`); return 42 }, + async getWorkspaceResources() { + if (runPollsLeft > 0) { runPollsLeft -= 1; return [{ id: -1, name: 'Refreshing resources' }] } + polls += 1 + if (opts.resources) return opts.resources[Math.min(polls - 1, opts.resources.length - 1)] + if (polls <= (opts.importPolls ?? 2)) return [{ id: -1, name: 'Importing resources into workspace' }] + // What OSB's scan lists once the repository is in: its notebooks; after a successful run, the + // executed ones too; after a failed one, in notebooks.failed/ (its workflow scans either way). + const repo = imports.find((i) => i.resourceType === 'g') + const listed: WorkspaceResourceState[] = repo ? [{ id: 7, name: '01_load.ipynb', status: 'a', path: `${repo.folder}/${repo.name}/notebooks/01_load.ipynb` }] : [] + if (runInput && !opts.runFails) { + for (const nb of runInput.notebooks) listed.push({ id: 8, name: nb, status: 'a', path: `${runInput.results.notebooks}/${nb.split('/').pop()}` }) + } + if (runInput && opts.runFails === 'notebook') { + for (const nb of runInput.notebooks) listed.push({ id: 9, name: nb, status: 'a', path: `${runInput.results.notebooks}.failed/${nb.split('/').pop()}` }) + } + return listed + }, + async importResource(_t, input) { calls.push(`import:${input.resourceType}`); imports.push(input) }, + async startRun(_t, ws, input) { + calls.push(`run:${ws}`); runInput = input; runPollsLeft = opts.runPolls ?? 2 + return { workflow: 'osb-run-notebooks-job-1' } + }, + } + const store: IObjectStore = { + async put(input) { + calls.push('upload') + if (opts.failUpload) throw new Error('403 Forbidden') + return { url: `https://storage.googleapis.com/maabcd/uploads/${input.protocolId}/${input.userSub}/u/${input.file.name}`, key: 'k' } + }, + } + return { api, store, calls, imports, runInput: () => runInput } +} + +async function run(f: ReturnType, input: Partial>[0]> = {}) { + const states: RunState[] = [] + await createRunProtocolUseCase(auth, f.api, f.store)( + { protocol: FOUR_CHOICE, file: file(), workspaceName: 'Four-choice', ...input }, (s) => states.push(s)) + return states +} + +test('uploads, imports both into the run folder, waits, runs, and reports the run folder', async () => { + const f = fakes() + const states = await run(f) + const last = states[states.length - 1] + + assert.equal(last.phase, 'succeeded', last.error) + assert.equal(last.workspaceId, 42) + assert.deepEqual(last.steps.map((s) => s.state), Array(6).fill('succeeded')) + assert.ok(f.calls.includes('create:Four-choice:maabcd:four-choice-reversal')) + + // /run--/: the code and the upload, imported into the run's folder. + const [repo, data] = f.imports + const folder = repo.folder + assert.match(folder, /^four-choice-reversal\/run-four-choice-reversal-\d{4}-\d\d-\d\dT\d\d-\d\d-\d\dZ$/) + assert.equal(last.outputsDir, folder) + assert.deepEqual(repo, { workspaceId: 42, name: 'four-choice-example-main', url: FOUR_CHOICE.repoZipUrl, folder, resourceType: 'g' }) + assert.equal(data.folder, `${folder}/inputs`) + assert.equal(data.resourceType, 'e') + assert.match(data.url, /^https:\/\/storage\.googleapis\.com\/maabcd\/uploads\/four-choice-reversal\/user-1\//) + + assert.deepEqual(f.runInput(), { + repo: { dir: `${folder}/four-choice-example-main`, discard: true }, + setup: { requirements: 'requirements.txt', pythonPath: ['scripts'], install: ['scripts/install.py', 'scripts/setup.py', 'scripts/pyproject.toml'] }, + notebooks: ['notebooks/01_load.ipynb'], + inputDir: `${folder}/inputs`, + outputDir: `${folder}/outputs`, + results: { notebooks: `${folder}/notebooks`, log: `${folder}/run.log` }, + }) +}) + +test('a protocol can override how its repository is set up', async () => { + const f = fakes() + await run(f, { protocol: { ...FOUR_CHOICE, requirements: 'env/requirements.txt', pythonPath: [], install: ['setup/pyproject.toml'] } }) + const { requirements, pythonPath, install } = f.runInput()!.setup! + assert.deepEqual({ requirements, pythonPath, install }, { requirements: 'env/requirements.txt', pythonPath: [], install: ['setup/pyproject.toml'] }) +}) + +test('runs the notebooks directly in the notebooks folder, in byte order of their names', async () => { + const f = fakes() + const listed = (path: string) => ({ id: 7, name: path, status: 'a' as const, path }) + f.api.getWorkspaceResources = async () => { + const repo = f.imports.find((i) => i.resourceType === 'g')! + const nb = `${repo.folder}/${repo.name}/notebooks` + return [`${nb}/10_c.ipynb`, `${nb}/02_b.ipynb`, `${nb}/01_a.ipynb`, `${nb}/old/00_x.ipynb`, `${nb}/.hidden.ipynb`, + `${nb}/notes.md`, `${repo.folder}/${repo.name}/other/01.ipynb`].map(listed) + } + await run(f) + assert.deepEqual(f.runInput()?.notebooks, ['notebooks/01_a.ipynb', 'notebooks/02_b.ipynb', 'notebooks/10_c.ipynb']) +}) + +test('the upload lands before anything is created in OSB; then repo import, data import, run', async () => { + const f = fakes() + await run(f) + const order = (c: string) => f.calls.findIndex((x) => x.startsWith(c)) + assert.ok(order('upload') < order('create')) + assert.ok(order('create') < order('import:g')) + assert.ok(order('import:g') < order('import:e')) + assert.ok(order('import:e') < order('run')) +}) + +test('a failed upload creates no workspace and imports nothing', async () => { + const f = fakes({ failUpload: true }) + await run(f) + assert.deepEqual(f.calls, ['upload']) +}) + +test('an existing workspace is reused, not created', async () => { + const f = fakes() + const states = await run(f, { workspaceId: 7 }) + assert.ok(!f.calls.some((c) => c.startsWith('create'))) + assert.ok(f.calls.includes('run:7')) + assert.equal(states[states.length - 1].phase, 'succeeded') +}) + +test('without a file: no upload, no data import, the notebooks run on the example data', async () => { + const f = fakes() + const states = await run(f, { file: null }) + const last = states[states.length - 1] + assert.equal(last.phase, 'succeeded') + assert.deepEqual(f.imports.map((i) => i.resourceType), ['g']) + assert.equal(f.runInput()?.inputDir, undefined) + assert.deepEqual(last.steps.filter((s) => s.state === 'skipped').map((s) => s.id), ['upload', 'data']) +}) + +test('a failed upload marks only the upload step failed', async () => { + const states = await run(fakes({ failUpload: true })) + const last = states[states.length - 1] + assert.equal(last.phase, 'failed') + // The user reads plain words; the 403 goes to the console. + assert.equal(last.error, 'Your file could not be uploaded. Check your connection and try again.') + assert.deepEqual(last.steps.filter((s) => s.state === 'failed').map((s) => s.id), ['upload']) +}) + +test('the run is followed through the workspace\'s placeholder: running while listed, finished once gone', async () => { + const states = await run(fakes({ runPolls: 3 })) + assert.ok(states.some((s) => s.steps.find((x) => x.id === 'run')?.detail === 'Running the notebooks')) + const last = states[states.length - 1] + assert.equal(last.phase, 'succeeded') + assert.equal(last.message, 'The analysis has finished') + assert.match(last.steps.find((s) => s.id === 'run')?.detail ?? '', /^Results in four-choice-reversal\/run-/) +}) + +test('a run that ends between two polls is still judged by its executed notebooks', async () => { + RUN_SETTINGS.runStartTimeoutMs = 10 + assert.equal((await run(fakes({ runPolls: 0 }))).pop()!.phase, 'succeeded') + assert.equal((await run(fakes({ runPolls: 0, runFails: 'notebook' }))).pop()!.phase, 'failed') +}) + +test('an import OSB marks as failed stops the run', async () => { + const f = fakes() + f.api.getWorkspaceResources = async () => { + const data = f.imports.find((i) => i.resourceType === 'e')! + return [{ id: 3, name: 'animal_01.xlsx', status: 'e', path: `${data.folder}/animal_01.xlsx` }] + } + const last = (await run(f)).pop()! + assert.equal(last.phase, 'failed') + assert.equal(last.error, 'The files could not be copied into the workspace. Please try again.') + assert.ok(!f.calls.some((c) => c.startsWith('run'))) +}) + +test('a reused workspace\'s older failed or stuck resources don\'t affect this run', async () => { + const f = fakes() + const list = f.api.getWorkspaceResources.bind(f.api) + f.api.getWorkspaceResources = async (t, ws) => [ + { id: 1, name: 'old.xlsx', status: 'e', path: 'four-choice-reversal/run-old/inputs/old.xlsx' }, + { id: 2, name: 'stuck.nwb', status: 'p', path: 'stuck.nwb' }, + ...await list(t, ws), + ] + assert.equal((await run(f, { workspaceId: 7 })).pop()!.phase, 'succeeded') +}) + +test('imports that leave no notebooks in the notebooks folder stop the run', async () => { + const f = fakes({ resources: [[{ id: 7, name: '01_load.ipynb', status: 'a', path: 'elsewhere/notebooks/01_load.ipynb' }]] }) + const last = (await run(f)).pop()! + assert.equal(last.phase, 'failed') + assert.match(last.error ?? '', /no notebooks to run/) + assert.equal(last.steps.find((s) => s.id === 'imports')?.state, 'failed') + assert.ok(!f.calls.some((c) => c.startsWith('run'))) +}) + +test('a file the protocol cannot read fails before anything is uploaded or created', async () => { + const f = fakes() + const last = (await run(f, { file: new File(['x'], 'notes.txt') })).pop()! + assert.equal(last.phase, 'failed') + assert.deepEqual(f.calls, []) +}) + +test('an empty file fails before anything is uploaded or created', async () => { + const f = fakes() + const last = (await run(f, { file: new File([], 'animal_01.xlsx') })).pop()! + assert.equal(last.phase, 'failed') + assert.equal(last.error, 'animal_01.xlsx is empty.') + assert.deepEqual(f.calls, []) +}) + +test('a repository zip that is not GitHub codeload is not offered', async () => { + const f = fakes() + const last = (await run(f, { protocol: { ...FOUR_CHOICE, repoZipUrl: 'http://example.org/x.zip' } })).pop()! + assert.equal(last.phase, 'failed') + assert.match(last.error ?? '', /isn't available yet/) +}) + +test('a run whose notebook failed is seen in notebooks.failed/, and says so in plain words', async () => { + RUN_SETTINGS.runResultTimeoutMs = 60_000 // decided by the listing, not by waiting + const f = fakes({ runFails: 'notebook' }) + const last = (await run(f)).pop()! + assert.equal(last.phase, 'failed') + assert.equal(last.steps.find((s) => s.id === 'run')?.state, 'failed') + assert.equal(last.error, 'The analysis did not finish. Open the workspace to see what happened.') +}) + +test('a run that stops before any notebook (nothing listed) failed, and says so in plain words', async () => { + const f = fakes({ runFails: 'setup' }) + const last = (await run(f)).pop()! + assert.equal(last.phase, 'failed') + assert.equal(last.steps.find((s) => s.id === 'run')?.state, 'failed') + assert.equal(last.error, 'The analysis did not finish. Open the workspace to see what happened.') +}) + +test('a protocol without a repository fails clearly', async () => { + const f = fakes() + const last = (await run(f, { protocol: { id: 'open-field', name: 'Open field task' } })).pop()! + assert.equal(last.phase, 'failed') + assert.match(last.error ?? '', /isn't available yet/) +}) + +test('a failed repository import marks the repository step', async () => { + const f = fakes() + f.api.importResource = async () => { throw new Error('500 MalformedModelDictionaryError') } + const last = (await run(f)).pop()! + assert.equal(last.phase, 'failed') + assert.deepEqual(last.steps.filter((s) => s.state === 'failed').map((s) => s.id), ['repo']) + assert.equal(last.steps.find((s) => s.id === 'upload')?.state, 'succeeded') +}) + +test('a run still going past the watch limit stops being followed, and says so', async () => { + RUN_SETTINGS.runTimeoutMs = 5 + const last = (await run(fakes({ runPolls: 1_000 }))).pop()! + assert.equal(last.phase, 'stillRunning') + assert.match(last.message, /Still running in the workspace/) + assert.equal(last.steps.find((s) => s.id === 'run')?.detail, 'Still running in the workspace') + assert.ok(!last.steps.some((s) => s.state === 'running')) +}) + +test('an ended session mid-run asks to sign in again', async () => { + let calls = 0 + const expiring = { ...auth, getToken: async () => { + if (++calls > 2) throw new Error('No access token available. Please sign in again.') + return 'token' + } } + const states: RunState[] = [] + await createRunProtocolUseCase(expiring, fakes().api, fakes().store)( + { protocol: FOUR_CHOICE, file: file(), workspaceName: 'Four-choice' }, (s) => states.push(s)) + const last = states.pop()! + assert.equal(last.phase, 'failed') + assert.match(last.error ?? '', /sign in again/) +}) diff --git a/applications/idp-arc/frontend/tests/unit/ts-resolve.mjs b/applications/idp-arc/frontend/tests/unit/ts-resolve.mjs new file mode 100644 index 0000000..0887bb5 --- /dev/null +++ b/applications/idp-arc/frontend/tests/unit/ts-resolve.mjs @@ -0,0 +1,20 @@ +// Lets `node --test` load src/ modules that import each other without a file extension, the way +// Vite resolves them (`import … from '../dandisetWorkspaceLink'`). Registered via `--import` in +// the `test:unit` script. Relative specifiers without an extension get `.ts`, then `/index.ts`. +import { register } from 'node:module' + +register('data:text/javascript,' + encodeURIComponent(` + export async function resolve(specifier, context, next) { + try { + return await next(specifier, context) + } catch (err) { + const relative = specifier.startsWith('./') || specifier.startsWith('../') + const hasExt = /\\.[cm]?[jt]sx?$|\\.json$/.test(specifier) + if (err?.code !== 'ERR_MODULE_NOT_FOUND' || !relative || hasExt) throw err + for (const candidate of [specifier + '.ts', specifier + '/index.ts']) { + try { return await next(candidate, context) } catch { /* try the next one */ } + } + throw err + } + } +`)) diff --git a/applications/idp-arc/frontend/tests/unit/userMessages.test.ts b/applications/idp-arc/frontend/tests/unit/userMessages.test.ts new file mode 100644 index 0000000..faba529 --- /dev/null +++ b/applications/idp-arc/frontend/tests/unit/userMessages.test.ts @@ -0,0 +1,20 @@ +// What the user reads when a run fails (core/userMessages.ts). Run: yarn test:unit +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { userMessage, UserFacingError } from '../../src/core/userMessages' + +test('an error written for the user is shown as is', () => { + assert.equal(userMessage(new UserFacingError('Not signed in; please sign in again', 'token expired'), 'upload'), + 'Not signed in; please sign in again') +}) + +test('any other error shows only its step\'s plain sentence', () => { + assert.equal(userMessage(new Error('Starting the notebooks failed: 500 Internal Server Error'), 'run'), + 'The analysis did not finish. Open the workspace to see what happened.') + assert.equal(userMessage(new Error('boom')), 'Something went wrong. Please try again.') +}) + +test('an ended session says to sign in again, whichever step asked for the token', () => { + assert.equal(userMessage(new Error('No access token available. Please sign in again.'), 'imports'), + 'Your session has expired. Please sign in again.') +}) diff --git a/applications/idp-arc/frontend/tests/unit/workspaceLayout.test.ts b/applications/idp-arc/frontend/tests/unit/workspaceLayout.test.ts new file mode 100644 index 0000000..89fa67b --- /dev/null +++ b/applications/idp-arc/frontend/tests/unit/workspaceLayout.test.ts @@ -0,0 +1,16 @@ +// Where a run's files go (core/workspaceLayout.ts). Run: yarn test:unit +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { workspaceLayout } from '../../src/core/workspaceLayout' + +test('one folder per protocol, one per run inside it, named by the UTC time without colons', () => { + const layout = workspaceLayout('four-choice-reversal', new Date('2026-10-05T10:54:43.123Z')) + assert.deepEqual(layout, { + run: 'four-choice-reversal/run-four-choice-reversal-2026-10-05T10-54-43Z', + inputs: 'four-choice-reversal/run-four-choice-reversal-2026-10-05T10-54-43Z/inputs', + outputs: 'four-choice-reversal/run-four-choice-reversal-2026-10-05T10-54-43Z/outputs', + notebooks: 'four-choice-reversal/run-four-choice-reversal-2026-10-05T10-54-43Z/notebooks', + failedNotebooks: 'four-choice-reversal/run-four-choice-reversal-2026-10-05T10-54-43Z/notebooks.failed', + log: 'four-choice-reversal/run-four-choice-reversal-2026-10-05T10-54-43Z/run.log', + }) +}) diff --git a/applications/idp-arc/frontend/vite.config.ts b/applications/idp-arc/frontend/vite.config.ts index c10c402..5509adb 100644 --- a/applications/idp-arc/frontend/vite.config.ts +++ b/applications/idp-arc/frontend/vite.config.ts @@ -1,6 +1,26 @@ -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' +import http from 'node:http' +import https from 'node:https' + +// Must use loadEnv(), not process.env — Vite doesn't load .env into process.env for this +// file, so process.env here silently falls back to the production domain. +const env = loadEnv(process.env.NODE_ENV ?? 'development', process.cwd(), '') +const BASE_DOMAIN = env.VITE_OSB_BASE_DOMAIN || 'v2dev.opensourcebrain.org' +const PROTOCOL = env.VITE_OSB_PROTOCOL || 'https' + +const WWW_ORIGIN = `${PROTOCOL}://www.${BASE_DOMAIN}` + +// eslint-disable-next-line no-console +console.log(`[vite] proxying OSB -> ${WWW_ORIGIN}`) + +// IPv4 lookups and reused connections for every proxied call. macOS resolves `*.local` names +// (local minikube: `osb.local`) through multicast DNS first, and the IPv6 query only falls back +// to /etc/hosts after a 5 s timeout, which Node paid on each new upstream connection: every +// proxied request took 5 s or more (measured 30 Sep: 5018 ms default lookup vs 6 ms for IPv4). +const agentOptions = { keepAlive: true, family: 4 } +const upstreamAgent = PROTOCOL === 'https' ? new https.Agent(agentOptions) : new http.Agent(agentOptions) export default defineConfig({ plugins: [react()], @@ -10,12 +30,24 @@ export default defineConfig({ assetsDir: 'assets', }, server: { + host: '0.0.0.0', port: 5173, - open: true + strictPort: true, + watch: { + usePolling: true + }, + proxy: { + '/api-proxy': { + target: WWW_ORIGIN, + agent: upstreamAgent, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api-proxy/, ''), + }, + }, }, resolve: { alias: { '@': path.resolve(__dirname, './src'), - } + }, } }) diff --git a/applications/idp-arc/frontend/yarn.lock b/applications/idp-arc/frontend/yarn.lock index afa5e9f..ecd6b8e 100644 --- a/applications/idp-arc/frontend/yarn.lock +++ b/applications/idp-arc/frontend/yarn.lock @@ -2,9 +2,9 @@ # yarn lockfile v1 -"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz" integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== dependencies: "@babel/helper-validator-identifier" "^7.28.5" @@ -13,12 +13,12 @@ "@babel/compat-data@^7.28.6": version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz" integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== "@babel/core@^7.24.4", "@babel/core@^7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz" integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== dependencies: "@babel/code-frame" "^7.29.0" @@ -39,7 +39,7 @@ "@babel/generator@^7.29.0": version "7.29.1" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz" integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== dependencies: "@babel/parser" "^7.29.0" @@ -50,7 +50,7 @@ "@babel/helper-compilation-targets@^7.28.6": version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz" integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== dependencies: "@babel/compat-data" "^7.28.6" @@ -61,12 +61,12 @@ "@babel/helper-globals@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== -"@babel/helper-module-imports@^7.28.6": +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.28.6": version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz" integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== dependencies: "@babel/traverse" "^7.28.6" @@ -74,7 +74,7 @@ "@babel/helper-module-transforms@^7.28.6": version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz" integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== dependencies: "@babel/helper-module-imports" "^7.28.6" @@ -83,27 +83,27 @@ "@babel/helper-plugin-utils@^7.27.1": version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz" integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== "@babel/helper-string-parser@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== "@babel/helper-validator-identifier@^7.28.5": version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== "@babel/helper-validator-option@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== "@babel/helpers@^7.28.6": version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz" integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== dependencies: "@babel/template" "^7.28.6" @@ -111,28 +111,33 @@ "@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.4", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz" integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== dependencies: "@babel/types" "^7.29.0" "@babel/plugin-transform-react-jsx-self@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz" integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-react-jsx-source@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz" integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.29.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.29.2" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz" + integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== + "@babel/template@^7.28.6": version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz" integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== dependencies: "@babel/code-frame" "^7.28.6" @@ -141,7 +146,7 @@ "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz" integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== dependencies: "@babel/code-frame" "^7.29.0" @@ -154,12 +159,119 @@ "@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz" integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" +"@emotion/babel-plugin@^11.13.5": + version "11.13.5" + resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz" + integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.3.3" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.14.0": + version "11.14.0" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz" + integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@emotion/is-prop-valid@^1.3.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz" + integrity sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw== + dependencies: + "@emotion/memoize" "^0.9.0" + +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.14.0": + version "11.14.0" + resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz" + integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/cache" "^11.14.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.3.3": + version "1.3.3" + resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz" + integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== + dependencies: + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.2" + csstype "^3.0.2" + +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== + +"@emotion/styled@^11.14.1": + version "11.14.1" + resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz" + integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/is-prop-valid" "^1.3.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== + +"@emotion/use-insertion-effect-with-fallbacks@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz" + integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== + +"@emotion/utils@^1.4.2": + version "1.4.2" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz" + integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== + +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== + "@esbuild/aix-ppc64@0.27.3": version "0.27.3" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2" @@ -182,7 +294,7 @@ "@esbuild/darwin-arm64@0.27.3": version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz#9f6cac72b3a8532298a6a4493ed639a8988e8abd" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz" integrity sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg== "@esbuild/darwin-x64@0.27.3": @@ -292,19 +404,19 @@ "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz" integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== dependencies: eslint-visitor-keys "^3.4.3" "@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2": version "4.12.2" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz" integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== "@eslint/config-array@^0.21.1": version "0.21.1" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713" + resolved "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz" integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA== dependencies: "@eslint/object-schema" "^2.1.7" @@ -313,21 +425,21 @@ "@eslint/config-helpers@^0.4.2": version "0.4.2" - resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda" + resolved "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz" integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw== dependencies: "@eslint/core" "^0.17.0" "@eslint/core@^0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c" + resolved "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz" integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ== dependencies: "@types/json-schema" "^7.0.15" "@eslint/eslintrc@^3.3.1": version "3.3.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz#26393a0806501b5e2b6a43aa588a4d8df67880ac" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz" integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ== dependencies: ajv "^6.12.4" @@ -342,17 +454,17 @@ "@eslint/js@9.39.2", "@eslint/js@^9.39.1": version "9.39.2" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599" + resolved "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz" integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA== "@eslint/object-schema@^2.1.7": version "2.1.7" - resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad" + resolved "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz" integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== "@eslint/plugin-kit@^0.4.1": version "0.4.1" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2" + resolved "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz" integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA== dependencies: "@eslint/core" "^0.17.0" @@ -360,12 +472,12 @@ "@humanfs/core@^0.19.1": version "0.19.1" - resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== "@humanfs/node@^0.16.6": version "0.16.7" - resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26" + resolved "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz" integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== dependencies: "@humanfs/core" "^0.19.1" @@ -373,17 +485,17 @@ "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": version "0.4.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" + resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" @@ -391,7 +503,7 @@ "@jridgewell/remapping@^2.3.5": version "2.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz" integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== dependencies: "@jridgewell/gen-mapping" "^0.3.5" @@ -399,25 +511,121 @@ "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.5" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": version "0.3.31" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@mui/core-downloads-tracker@^9.0.0": + version "9.0.0" + resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.0.tgz" + integrity sha512-uwQNGkhv0lf7ufxw6QXev77BW6pWbW+7uxYjU5+rfp4lBkFtMEgJCsarTM3Tn+i0lGx6+Ol2u88JdGXr0GDskA== + +"@mui/icons-material@^9.0.0": + version "9.0.0" + resolved "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.0.0.tgz" + integrity sha512-oDwyvI6LgjWRC9MBcSGvLkPud9S9ELgSBQFYxa1rYcZn6Br55dn22SyvsPDMsn0G8OndFk53iMT45W5mNqrogw== + dependencies: + "@babel/runtime" "^7.29.2" + +"@mui/material@^9.0.0": + version "9.0.0" + resolved "https://registry.npmjs.org/@mui/material/-/material-9.0.0.tgz" + integrity sha512-+VP/oQCDhDR87NQQgXnNBG8dwy6GNuQLnenS1pZvkbn2dKFSxRSRMybTpH9xUxXP+316mlYDy5CSbYtusnCWtw== + dependencies: + "@babel/runtime" "^7.29.2" + "@mui/core-downloads-tracker" "^9.0.0" + "@mui/system" "^9.0.0" + "@mui/types" "^9.0.0" + "@mui/utils" "^9.0.0" + "@popperjs/core" "^2.11.8" + "@types/react-transition-group" "^4.4.12" + clsx "^2.1.1" + csstype "^3.2.3" + prop-types "^15.8.1" + react-is "^19.2.4" + react-transition-group "^4.4.5" + +"@mui/private-theming@^9.0.0": + version "9.0.0" + resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.0.0.tgz" + integrity sha512-JtuZoaiCqwD6vjgYu6Xp3T7DZkrxJlgtDz5yESzhI34fEX5hHMh2VJUbuL9UOg8xrfIFMrq6dcYoH/7Zi4G0RA== + dependencies: + "@babel/runtime" "^7.29.2" + "@mui/utils" "^9.0.0" + prop-types "^15.8.1" + +"@mui/styled-engine@^9.0.0": + version "9.0.0" + resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.0.0.tgz" + integrity sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg== + dependencies: + "@babel/runtime" "^7.29.2" + "@emotion/cache" "^11.14.0" + "@emotion/serialize" "^1.3.3" + "@emotion/sheet" "^1.4.0" + csstype "^3.2.3" + prop-types "^15.8.1" + +"@mui/system@^9.0.0": + version "9.0.0" + resolved "https://registry.npmjs.org/@mui/system/-/system-9.0.0.tgz" + integrity sha512-YnC5Zg6j04IxiLc/boAKs0464jfZlLFVa7mf5E8lF0XOtZVUvG6R6gJK50lgUYdaaLdyLfxF6xR7LaPuEpeT/g== + dependencies: + "@babel/runtime" "^7.29.2" + "@mui/private-theming" "^9.0.0" + "@mui/styled-engine" "^9.0.0" + "@mui/types" "^9.0.0" + "@mui/utils" "^9.0.0" + clsx "^2.1.1" + csstype "^3.2.3" + prop-types "^15.8.1" + +"@mui/types@^9.0.0": + version "9.0.0" + resolved "https://registry.npmjs.org/@mui/types/-/types-9.0.0.tgz" + integrity sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg== + dependencies: + "@babel/runtime" "^7.29.2" + +"@mui/utils@^9.0.0": + version "9.0.0" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-9.0.0.tgz" + integrity sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg== + dependencies: + "@babel/runtime" "^7.29.2" + "@mui/types" "^9.0.0" + "@types/prop-types" "^15.7.15" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^19.2.4" + +"@playwright/test@^1.52.0": + version "1.58.2" + resolved "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz" + integrity sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA== + dependencies: + playwright "1.58.2" + +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + "@rolldown/pluginutils@1.0.0-rc.3": version "1.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz#8a88cc92a0f741befc7bc109cb1a4c6b9408e1c5" + resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz" integrity sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q== "@rollup/rollup-android-arm-eabi@4.57.1": @@ -432,7 +640,7 @@ "@rollup/rollup-darwin-arm64@4.57.1": version "4.57.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz#1e99ab04c0b8c619dd7bbde725ba2b87b55bfd81" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz" integrity sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg== "@rollup/rollup-darwin-x64@4.57.1": @@ -547,7 +755,7 @@ "@types/babel__core@^7.20.5": version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== dependencies: "@babel/parser" "^7.20.7" @@ -558,14 +766,14 @@ "@types/babel__generator@*": version "7.27.0" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz" integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== dependencies: "@babel/parser" "^7.1.0" @@ -573,43 +781,63 @@ "@types/babel__traverse@*": version "7.28.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz" integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== dependencies: "@babel/types" "^7.28.2" "@types/estree@1.0.8", "@types/estree@^1.0.6": version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== "@types/json-schema@^7.0.15": version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/node@^24.10.1": version "24.10.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.13.tgz#2fac25c0e30f3848e19912c3b8791a28370e9e07" + resolved "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz" integrity sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg== dependencies: undici-types "~7.16.0" +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + +"@types/prop-types@^15.7.15": + version "15.7.15" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== + "@types/react-dom@^19.2.3": version "19.2.3" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz" integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== +"@types/react-transition-group@^4.4.12": + version "4.4.12" + resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== + "@types/react@^19.2.7": version "19.2.14" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" + resolved "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz" integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== dependencies: csstype "^3.2.2" +"@types/spark-md5@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.5.tgz#eddec8639217e518c26e9e221ff56bf5f5f5c900" + integrity sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg== + "@typescript-eslint/eslint-plugin@8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz#5aec3db807a6b8437ea5d5ebf7bd16b4119aba8d" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz" integrity sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw== dependencies: "@eslint-community/regexpp" "^4.12.2" @@ -623,7 +851,7 @@ "@typescript-eslint/parser@8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.0.tgz#8ecff1678b8b1a742d29c446ccf5eeea7f971d72" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz" integrity sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg== dependencies: "@typescript-eslint/scope-manager" "8.56.0" @@ -634,7 +862,7 @@ "@typescript-eslint/project-service@8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.0.tgz#bb8562fecd8f7922e676fc6a1189c20dd7991d73" + resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz" integrity sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg== dependencies: "@typescript-eslint/tsconfig-utils" "^8.56.0" @@ -643,7 +871,7 @@ "@typescript-eslint/scope-manager@8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz#604030a4c6433df3728effdd441d47f45a86edb4" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz" integrity sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w== dependencies: "@typescript-eslint/types" "8.56.0" @@ -651,12 +879,12 @@ "@typescript-eslint/tsconfig-utils@8.56.0", "@typescript-eslint/tsconfig-utils@^8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz#2538ce83cbc376e685487960cbb24b65fe2abc4e" + resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz" integrity sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg== "@typescript-eslint/type-utils@8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz#72b4edc1fc73988998f1632b3ec99c2a66eaac6e" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz" integrity sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA== dependencies: "@typescript-eslint/types" "8.56.0" @@ -667,12 +895,12 @@ "@typescript-eslint/types@8.56.0", "@typescript-eslint/types@^8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.0.tgz#a2444011b9a98ca13d70411d2cbfed5443b3526a" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz" integrity sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ== "@typescript-eslint/typescript-estree@8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz#fadbc74c14c5bac947db04980ff58bb178701c2e" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz" integrity sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q== dependencies: "@typescript-eslint/project-service" "8.56.0" @@ -687,7 +915,7 @@ "@typescript-eslint/utils@8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.0.tgz#063ce6f702ec603de1b83ee795ed5e877d6f7841" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz" integrity sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ== dependencies: "@eslint-community/eslint-utils" "^4.9.1" @@ -697,7 +925,7 @@ "@typescript-eslint/visitor-keys@8.56.0": version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz#7d6592ab001827d3ce052155edf7ecad19688d7d" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz" integrity sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg== dependencies: "@typescript-eslint/types" "8.56.0" @@ -705,7 +933,7 @@ "@vitejs/plugin-react@^5.1.1": version "5.1.4" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz#5b477e060bf612a7394c4febacc5de33a219b0e4" + resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz" integrity sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA== dependencies: "@babel/core" "^7.29.0" @@ -717,17 +945,17 @@ acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn@^8.15.0: version "8.15.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== ajv@^6.12.4: version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -737,29 +965,38 @@ ajv@^6.12.4: ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" argparse@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== baseline-browser-mapping@^2.9.0: version "2.9.19" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488" + resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz" integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg== brace-expansion@^1.1.7: version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" @@ -767,14 +1004,14 @@ brace-expansion@^1.1.7: brace-expansion@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" browserslist@^4.24.0: version "4.28.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz" integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== dependencies: baseline-browser-mapping "^2.9.0" @@ -785,78 +1022,124 @@ browserslist@^4.24.0: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== caniuse-lite@^1.0.30001759: version "1.0.30001770" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz#4dc47d3b263a50fbb243448034921e0a88591a84" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz" integrity sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw== chalk@^4.0.0: version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + convert-source-map@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie@^1.0.1: + version "1.1.1" + resolved "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cross-spawn@^7.0.6: version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" which "^2.0.1" -csstype@^3.2.2: +csstype@^3.0.2, csstype@^3.2.2, csstype@^3.2.3: version "3.2.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.3: version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" deep-is@^0.1.3: version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + electron-to-chromium@^1.5.263: version "1.5.286" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz#142be1ab5e1cd5044954db0e5898f60a4960384e" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz" integrity sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A== +error-ex@^1.3.1: + version "1.3.4" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + esbuild@^0.27.0: version "0.27.3" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz" integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== optionalDependencies: "@esbuild/aix-ppc64" "0.27.3" @@ -888,17 +1171,17 @@ esbuild@^0.27.0: escalade@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== eslint-plugin-react-hooks@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz" integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA== dependencies: "@babel/core" "^7.24.4" @@ -909,12 +1192,12 @@ eslint-plugin-react-hooks@^7.0.1: eslint-plugin-react-refresh@^0.4.24: version "0.4.26" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz#2bcdd109ea9fb4e0b56bb1b5146cf8841b21b626" + resolved "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz" integrity sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ== eslint-scope@^8.4.0: version "8.4.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz" integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== dependencies: esrecurse "^4.3.0" @@ -922,22 +1205,22 @@ eslint-scope@^8.4.0: eslint-visitor-keys@^3.4.3: version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint-visitor-keys@^4.2.1: version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== eslint-visitor-keys@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz#b9aa1a74aa48c44b3ae46c1597ce7171246a94a9" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz" integrity sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q== eslint@^9.39.1: version "9.39.2" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c" + resolved "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz" integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw== dependencies: "@eslint-community/eslint-utils" "^4.8.0" @@ -977,7 +1260,7 @@ eslint@^9.39.1: espree@^10.0.1, espree@^10.4.0: version "10.4.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" + resolved "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz" integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== dependencies: acorn "^8.15.0" @@ -986,58 +1269,63 @@ espree@^10.0.1, espree@^10.4.0: esquery@^1.5.0: version "1.7.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz" integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== dependencies: estraverse "^5.1.0" esrecurse@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: estraverse "^5.2.0" estraverse@^5.1.0, estraverse@^5.2.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-json-stable-stringify@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-levenshtein@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fdir@^6.5.0: version "6.5.0" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== file-entry-cache@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: flat-cache "^4.0.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" @@ -1045,7 +1333,7 @@ find-up@^5.0.0: flat-cache@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz" integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: flatted "^3.2.9" @@ -1053,66 +1341,104 @@ flat-cache@^4.0.0: flatted@^3.2.9: version "3.3.3" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== glob-parent@^6.0.2: version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" globals@^14.0.0: version "14.0.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz" integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== globals@^16.5.0: version "16.5.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-16.5.0.tgz#ccf1594a437b97653b2be13ed4d8f5c9f850cac1" + resolved "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz" integrity sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ== has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +hasown@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz" + integrity sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg== + dependencies: + function-bind "^1.1.2" + hermes-estree@0.25.1: version "0.25.1" - resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480" + resolved "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz" integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw== hermes-parser@^0.25.1: version "0.25.1" - resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1" + resolved "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz" integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA== dependencies: hermes-estree "0.25.1" +hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + +i18next@^26.0.3: + version "26.0.3" + resolved "https://registry.npmjs.org/i18next/-/i18next-26.0.3.tgz" + integrity sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg== + dependencies: + "@babel/runtime" "^7.29.2" + ignore@^5.2.0: version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== ignore@^7.0.5: version "7.0.5" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz" integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== import-fresh@^3.2.1: version "3.3.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" @@ -1120,134 +1446,173 @@ import-fresh@^3.2.1: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-glob@^4.0.0, is-glob@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.1.1: version "4.1.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz" integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" jsesc@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== json-buffer@3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json5@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +keycloak-js@^26.2.3: + version "26.2.3" + resolved "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.3.tgz" + integrity sha512-widjzw/9T6bHRgEp6H/Se3NCCarU7u5CwFKBcwtu7xfA1IfdZb+7Q7/KGusAnBo34Vtls8Oz9vzSqkQvQ7+b4Q== + keyv@^4.5.4: version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" levn@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: prelude-ls "^1.2.1" type-check "~0.4.0" +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + locate-path@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: p-locate "^5.0.0" lodash.merge@^4.6.2: version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lru-cache@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" minimatch@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" minimatch@^9.0.5: version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== nanoid@^3.3.11: version "3.3.11" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== node-releases@^2.0.27: version "2.0.27" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz" integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + optionator@^0.9.3: version "0.9.4" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" @@ -1259,48 +1624,82 @@ optionator@^0.9.3: p-limit@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" p-locate@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: p-limit "^3.0.2" parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picocolors@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== +playwright-core@1.58.2: + version "1.58.2" + resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz" + integrity sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg== + +playwright@1.58.2: + version "1.58.2" + resolved "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz" + integrity sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A== + dependencies: + playwright-core "1.58.2" + optionalDependencies: + fsevents "2.3.2" + postcss@^8.5.6: version "8.5.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: nanoid "^3.3.11" @@ -1309,39 +1708,102 @@ postcss@^8.5.6: prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prop-types@^15.6.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + punycode@^2.1.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== react-dom@^19.2.0: version "19.2.4" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz" integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ== dependencies: scheduler "^0.27.0" +react-i18next@^17.0.2: + version "17.0.2" + resolved "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.2.tgz" + integrity sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA== + dependencies: + "@babel/runtime" "^7.29.2" + html-parse-stringify "^3.0.1" + use-sync-external-store "^1.6.0" + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^19.2.4: + version "19.2.5" + resolved "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz" + integrity sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ== + react-refresh@^0.18.0: version "0.18.0" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz" integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== +react-router-dom@^7.14.2: + version "7.14.2" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz" + integrity sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ== + dependencies: + react-router "7.14.2" + +react-router@7.14.2: + version "7.14.2" + resolved "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz" + integrity sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw== + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^19.2.0: version "19.2.4" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a" + resolved "https://registry.npmjs.org/react/-/react-19.2.4.tgz" integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== resolve-from@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve@^1.19.0: + version "1.22.12" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + rollup@^4.43.0: version "4.57.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.57.1.tgz#947f70baca32db2b9c594267fe9150aa316e5a88" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz" integrity sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A== dependencies: "@types/estree" "1.0.8" @@ -1375,51 +1837,76 @@ rollup@^4.43.0: scheduler@^0.27.0: version "0.27.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz" integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== semver@^6.3.1: version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.7.3: version "7.7.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== source-map-js@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +spark-md5@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc" + integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw== + strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + 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== + tinyglobby@^0.2.15: version "0.2.15" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz" integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== dependencies: fdir "^6.5.0" @@ -1427,19 +1914,19 @@ tinyglobby@^0.2.15: ts-api-utils@^2.4.0: version "2.4.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" + resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz" integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: prelude-ls "^1.2.1" typescript-eslint@^8.48.0: version "8.56.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.56.0.tgz#f4686ccaaf2fb86daf0133820da40ca5961a2236" + resolved "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz" integrity sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg== dependencies: "@typescript-eslint/eslint-plugin" "8.56.0" @@ -1449,17 +1936,17 @@ typescript-eslint@^8.48.0: typescript@~5.9.3: version "5.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== undici-types@~7.16.0: version "7.16.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz" integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== update-browserslist-db@^1.2.0: version "1.2.3" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz" integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== dependencies: escalade "^3.2.0" @@ -1467,14 +1954,19 @@ update-browserslist-db@^1.2.0: uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" +use-sync-external-store@^1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + vite@^7.3.1: version "7.3.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507" + resolved "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz" integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== dependencies: esbuild "^0.27.0" @@ -1486,34 +1978,44 @@ vite@^7.3.1: optionalDependencies: fsevents "~2.3.3" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" word-wrap@^1.2.5: version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== yallist@^3.0.2: version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml@^1.10.0: + version "1.10.3" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz" + integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA== + yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== "zod-validation-error@^3.5.0 || ^4.0.0": version "4.0.2" - resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918" + resolved "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz" integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ== "zod@^3.25.0 || ^4.0.0": version "4.3.6" - resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" + resolved "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz" integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg== diff --git a/deployment/codefresh-dev.yaml b/deployment/codefresh-dev.yaml index da9a795..70bc13f 100644 --- a/deployment/codefresh-dev.yaml +++ b/deployment/codefresh-dev.yaml @@ -142,6 +142,7 @@ steps: build_arguments: - NOCACHE=${{CF_BUILD_ID}} - CLOUDHARNESS_FRONTEND_BUILD=${{REGISTRY}}/cloud-harness/cloudharness-frontend-build:${{CLOUDHARNESS_FRONTEND_BUILD_TAG}} + - VITE_UPLOAD_BUCKET_URL=${{VITE_UPLOAD_BUCKET_URL}} image_name: cloud-harness/idp-arc title: Idp arc working_directory: ./applications/idp-arc