diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5701c7c..c2c85ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: name: Build and Validate runs-on: ubuntu-latest env: + NODE_ENV: test IS_NEXT_BUILD: "1" SECRET_KEY: ci_secret_key_ideon APP_PORT: ${{ vars.APP_PORT }} @@ -49,6 +50,10 @@ jobs: run: npm run check - name: Run unit tests + env: + VITEST: "1" + SQLITE_PATH: ":memory:" + NODE_ENV: test run: npm run test - name: Build application diff --git a/CHANGELOG.md b/CHANGELOG.md index 88cd6ca..e5f1bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The Ideon project follows the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.6] - 2026-03-XX + +### Added + +- Added Ability to copy multiple blocks content by concatenating them in the order they were selected. +- Added ability to set the application's log level via environment variables. +- Added `Ctrl+E` and `Ctrl+P` note block shortcuts to switch between edit and preview modes while preserving inline code formatting and command palette conflicts. +- Added `Duplicate` option in the block context menu to quickly duplicate a block. + +### Fixed + +- Fixed blurry content when zooming out in the canvas. +- Fixed OIDC/SSO login lockout for invited or existing users when the provider returned `email_verified=false` [#62](https://github.com/3xpyth0n/ideon/issues/62). + ## [0.7.5] - 2026-03-23 ### Fixed @@ -14,9 +28,9 @@ and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.7.4] - 2026-03-20 -### Added +### Fixed -- Improved folder block styling [#61](https://github.com/3xpyth0n/ideon/issues/61). +- Fixed poor readability of folder cards in light mode dashboard view [#61](https://github.com/3xpyth0n/ideon/issues/61). ## [0.7.3] - 2026-03-19 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66099ac..bc614e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,41 +4,41 @@ Thank you for your interest in contributing to Ideon! We appreciate your help in ## Getting Started -Ideon is designed to be easy to develop on. It supports two database modes depending on the environment: +Ideon uses a **Docker-standardized development environment** to ensure high-fidelity performance and consistency across all machines. -- **SQLite** (Development): Used automatically when running in dev mode. Zero setup required. -- **PostgreSQL** (Production): Used when running via Docker or `npm start`. +### Prerequisites -### Local Development (SQLite) +- **Docker** & **Docker Compose v2** +- **Node.js** (for running checks and tests locally) -This is the recommended way to work on the codebase. It uses a local SQLite file (`storage/dev.db`). +### Initial Setup 1. **Fork the repository** and clone it locally. -2. **Install dependencies**: +2. **Configure the environment**: ```bash - npm install + cp env.example .env + # Edit .env and ensure SECRET_KEY is set (e.g. using openssl rand -hex 32) ``` -3. **Run the development server**: +3. **Install local dependencies** (required for IDE support and linting): ```bash - npm run dev + npm install ``` - The app will be available at `http://localhost:3000`. -### Production Simulation (PostgreSQL) +### Running the Development Environment -If you need to test the production behavior or database migrations with Postgres: +The development server runs within Docker to guarantee production-like performance, especially for large spatial canvases. -1. **Configure the environment**: +```bash +npm run dev +``` - ```bash - cp env.example .env - # Edit .env and ensure SECRET_KEY is set (e.g. using openssl rand -hex 32) - ``` +This command will: -2. **Start the services**: - ```bash - docker compose up -d - ``` +1. Build the Ideon container images. +2. Start the stack including the application and a **PostgreSQL** database. +3. The app will be available at `http://localhost:3000`. + +_Note: Ideon runs in production mode within Docker for maximum performance. You must run `npm run dev` each time you want to apply your changes, as this will trigger a rebuild of the local container image._ ## Development Workflow @@ -49,6 +49,7 @@ If you need to test the production behavior or database migrations with Postgres # Install pre-commit (if not already installed) # macOS: brew install pre-commit # Linux: sudo apt install pre-commit + # Arch: sudo pacman -S pre-commit # Install the git hooks pre-commit install @@ -82,7 +83,7 @@ If you need to test the production behavior or database migrations with Postgres - We use **Prettier** for formatting. - We use **ESLint** for linting. - Follow the existing naming conventions (camelCase for logic, PascalCase for components). -- **No inline styles** allowed (use CSS modules or global CSS). +- **No inline styles** allowed (use CSS files). - **No hardcoded strings** in the UI (use i18n dictionaries). ## Adding a New Language diff --git a/commitlint.config.ts b/commitlint.config.mts similarity index 100% rename from commitlint.config.ts rename to commitlint.config.mts diff --git a/compose.sh b/compose.sh new file mode 100755 index 0000000..e83c351 --- /dev/null +++ b/compose.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Define the base command using both production and dev override files +# This ensures we use the local build (from dev.yml) instead of the GHCR image +COMPOSE="docker compose -f docker-compose.yml -f docker-compose.dev.yml" + +show_help() { + echo "Usage: ./compose.sh [command]" + echo "" + echo "Commands:" + echo " build Build the images" + echo " start Start the stack" + echo " restart Restart the stack" + echo " stop Stop the stack" + echo " down Stop and remove containers" + echo "" +} + +# If no arguments provided, show help. +if [ -z "$1" ]; then + show_help + exit 1 +fi + +case "$1" in + start) + echo "Starting stack..." + $COMPOSE up -d + ;; + restart) + echo "Restarting stack..." + $COMPOSE down + $COMPOSE up -d + ;; + stop) + echo "Stopping stack..." + $COMPOSE stop + ;; + down) + echo "Tearing down stack..." + $COMPOSE down + ;; + build) + echo "Building stack..." + $COMPOSE build + ;; + *) + echo "Error: Unknown command '$1'" + show_help + exit 1 + ;; +esac diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..4f533c5 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,3 @@ +services: + ideon-app: + build: . diff --git a/env.example b/env.example index e1acaeb..cfbb672 100644 --- a/env.example +++ b/env.example @@ -10,10 +10,13 @@ APP_URL=http://localhost:3000 # Canonical timezone for server logs ONLY TIMEZONE=UTC -### Database -# Note: PostgreSQL variables are not required in "development" mode, SQLite is used automatically (storage/dev.db). -# Override SQLite path (optional) -SQLITE_PATH=./storage/dev.db +# Server log level. Defaults to `debug` when `NODE_ENV=test`, otherwise `info`. +# Options: debug|info|warn|error|fatal +LOG_LEVEL=info + +# Public client log level. Only affects client-side logging and is bundled into the client. +# Options: debug|info|warn|error +NEXT_PUBLIC_LOG_LEVEL=error # PostgreSQL host or service name (Docker Compose: ideon-db) DB_HOST=ideon-db @@ -37,9 +40,6 @@ DB_PASS=ideon # Use a strong, random string (e.g., openssl rand -hex 32) SECRET_KEY= -# Origin allowed during Next.js dev server -ALLOWED_DEV_ORIGIN= - ### SMTP # Sender email address SMTP_FROM_EMAIL= diff --git a/eslint.config.ts b/eslint.config.mts similarity index 100% rename from eslint.config.ts rename to eslint.config.mts diff --git a/package-lock.json b/package-lock.json index c38200c..be33ab9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ideon", - "version": "0.7.5", + "version": "0.7.6-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ideon", - "version": "0.7.5", + "version": "0.7.6-beta", "license": "AGPL-3.0-or-later", "dependencies": { "@boxyhq/saml-jackson": "^26.2.0", @@ -27,24 +27,24 @@ "@excalidraw/excalidraw": "^0.18.0", "@react-email/components": "^1.0.10", "@replit/codemirror-vim": "^6.3.0", - "@tiptap/extension-bubble-menu": "^3.20.4", - "@tiptap/extension-link": "^3.20.4", - "@tiptap/extension-placeholder": "^3.20.4", - "@tiptap/extension-table": "^3.20.4", - "@tiptap/extension-table-cell": "^3.20.4", - "@tiptap/extension-table-header": "^3.20.4", - "@tiptap/extension-table-row": "^3.20.4", - "@tiptap/extension-task-item": "^3.20.4", - "@tiptap/extension-task-list": "^3.20.4", - "@tiptap/extension-underline": "^3.20.4", - "@tiptap/react": "^3.20.4", - "@tiptap/starter-kit": "^3.20.4", + "@tiptap/extension-bubble-menu": "^3.21.0", + "@tiptap/extension-link": "^3.21.0", + "@tiptap/extension-placeholder": "^3.21.0", + "@tiptap/extension-table": "^3.21.0", + "@tiptap/extension-table-cell": "^3.21.0", + "@tiptap/extension-table-header": "^3.21.0", + "@tiptap/extension-table-row": "^3.21.0", + "@tiptap/extension-task-item": "^3.21.0", + "@tiptap/extension-task-list": "^3.21.0", + "@tiptap/extension-underline": "^3.21.0", + "@tiptap/react": "^3.21.0", + "@tiptap/starter-kit": "^3.21.0", "@types/chroma-js": "^3.1.2", - "@uiw/react-codemirror": "^4.25.8", + "@uiw/react-codemirror": "^4.25.9", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-serialize": "^0.14.0", "@xterm/xterm": "^6.0.0", - "@xyflow/react": "^12.10.1", + "@xyflow/react": "^12.10.2", "argon2": "^0.44.0", "better-sqlite3": "^12.8.0", "chroma-js": "^3.2.0", @@ -57,12 +57,12 @@ "kysely": "^0.28.14", "lib0": "^0.2.117", "lodash.debounce": "^4.0.8", - "lucide-react": "^0.577.0", + "lucide-react": "^1.7.0", "next": "^16.2.1", "next-auth": "^5.0.0-beta.30", "node-html-parser": "^7.1.0", "node-pty": "^1.1.0", - "nodemailer": "^8.0.3", + "nodemailer": "^8.0.4", "perfect-freehand": "^1.2.3", "pg": "^8.20.0", "pino": "^10.3.1", @@ -73,6 +73,7 @@ "react-email": "^5.2.10", "react-icons": "^5.6.0", "react-simple-code-editor": "^0.14.1", + "semver": "^7.7.4", "sonner": "^2.0.7", "textarea-caret": "^3.1.0", "tiptap-markdown": "^0.9.0", @@ -103,24 +104,24 @@ "@types/prismjs": "^1.26.6", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/semver": "^7.7.1", "@types/textarea-caret": "^3.0.4", "@types/ws": "^8.18.1", - "@typescript-eslint/eslint-plugin": "^8.57.1", - "@typescript-eslint/parser": "^8.57.1", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", "autoprefixer": "^10.4.27", "eslint": "^10.1.0", "globals": "^17.4.0", "husky": "^9.1.7", "jiti": "^2.6.1", "jsdom": "^29.0.1", - "pino-pretty": "^13.1.3", "postcss": "^8.5.8", "socks": "^2.8.7", "tailwindcss": "^4.2.2", "tsup": "^8.5.1", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vitest": "^4.1.0" + "vitest": "^4.1.2" } }, "node_modules/@alloc/quick-lru": { @@ -4459,9 +4460,9 @@ "license": "MIT" }, "node_modules/@excalidraw/excalidraw/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -6156,20 +6157,10 @@ "node": ">=14" } }, - "node_modules/@oxc-project/runtime": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", - "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@oxc-project/types": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", - "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", "dev": true, "license": "MIT", "funding": { @@ -7635,9 +7626,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", "cpu": [ "arm64" ], @@ -7652,9 +7643,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", "cpu": [ "arm64" ], @@ -7669,9 +7660,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", "cpu": [ "x64" ], @@ -7686,9 +7677,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", "cpu": [ "x64" ], @@ -7703,9 +7694,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", - "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", "cpu": [ "arm" ], @@ -7720,9 +7711,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", "cpu": [ "arm64" ], @@ -7740,9 +7731,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", "cpu": [ "arm64" ], @@ -7760,9 +7751,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", "cpu": [ "ppc64" ], @@ -7780,9 +7771,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", "cpu": [ "s390x" ], @@ -7800,9 +7791,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", "cpu": [ "x64" ], @@ -7820,9 +7811,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", "cpu": [ "x64" ], @@ -7840,9 +7831,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", "cpu": [ "arm64" ], @@ -7857,9 +7848,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", - "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", "cpu": [ "wasm32" ], @@ -7874,9 +7865,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", "cpu": [ "arm64" ], @@ -7891,9 +7882,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", "cpu": [ "x64" ], @@ -7908,9 +7899,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", - "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", "dev": true, "license": "MIT" }, @@ -9454,48 +9445,48 @@ } }, "node_modules/@tiptap/core": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz", - "integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.21.0.tgz", + "integrity": "sha512-IfnQiuEeabDSPr1C/zHFTbnvlTf5z0DE/d/xz4C6bkL4ZBDJ3rr99h2qsaV0l8F+kbNswZMlQdM8rxNlMy95fQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^3.20.4" + "@tiptap/pm": "^3.21.0" } }, "node_modules/@tiptap/extension-blockquote": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.4.tgz", - "integrity": "sha512-9sskyyhYj2oKat//lyZVXCp9YrPt4oJAZnGHYWXS0xlskjsLElrfKKlM4vpbhGss3VrhQRoEGqWLnIaJYPF1zw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.21.0.tgz", + "integrity": "sha512-JDM/RR6rM0dMCZ1UnEf7eqmN6pAdIa2llhN+E24HdTGNJCklMFhLAGE/OT8/1r7M0WWA9GVO7/PTe4EdGh6+lQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-bold": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.4.tgz", - "integrity": "sha512-Md7/mNAeJCY+VLJc8JRGI+8XkVPKiOGB1NgqQPdh3aYtxXQDChQOZoJEQl6TuudDxZ85bLZB67NjZlx3jo8/0g==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.21.0.tgz", + "integrity": "sha512-iyEJRzG7XTCPlHwEDzUw3HnuYYCfL7lNpcCHmxcpYMrIUA8rv7EUxerIwApT6xY8hQ/07ljuJKgOyPvnJOOzuA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.4.tgz", - "integrity": "sha512-EXywPlI8wjPcAb8ozymgVhjtMjFrnhtoyNTy8ZcObdpUi5CdO9j892Y7aPbKe5hLhlDpvJk7rMfir4FFKEmfng==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.21.0.tgz", + "integrity": "sha512-/fabRRhhf8i4LAx9e8xz9ppqN5KgdJk3TxMuxAD5vAWGsejvhSoPa8O8H/QwwyntXm1Vue8aQiMHsUk48b2hGQ==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.0.0" @@ -9505,80 +9496,80 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4", - "@tiptap/pm": "^3.20.4" + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.4.tgz", - "integrity": "sha512-1RTGrur1EKoxfnLZ3M6xeNj8GITAz74jH2DHGcjLsd2Xr7Q7BozGaIq6GkkvKguMwbI1zCOxTHFCpUETXAIQQA==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.21.0.tgz", + "integrity": "sha512-PWNF+xwxgOeXYGD88sCQLKL0eBoQqjUnZNALxBjN3Y7x4llalh42rHOp2Nt2t6UbQgqTBtBzU/uFcussTpxreQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.4" + "@tiptap/extension-list": "^3.21.0" } }, "node_modules/@tiptap/extension-code": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.4.tgz", - "integrity": "sha512-7j8Hi964bH1SZ9oLdZC1fkqWz27mliSDV7M8lmL/M14+Qw42D/VOAKS4Aw9OCFtHMlTsjLR6qsoVxL8Lpkt6NA==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.21.0.tgz", + "integrity": "sha512-D7wA9jp+4X2r1f3FIoga73s6Rn4rmZY57Jes6a4rK3HY+3yHk1r057pPIZSY8Drfs97jxHQVFdfUYUomLSFYBA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-code-block": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.4.tgz", - "integrity": "sha512-Zlw3FrXTy01+o1yISeX/LC+iJeHA+ym602bMXGmtA6lyl7QSOSO7WExweJ6xeJGhbCjldwT5al6fkRAs8iGJZg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.21.0.tgz", + "integrity": "sha512-zrVOcOzDCjHQ8NJcC+qHmZZKiwnP/NMSb3qVJlSMN8TzuHept1MZCDa2Mbo70O6I0txo456SGuXB9sqV1vHmGg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4", - "@tiptap/pm": "^3.20.4" + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" } }, "node_modules/@tiptap/extension-document": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.4.tgz", - "integrity": "sha512-zF1CIFVLt8MfSpWWnPwtGyxPOsT0xYM2qJKcXf2yZcTG37wDKmUi6heG53vGigIavbQlLaAFvs+1mNdOu2x/0A==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.21.0.tgz", + "integrity": "sha512-7oCyzXI9ChvJQUlr23AURdfVar4OIsrYUvqdhEwo3bjcI/Q/j0KJiXfuh6ZzL5eVaINSailH53sZaGg4THQtUg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.4.tgz", - "integrity": "sha512-TgMwvZ8myXYdmd6bUV7qkpZXv7ZUiSmX/8eo+iPEzYo2CnDLAGvDKgC50nfq/g87SDvfBgPuAiBfFvsMQQWaTw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.21.0.tgz", + "integrity": "sha512-6fsDSVAM2iz7eElvT6iivMrGBGjIP/oPigVZ/SPm6f31phaYhz6TIOEgV/Lr2jaPIOgyK4U0cU4Yd4KUBCmhzQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.20.4" + "@tiptap/extensions": "^3.21.0" } }, "node_modules/@tiptap/extension-floating-menu": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.4.tgz", - "integrity": "sha512-AaPTFhoO8DBIElJyd/RTVJjkctvJuL+GHURX0npbtTxXq5HXbebVwf2ARNR7jMd/GThsmBaNJiGxZg4A2oeDqQ==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.21.0.tgz", + "integrity": "sha512-n2HzTB+I/5rAl8R/1sKMv92JiY1oDK1hroXizxEKYa6dskJcAMW0CfYyPcPOZWQQEe7qoeOvQISr2ooLAKW+Mw==", "license": "MIT", "optional": true, "funding": { @@ -9587,80 +9578,80 @@ }, "peerDependencies": { "@floating-ui/dom": "^1.0.0", - "@tiptap/core": "^3.20.4", - "@tiptap/pm": "^3.20.4" + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.4.tgz", - "integrity": "sha512-JJ6f1iQ1e0s4kISgq55U3UYGwWV/N9f0PYMtB6e3L+SBQjXnywaLK0g6vfN6IvTCC2vdIuqeSOX8VlSO97sJLw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.21.0.tgz", + "integrity": "sha512-wGjgAoYBTvPAe9QYMI5px355XcNeMkaUrMY9IHbMqgqdmHcDxqooxM4H6sYVX2CRcHwXy4I8NQAoOhSYrQJDMg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.20.4" + "@tiptap/extensions": "^3.21.0" } }, "node_modules/@tiptap/extension-hard-break": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.4.tgz", - "integrity": "sha512-gJbq58d8zB1gzyqVEopowej5CpW4/Fpg6oGJvlZxaCukqd0gJRWGC89K+jE62YA1Td4sfcKrekKvN7jm2y/ZUg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.21.0.tgz", + "integrity": "sha512-6JFVSAOQ1qhQHi9mVcdn2/XO8YIMgYV8zjarzNUzP6Sf2waeE5BLXjlg6rIH/945sY1J+FndTojLru6gQ07a5A==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-heading": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.4.tgz", - "integrity": "sha512-xsnkmTGggJc5P2iCwS1lv8KFG31xC/GNPJKoi/3UH67j/lKDhA3AdtshsLeyv2FKtTtYDb8oV0IqzHB1MM6a7w==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.21.0.tgz", + "integrity": "sha512-ji6VJmoRnDzAHYflEYEZohMHRi77UGLW1o3ua7UhI32iJ9nuYssbPNuzEeE4SvENMQwZRszad5+a+dKAa+NC7g==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.4.tgz", - "integrity": "sha512-y6joCi49haAA0bo3EGUY+dWUMHH1GPUc84hxrBY/0pMs+Bn+kQ1+DQJErZDTWGJrlHPWU/yekBZT72SNdp0DNA==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.21.0.tgz", + "integrity": "sha512-vNBnOfFEY62CoJPGo4nonRM7RiOvhII1vhoO+WFr1GxDqCAfmEFjToflt7JT1UJdo6lMVcD+aaaAgOiuSz5p6g==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4", - "@tiptap/pm": "^3.20.4" + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" } }, "node_modules/@tiptap/extension-italic": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.4.tgz", - "integrity": "sha512-4ZqiWr7cmqPFux8tj1ZLiYytyWf343IvQemNX6AvVWvscrJcrfj3YX4Le2BA0RW3A3M6RpLQXXozuF8vxYFDeQ==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.21.0.tgz", + "integrity": "sha512-2I8oPvwyXhRn1k8lbDFIutzvhtLEjoO5mmQCNX4TnT4PdxxaSrK9+ihYg12VeqhUeO7dg1MKiFqws0HVBrwzWg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-link": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.4.tgz", - "integrity": "sha512-JNDSkWrVdb8NSvbQXwHWvK5tCMbTWwOHFOweknQZ1JPK4dei9FJVofYQaHyW4bJBdcCjds3NZSnXE8DM9iAWmg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.21.0.tgz", + "integrity": "sha512-oMU7Yve1sbgBsaFAUc2R0GPf4d3ZPVJeMUFC6b6X9rJIvx/IhEUEn9toQcSBGfp02uWK9NdQyIFYFdWlVXH++w==", "license": "MIT", "dependencies": { "linkifyjs": "^4.3.2" @@ -9670,225 +9661,225 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4", - "@tiptap/pm": "^3.20.4" + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" } }, "node_modules/@tiptap/extension-list": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.4.tgz", - "integrity": "sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.21.0.tgz", + "integrity": "sha512-KeBlEtLrGce2d3dgL89hmwWEtREuzlW4XY5bYWpKNvCbFqvdSb3n7vkdkw32YclZmMWxAcABgW6ucCStkE0rsQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4", - "@tiptap/pm": "^3.20.4" + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" } }, "node_modules/@tiptap/extension-list-item": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.4.tgz", - "integrity": "sha512-QoTc5RACXaZF+vIIBBxjGO7D0oWFUDgBKJCpvUZ0CoGGKosnfe4a9I5THFyLj4201cf0oUqgf1oZhTqETGxlVw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.21.0.tgz", + "integrity": "sha512-1ZymZmlQVbAoC4q5x3cro0v5+3I6l+BHqbhIMQLjQFlAOJfcE0pvqRzAFW7PduxUj41tXEtsYqp2NREvO9F5Fg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.4" + "@tiptap/extension-list": "^3.21.0" } }, "node_modules/@tiptap/extension-list-keymap": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.4.tgz", - "integrity": "sha512-RIqXM649+8IP7p/KVfaGlJiwjCylm1m6OPlaoM3K8O7oEOGRQzNeexexECCD2jsXRxew4E+vBNMD2orXqJmu8A==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.21.0.tgz", + "integrity": "sha512-EzrfW3ASNFPWKhR8sNOq7Kqw4hvaTAOn4dlI7chB8HIANSrlyPOUn+eKAnO6HQgsUgsbjg2GbTUrGrxcoLykUg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.4" + "@tiptap/extension-list": "^3.21.0" } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.4.tgz", - "integrity": "sha512-3budNL8BgBon3TcXZ4hjT0YpFvx1Ka3uSIECKDxHgES+OQcR+6cagxSb60gFEccf3Dr0PIwcVTY6g14lC1qKRQ==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.21.0.tgz", + "integrity": "sha512-+d+0orokMfqaBfvr9tUBgGvo2ZCV+fR3JzsJTmnLBWOkhBSJN7H4pnfXPTue0qwspUwRmkLJxdIlU+J7HkMrng==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.4" + "@tiptap/extension-list": "^3.21.0" } }, "node_modules/@tiptap/extension-paragraph": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.4.tgz", - "integrity": "sha512-lm6fOScWuZAF/Sfp97igUwFd3L1QHIVLAWP5NVdh0DTLrEIt4rMBmsww+yOpMQRhvz2uTgMbMXynrimhzi/QVw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.21.0.tgz", + "integrity": "sha512-cMPG/jCoZ9NmLZ5ctFziILaxJGfDtMTb5OLBhifMFZeMVwF1pEJIygDEfnX/HSruv507weZSQG4pERO2tRszMg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-placeholder": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.20.4.tgz", - "integrity": "sha512-GB0KWtqm83YHG8cnqBLijvUBm+xvLfQHDfFRRH2fb3EzH3eIsM9jKRC31ADT27RSV1zVpHMFGcP3/pWpdrN1Lw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.21.0.tgz", + "integrity": "sha512-fs+cQqMh1d1naV6OgOhP/0qbRJwtw8DpQMj3/oqGKbaRRKIeecEaZPXYRd7MYa4e9K0Cfk5Bm0MNs9lwu/BYsw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.20.4" + "@tiptap/extensions": "^3.21.0" } }, "node_modules/@tiptap/extension-strike": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.4.tgz", - "integrity": "sha512-It1Px9uDGTsVqyyg6cy7DigLoenljpQwqdI0jssM7QclZrHnsrye9fZxBBiiuCzzV1305MxKgHvratkHwqmVNA==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.21.0.tgz", + "integrity": "sha512-easnVaN11Wl+5fOtfvzJ10J762S9TRXZaMj5rLBGavgf82DCYHqhGhBqpLQrJ41r4nPABGlYvTRoxfvBLB74Lg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-table": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.20.4.tgz", - "integrity": "sha512-vEHXRL9k9G02pp3P+DyUnN4YRaRAHGfTBC6gck0s9TpsCM9NIchL0qI1fb/u46Bu6UaoMMk58DGr7xaJ29g7KQ==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.21.0.tgz", + "integrity": "sha512-/iPVM/5TtRy7wGrLW/uL+qjMHsor26P/nH2nHasgPr/EIwliLGhHp9Gwjhix3OTyM2UK+VNcuhH6X50UbcByTQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4", - "@tiptap/pm": "^3.20.4" + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" } }, "node_modules/@tiptap/extension-table-cell": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.20.4.tgz", - "integrity": "sha512-R+AKOPKCq2NwETa4/nsidXnYe7UP8CxdbvJI1r6DMYipdj4ksQI24ij91hkwDLkeAhJyNGKub4soEMRdLgcQyQ==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.21.0.tgz", + "integrity": "sha512-jQc4ruEbLgUMM3PahNZENFEiqg3LPBmrrvm/yhwY3KUaGAGfLVuduCogsbXIwu5OKNdMafH5HJz7CbdYqrOUlA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-table": "^3.20.4" + "@tiptap/extension-table": "^3.21.0" } }, "node_modules/@tiptap/extension-table-header": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.20.4.tgz", - "integrity": "sha512-f8xlcNfmh2MD+fzPYXhy1zvao5DlWf+Sdd7eQ4G3baui/PdgBpJEroVCPYZIOazVRHL43MFxtvPeSJ5RN6J+bw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.21.0.tgz", + "integrity": "sha512-3IgtyFwBi+um1+HgywHAxLpZjJFoueDM4r2FKroSrvhAl+v1Z6d4BYp8dzRRimlee6QFx0hCE+VXpFvd83aiZw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-table": "^3.20.4" + "@tiptap/extension-table": "^3.21.0" } }, "node_modules/@tiptap/extension-table-row": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.20.4.tgz", - "integrity": "sha512-bW9pXTMpNmDoZrLVPYylkfhiSwPxTYpubFduW3cjsiSJK046kSw5zOlvlv4+9vZI1TSlA+BFykPeW34YqLbr6Q==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.21.0.tgz", + "integrity": "sha512-A1uL9O5atz2ZBtLpikSXBwiULjg3FJBVBIwtpyHkUdA9V13yS/NnFqt2jrKgT1g3EGwGjkqgY7uK9OF8e2XJhg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-table": "^3.20.4" + "@tiptap/extension-table": "^3.21.0" } }, "node_modules/@tiptap/extension-task-item": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-3.20.4.tgz", - "integrity": "sha512-mEWyAtZ61USZnKyLDxi2DtnSREfW0yUFXDOFWstNg1i6hva197BuAy6VRQMQxTOq+cFAgAt1MEZKanW0Obsa+g==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-3.21.0.tgz", + "integrity": "sha512-Rt7g61pcmgTHTy8mU2MDi4+FERKLAvoejJqmrCKf1CJUGzwWMsyRlKvS7FykpzfSrDiVlE7uSFCUkd/avgV0HQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.4" + "@tiptap/extension-list": "^3.21.0" } }, "node_modules/@tiptap/extension-task-list": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-3.20.4.tgz", - "integrity": "sha512-QvLrpffkxkr7TTgMmk6fnPAE34HYrUosHiuZJpRK008MuJDOoANblS221M4lLuRE73w3KI7hd/fi2CliBcCC4A==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-3.21.0.tgz", + "integrity": "sha512-uH2h4Z2JmafFH5TfWYmKbfO7+jt3mgF59ESj+Qc9Szd1j9JJvLOH6anrIxqsyi50jz0QCl/lPv38PKr/ugvjtQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.20.4" + "@tiptap/extension-list": "^3.21.0" } }, "node_modules/@tiptap/extension-text": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.4.tgz", - "integrity": "sha512-jchJcBZixDEO2J66Zx5dchsI2mA6IYsROqF8P1poxL4ienH7RVQRCTsBNnSfIeOtREKKWeOU/tEs5fcpvvGwIQ==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.21.0.tgz", + "integrity": "sha512-Zx8QdB8a5iBuE4uO21c3BjmpBfaJEr2Jd1QFnsdgx11fm6P7dGgZaGko1FaINhfOPRGTN6O/kiF02cDMdOHa/w==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extension-underline": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.4.tgz", - "integrity": "sha512-0OjMc3FDujX16G+jhvqcY/mLot8SrNtDu8ggUwNLAfiI/QIvMVgk7giFD71DATC/4Nb8i/iwAEegTD8MxBIXCg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.21.0.tgz", + "integrity": "sha512-gGmBEymbWnr8AIS8bI/bPw5rcwo7wAFcBw/TsLd1nAanu1dDqSRNDBrit3m02Ru+D88u2SfNvmbOPI1pz+1f5w==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4" + "@tiptap/core": "^3.21.0" } }, "node_modules/@tiptap/extensions": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.4.tgz", - "integrity": "sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.21.0.tgz", + "integrity": "sha512-MN1uh5PmHT1F2BNsbc21MIS0AMFFA73oODlp/4ckpBR4o5AxRwV+8f43Cd52UL4MgMkKj/A+QfZ7iK9IDb0h5A==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.20.4", - "@tiptap/pm": "^3.20.4" + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0" } }, "node_modules/@tiptap/pm": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz", - "integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.21.0.tgz", + "integrity": "sha512-I3sNo7oMMsR6FFz1ecvPb9uCF0VQuS2WV67j8Io2M7DJicRWCE/GM5DaiYjTeWBbnByk6BuG0txoJATAqPVliQ==", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.3.0", @@ -9916,9 +9907,9 @@ } }, "node_modules/@tiptap/react": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.20.4.tgz", - "integrity": "sha512-1B8iWsHWwb5TeyVaUs8BRPzwWo4PsLQcl03urHaz0zTJ8DauopqvxzV3+lem1OkzRHn7wnrapDvwmIGoROCaQw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.21.0.tgz", + "integrity": "sha512-p+OKJgxmFB3t5nY3mjaqjKaj8vJX9++OkdrZLRxYuG7ScAHemWraWQ25sgNZl1LDaRYrdnNYxx9MP0CXOSB6ew==", "license": "MIT", "dependencies": { "@types/use-sync-external-store": "^0.0.6", @@ -9930,12 +9921,12 @@ "url": "https://github.com/sponsors/ueberdosis" }, "optionalDependencies": { - "@tiptap/extension-bubble-menu": "^3.20.4", - "@tiptap/extension-floating-menu": "^3.20.4" + "@tiptap/extension-bubble-menu": "^3.21.0", + "@tiptap/extension-floating-menu": "^3.21.0" }, "peerDependencies": { - "@tiptap/core": "^3.20.4", - "@tiptap/pm": "^3.20.4", + "@tiptap/core": "^3.21.0", + "@tiptap/pm": "^3.21.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -9943,35 +9934,35 @@ } }, "node_modules/@tiptap/starter-kit": { - "version": "3.20.4", - "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.4.tgz", - "integrity": "sha512-WcyK6hsTl8eBsQhQ+d9Sq8fYZKOYdL+D45MyH3hz583elXqJlW3h3JPFYb0o87gddGxn8Mm57OA/gA1zEdeDMw==", - "license": "MIT", - "dependencies": { - "@tiptap/core": "^3.20.4", - "@tiptap/extension-blockquote": "^3.20.4", - "@tiptap/extension-bold": "^3.20.4", - "@tiptap/extension-bullet-list": "^3.20.4", - "@tiptap/extension-code": "^3.20.4", - "@tiptap/extension-code-block": "^3.20.4", - "@tiptap/extension-document": "^3.20.4", - "@tiptap/extension-dropcursor": "^3.20.4", - "@tiptap/extension-gapcursor": "^3.20.4", - "@tiptap/extension-hard-break": "^3.20.4", - "@tiptap/extension-heading": "^3.20.4", - "@tiptap/extension-horizontal-rule": "^3.20.4", - "@tiptap/extension-italic": "^3.20.4", - "@tiptap/extension-link": "^3.20.4", - "@tiptap/extension-list": "^3.20.4", - "@tiptap/extension-list-item": "^3.20.4", - "@tiptap/extension-list-keymap": "^3.20.4", - "@tiptap/extension-ordered-list": "^3.20.4", - "@tiptap/extension-paragraph": "^3.20.4", - "@tiptap/extension-strike": "^3.20.4", - "@tiptap/extension-text": "^3.20.4", - "@tiptap/extension-underline": "^3.20.4", - "@tiptap/extensions": "^3.20.4", - "@tiptap/pm": "^3.20.4" + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.21.0.tgz", + "integrity": "sha512-w7fWxglDtqXFBgRYH+LforJyUboSAQllnWQbGVSTyX4rsICqZjkb3f6CTSUWpGoGKmlmbb2ZpEuoik7tur9d8Q==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.21.0", + "@tiptap/extension-blockquote": "^3.21.0", + "@tiptap/extension-bold": "^3.21.0", + "@tiptap/extension-bullet-list": "^3.21.0", + "@tiptap/extension-code": "^3.21.0", + "@tiptap/extension-code-block": "^3.21.0", + "@tiptap/extension-document": "^3.21.0", + "@tiptap/extension-dropcursor": "^3.21.0", + "@tiptap/extension-gapcursor": "^3.21.0", + "@tiptap/extension-hard-break": "^3.21.0", + "@tiptap/extension-heading": "^3.21.0", + "@tiptap/extension-horizontal-rule": "^3.21.0", + "@tiptap/extension-italic": "^3.21.0", + "@tiptap/extension-link": "^3.21.0", + "@tiptap/extension-list": "^3.21.0", + "@tiptap/extension-list-item": "^3.21.0", + "@tiptap/extension-list-keymap": "^3.21.0", + "@tiptap/extension-ordered-list": "^3.21.0", + "@tiptap/extension-paragraph": "^3.21.0", + "@tiptap/extension-strike": "^3.21.0", + "@tiptap/extension-text": "^3.21.0", + "@tiptap/extension-underline": "^3.21.0", + "@tiptap/extensions": "^3.21.0", + "@tiptap/pm": "^3.21.0" }, "funding": { "type": "github", @@ -10465,6 +10456,13 @@ "@types/node": "*" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/textarea-caret": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/textarea-caret/-/textarea-caret-3.0.4.tgz", @@ -10501,17 +10499,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -10524,23 +10522,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "debug": "^4.4.3" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10554,16 +10552,22 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", - "debug": "^4.4.3" + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "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" @@ -10576,30 +10580,57 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "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/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "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/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -10608,20 +10639,25 @@ "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/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "engines": { @@ -10632,35 +10668,84 @@ "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.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "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/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "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/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "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/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -10678,17 +10763,16 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10698,18 +10782,66 @@ "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/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "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/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "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/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -10748,9 +10880,9 @@ } }, "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.25.8", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.8.tgz", - "integrity": "sha512-9Rr+liiBmK4xzZHszL+twNRJApthqmITBwDP3emNTtTrkBFN4gHlqfp+nodKmoVt1+bUH1qQCtyqt+7dbDTHiw==", + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.9.tgz", + "integrity": "sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -10775,16 +10907,16 @@ } }, "node_modules/@uiw/react-codemirror": { - "version": "4.25.8", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.8.tgz", - "integrity": "sha512-A0aLOuJZm2yJ+U9GlMFwxwFciztjd5LhcAG4SMqFxdD58wH+sCQXuY4UU5J2hqgS390qAlShtUgREvJPUonbuQ==", + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.9.tgz", + "integrity": "sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.25.8", + "@uiw/codemirror-extensions-basic-setup": "4.25.9", "codemirror": "^6.0.0" }, "funding": { @@ -10811,31 +10943,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -10844,7 +10976,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -10856,26 +10988,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.2", "pathe": "^2.0.3" }, "funding": { @@ -10883,14 +11015,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -10899,9 +11031,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "dev": true, "license": "MIT", "funding": { @@ -10909,15 +11041,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -11120,12 +11252,12 @@ ] }, "node_modules/@xyflow/react": { - "version": "12.10.1", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.1.tgz", - "integrity": "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==", + "version": "12.10.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", + "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", "license": "MIT", "dependencies": { - "@xyflow/system": "0.0.75", + "@xyflow/system": "0.0.76", "classcat": "^5.0.3", "zustand": "^4.4.0" }, @@ -11135,9 +11267,9 @@ } }, "node_modules/@xyflow/system": { - "version": "0.0.75", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.75.tgz", - "integrity": "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==", + "version": "0.0.76", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", + "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", "license": "MIT", "dependencies": { "@types/d3-drag": "^3.0.7", @@ -11292,9 +11424,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -11805,9 +11937,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -12368,13 +12500,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -13247,16 +13372,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/dayjs": { "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", @@ -14236,13 +14351,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/fast-copy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", - "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -14279,13 +14387,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -15063,13 +15164,6 @@ "he": "bin/he" } }, - "node_modules/help-me": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "dev": true, - "license": "MIT" - }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -16614,9 +16708,9 @@ } }, "node_modules/lucide-react": { - "version": "0.577.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", - "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -17219,9 +17313,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -17272,9 +17366,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz", - "integrity": "sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz", + "integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -17874,9 +17968,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -17917,31 +18011,6 @@ "split2": "^4.0.0" } }, - "node_modules/pino-pretty": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", - "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^4.0.0", - "fast-safe-stringify": "^2.1.1", - "help-me": "^5.0.0", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^3.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^4.0.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^5.0.2" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, "node_modules/pino-std-serializers": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", @@ -19492,14 +19561,14 @@ "license": "Unlicense" }, "node_modules/rolldown": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", - "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.115.0", - "@rolldown/pluginutils": "1.0.0-rc.9" + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" @@ -19508,21 +19577,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-x64": "1.0.0-rc.9", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, "node_modules/rollup": { @@ -19698,23 +19767,6 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, - "node_modules/secure-json-parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/seek-bzip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", @@ -20410,19 +20462,6 @@ "node": ">=6" } }, - "node_modules/strip-json-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", - "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strnum": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", @@ -20939,9 +20978,9 @@ } }, "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==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -21493,17 +21532,16 @@ } }, "node_modules/vite": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", - "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", + "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.9", + "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "bin": { @@ -21520,7 +21558,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.0.0-alpha.31", + "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -21572,19 +21610,19 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -21595,8 +21633,8 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -21612,13 +21650,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { diff --git a/package.json b/package.json index fcc1999..f3ed7eb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package.json", "name": "ideon", "private": true, - "version": "0.7.5", + "version": "0.7.6-beta", "description": "The Visual Hub for Everything Your Project Needs", "license": "AGPL-3.0-or-later", "repository": { @@ -11,7 +11,7 @@ }, "type": "commonjs", "scripts": { - "dev": "NODE_ENV=development NODE_OPTIONS='--max-old-space-size=8192' tsx watch src/server.ts", + "dev": "./compose.sh build && ./compose.sh restart", "build": "next build --webpack && tsup", "start": "NODE_OPTIONS='--max-old-space-size=8192' node dist/server.cjs", "check": "eslint . --max-warnings=0; tsc --noEmit", @@ -39,24 +39,24 @@ "@excalidraw/excalidraw": "^0.18.0", "@react-email/components": "^1.0.10", "@replit/codemirror-vim": "^6.3.0", - "@tiptap/extension-bubble-menu": "^3.20.4", - "@tiptap/extension-link": "^3.20.4", - "@tiptap/extension-placeholder": "^3.20.4", - "@tiptap/extension-table": "^3.20.4", - "@tiptap/extension-table-cell": "^3.20.4", - "@tiptap/extension-table-header": "^3.20.4", - "@tiptap/extension-table-row": "^3.20.4", - "@tiptap/extension-task-item": "^3.20.4", - "@tiptap/extension-task-list": "^3.20.4", - "@tiptap/extension-underline": "^3.20.4", - "@tiptap/react": "^3.20.4", - "@tiptap/starter-kit": "^3.20.4", + "@tiptap/extension-bubble-menu": "^3.21.0", + "@tiptap/extension-link": "^3.21.0", + "@tiptap/extension-placeholder": "^3.21.0", + "@tiptap/extension-table": "^3.21.0", + "@tiptap/extension-table-cell": "^3.21.0", + "@tiptap/extension-table-header": "^3.21.0", + "@tiptap/extension-table-row": "^3.21.0", + "@tiptap/extension-task-item": "^3.21.0", + "@tiptap/extension-task-list": "^3.21.0", + "@tiptap/extension-underline": "^3.21.0", + "@tiptap/react": "^3.21.0", + "@tiptap/starter-kit": "^3.21.0", "@types/chroma-js": "^3.1.2", - "@uiw/react-codemirror": "^4.25.8", + "@uiw/react-codemirror": "^4.25.9", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-serialize": "^0.14.0", "@xterm/xterm": "^6.0.0", - "@xyflow/react": "^12.10.1", + "@xyflow/react": "^12.10.2", "argon2": "^0.44.0", "better-sqlite3": "^12.8.0", "chroma-js": "^3.2.0", @@ -69,12 +69,12 @@ "kysely": "^0.28.14", "lib0": "^0.2.117", "lodash.debounce": "^4.0.8", - "lucide-react": "^0.577.0", + "lucide-react": "^1.7.0", "next": "^16.2.1", "next-auth": "^5.0.0-beta.30", "node-html-parser": "^7.1.0", "node-pty": "^1.1.0", - "nodemailer": "^8.0.3", + "nodemailer": "^8.0.4", "perfect-freehand": "^1.2.3", "pg": "^8.20.0", "pino": "^10.3.1", @@ -85,6 +85,7 @@ "react-email": "^5.2.10", "react-icons": "^5.6.0", "react-simple-code-editor": "^0.14.1", + "semver": "^7.7.4", "sonner": "^2.0.7", "textarea-caret": "^3.1.0", "tiptap-markdown": "^0.9.0", @@ -179,23 +180,23 @@ "@types/prismjs": "^1.26.6", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/semver": "^7.7.1", "@types/textarea-caret": "^3.0.4", "@types/ws": "^8.18.1", - "@typescript-eslint/eslint-plugin": "^8.57.1", - "@typescript-eslint/parser": "^8.57.1", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", "autoprefixer": "^10.4.27", "eslint": "^10.1.0", "globals": "^17.4.0", "husky": "^9.1.7", "jiti": "^2.6.1", "jsdom": "^29.0.1", - "pino-pretty": "^13.1.3", "postcss": "^8.5.8", "socks": "^2.8.7", "tailwindcss": "^4.2.2", "tsup": "^8.5.1", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vitest": "^4.1.0" + "vitest": "^4.1.2" } } diff --git a/src/app/api/account/avatar/route.ts b/src/app/api/account/avatar/route.ts index b357f72..df3e6b4 100644 --- a/src/app/api/account/avatar/route.ts +++ b/src/app/api/account/avatar/route.ts @@ -3,6 +3,7 @@ import { writeFile, unlink } from "fs/promises"; import { join } from "path"; import crypto from "crypto"; import { authenticatedAction } from "@lib/server-utils"; +import { logger } from "@lib/logger"; export const POST = authenticatedAction( async (req, { user: auth }) => { @@ -52,7 +53,7 @@ export const POST = authenticatedAction( await writeFile(filePath, buffer); } catch (error) { - console.error("Avatar upload error:", error); + logger.error({ error, userId }, "Avatar upload error"); throw { status: 500, message: "Avatar upload error.", diff --git a/src/app/api/auth/forgot-password/route.ts b/src/app/api/auth/forgot-password/route.ts index a31a383..43ec643 100644 --- a/src/app/api/auth/forgot-password/route.ts +++ b/src/app/api/auth/forgot-password/route.ts @@ -6,6 +6,7 @@ import { headers } from "next/headers"; import { v4 as uuidv4 } from "uuid"; import { hashToken } from "@lib/crypto"; import { checkRateLimit } from "@lib/rate-limit"; +import { logger } from "@lib/logger"; import { getClientIp } from "@lib/security-utils"; export async function POST(req: Request) { @@ -63,7 +64,7 @@ export async function POST(req: Request) { // Always return success to prevent account enumeration return NextResponse.json({ success: true }); } catch (error) { - console.error("Forgot password error:", error); + logger.error({ error }, "Forgot password error"); return NextResponse.json({ success: true }); } } diff --git a/src/app/api/auth/reset-password/route.ts b/src/app/api/auth/reset-password/route.ts index 96299be..0588d06 100644 --- a/src/app/api/auth/reset-password/route.ts +++ b/src/app/api/auth/reset-password/route.ts @@ -7,6 +7,7 @@ import { hashToken } from "@lib/crypto"; import { checkRateLimit } from "@lib/rate-limit"; import { z } from "zod"; import { getClientIp } from "@/lib/security-utils"; +import { logger } from "@lib/logger"; const resetPasswordSchema = z.object({ token: z.string().min(1), @@ -94,7 +95,7 @@ export async function POST(req: Request) { return NextResponse.json({ success: true }); } catch (error) { - console.error("Reset password error:", error); + logger.error({ error }, "Reset password error"); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, diff --git a/src/app/api/git/stats/route.ts b/src/app/api/git/stats/route.ts index 869a08d..833c0ed 100644 --- a/src/app/api/git/stats/route.ts +++ b/src/app/api/git/stats/route.ts @@ -3,6 +3,7 @@ import { getRepoStats } from "@lib/client/git-providers"; import { getDb, withAuthenticatedSession } from "@lib/db"; import { auth } from "@auth"; import { decryptApiKey } from "@lib/crypto"; +import { logger } from "@lib/logger"; export const dynamic = "force-dynamic"; @@ -59,7 +60,10 @@ export async function GET(req: NextRequest) { try { token = decryptApiKey(matchedToken.token, userId).trim(); } catch (e) { - console.error("[GitStats] Failed to decrypt token:", e); + logger.error( + { err: e, userId }, + "[GitStats] Failed to decrypt token", + ); } } }); @@ -78,7 +82,7 @@ export async function GET(req: NextRequest) { return NextResponse.json(result); } catch (error) { - console.error("Git proxy error:", error); + logger.error({ error, url }, "Git proxy error"); return NextResponse.json( { error: "Failed to fetch git stats" }, { status: 500 }, diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index fbaa869..6b59ef8 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { getDb } from "@lib/db"; import { sql } from "kysely"; +import { logger } from "@lib/logger"; export const dynamic = "force-dynamic"; @@ -16,7 +17,7 @@ export async function GET() { { status: 200 }, ); } catch (error) { - console.error("Health check failed:", error); + logger.error({ error }, "Health check failed"); return NextResponse.json( { status: "error", message: "Service unhealthy" }, { status: 500 }, diff --git a/src/app/api/projects/[id]/files/route.ts b/src/app/api/projects/[id]/files/route.ts index 03c86f5..17c6bc4 100644 --- a/src/app/api/projects/[id]/files/route.ts +++ b/src/app/api/projects/[id]/files/route.ts @@ -1,4 +1,5 @@ import { projectAction } from "@lib/server-utils"; +import { logger } from "@lib/logger"; import { writeFile, readFile, unlink, mkdir } from "fs/promises"; import { join } from "path"; import { NextResponse } from "next/server"; @@ -41,7 +42,7 @@ export const POST = projectAction(async (req, { project }) => { type: file.type, }; } catch (error) { - console.error("File upload error:", error); + logger.error({ error, projectId: project.id }, "File upload error"); throw { status: 500, message: "File upload error.", @@ -69,7 +70,7 @@ export const GET = projectAction(async (req, { project }) => { ); if (!existsSync(filePath)) { - console.error(`File not found at path: ${filePath}`); + logger.warn({ filePath, projectId: project.id }, "File not found at path"); throw { status: 404, message: "File not found" }; } @@ -133,7 +134,10 @@ export const DELETE = projectAction(async (req, { project }) => { } return { success: true }; } catch (error) { - console.error("Error deleting file:", error); + logger.error( + { error, filePath, projectId: project.id }, + "Error deleting file", + ); throw { status: 500, message: "Failed to delete file" }; } }); diff --git a/src/app/api/projects/[id]/graph/route.ts b/src/app/api/projects/[id]/graph/route.ts index 6eb96d7..8fdcba5 100644 --- a/src/app/api/projects/[id]/graph/route.ts +++ b/src/app/api/projects/[id]/graph/route.ts @@ -11,6 +11,7 @@ import { import type { BlockData } from "@components/project/CanvasBlock"; import { validateFolderLinkRules } from "@lib/folder-link-rules"; import { v4 as uuidv4 } from "uuid"; +import { logger } from "@lib/logger"; import { z } from "zod"; export const dynamic = "force-dynamic"; @@ -317,7 +318,10 @@ export const POST = projectAction( .values(reactionsToInsert.slice(i, i + 1000)) .execute(); } catch (error) { - console.error("Failed to insert reactions:", error); + logger.error( + { error, projectId: project.id }, + "Failed to insert reactions", + ); } } } diff --git a/src/app/api/projects/[id]/request-access/route.ts b/src/app/api/projects/[id]/request-access/route.ts index f2b2be9..af6d544 100644 --- a/src/app/api/projects/[id]/request-access/route.ts +++ b/src/app/api/projects/[id]/request-access/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { getDb, getPool } from "@lib/db"; import { authenticatedAction } from "@lib/server-utils"; import { v4 as uuidv4 } from "uuid"; +import { logger } from "@lib/logger"; export const POST = authenticatedAction<{ error?: string; status?: string }>( async (req, { params, user }) => { @@ -46,7 +47,10 @@ export const POST = authenticatedAction<{ error?: string; status?: string }>( ); } } catch (e) { - console.error("[RequestAccess] Error checking project:", e); + logger.error( + { err: e, projectId }, + "[RequestAccess] Error checking project", + ); throw e; } @@ -66,7 +70,10 @@ export const POST = authenticatedAction<{ error?: string; status?: string }>( ); } } catch (e) { - console.error("[RequestAccess] Error checking collaborator:", e); + logger.error( + { err: e, projectId }, + "[RequestAccess] Error checking collaborator", + ); throw e; } @@ -92,7 +99,10 @@ export const POST = authenticatedAction<{ error?: string; status?: string }>( ); } } catch (e) { - console.error("[RequestAccess] Error checking existing request:", e); + logger.error( + { err: e, projectId }, + "[RequestAccess] Error checking existing request", + ); throw e; } @@ -117,12 +127,15 @@ export const POST = authenticatedAction<{ error?: string; status?: string }>( await global.updateProjectRequests(projectId); } } catch (wsError) { - console.error("WebSocket notification failed:", wsError); + logger.error( + { err: wsError, projectId }, + "WebSocket notification failed", + ); } return NextResponse.json({ status: "pending" }); } catch (error) { - console.error("[RequestAccess] Insert failed:", error); + logger.error({ err: error, projectId }, "[RequestAccess] Insert failed"); // Rethrow to let authenticatedAction handle it, or return 500 explicitly throw error; } diff --git a/src/app/api/projects/[id]/requests/route.ts b/src/app/api/projects/[id]/requests/route.ts index 2d63ecd..efbb7d1 100644 --- a/src/app/api/projects/[id]/requests/route.ts +++ b/src/app/api/projects/[id]/requests/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { getDb } from "@lib/db"; import { authenticatedAction } from "@lib/server-utils"; import { z } from "zod"; +import { logger } from "@lib/logger"; // Declare global helper declare global { @@ -113,7 +114,10 @@ export const PATCH = authenticatedAction>( await global.notifyAccessGranted(projectId, userId); } } catch (wsError) { - console.error("Access granted notification failed:", wsError); + logger.error( + { err: wsError, projectId }, + "Access granted notification failed", + ); } } else if (action === "reject") { await db @@ -137,7 +141,7 @@ export const PATCH = authenticatedAction>( await global.updateProjectRequests(projectId); } } catch (wsError) { - console.error("WebSocket update failed:", wsError); + logger.error({ err: wsError, projectId }, "WebSocket update failed"); } return NextResponse.json({ success: true }); diff --git a/src/app/api/projects/trash/route.ts b/src/app/api/projects/trash/route.ts index ef6f088..e62e1e1 100644 --- a/src/app/api/projects/trash/route.ts +++ b/src/app/api/projects/trash/route.ts @@ -1,6 +1,7 @@ import { getDb, runTransaction } from "@lib/db"; import { authenticatedAction } from "@lib/server-utils"; import { logSecurityEvent } from "@lib/audit"; +import { logger } from "@lib/logger"; import { headers } from "next/headers"; export const DELETE = authenticatedAction( @@ -34,7 +35,7 @@ export const DELETE = authenticatedAction( return { success: true }; } catch (error) { - console.error("Empty trash failed:", error); + logger.error({ error, userId: user.id }, "Empty trash failed"); await logSecurityEvent("emptyTrash", "failure", { userId: user.id, ip, diff --git a/src/app/api/system/changelog/route.ts b/src/app/api/system/changelog/route.ts index 149ebee..6b6b73f 100644 --- a/src/app/api/system/changelog/route.ts +++ b/src/app/api/system/changelog/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { logger } from "@lib/logger"; export const dynamic = "force-dynamic"; @@ -24,7 +25,7 @@ export async function GET() { const content = await response.text(); return NextResponse.json({ content }); } catch (error) { - console.error("Failed to fetch changelog:", error); + logger.error({ error }, "Failed to fetch changelog"); return NextResponse.json( { error: "Failed to fetch changelog" }, { status: 500 }, diff --git a/src/app/api/system/version/route.ts b/src/app/api/system/version/route.ts index d603da5..ce6bb9f 100644 --- a/src/app/api/system/version/route.ts +++ b/src/app/api/system/version/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { logger } from "@lib/logger"; export const dynamic = "force-dynamic"; @@ -29,7 +30,7 @@ export async function GET() { const data = await response.json(); return NextResponse.json({ latest: data.tag_name }); } catch (error) { - console.error("Failed to check for updates:", error); + logger.error({ error }, "Failed to check for updates"); return NextResponse.json( { error: "Failed to check for updates" }, { status: 500 }, diff --git a/src/app/components/VersionBadge.tsx b/src/app/components/VersionBadge.tsx index 3b5a1aa..40f6c1f 100644 --- a/src/app/components/VersionBadge.tsx +++ b/src/app/components/VersionBadge.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; +import * as semver from "semver"; import { useI18n } from "@providers/I18nProvider"; import { Modal } from "./ui/Modal"; @@ -17,15 +18,10 @@ interface ChangelogSection { } function isVersionNewer(version: string, current: string): boolean { - const v1 = version.replace(/^v/, "").split(".").map(Number); - const v2 = current.replace(/^v/, "").split(".").map(Number); - for (let i = 0; i < Math.max(v1.length, v2.length); i++) { - const a = v1[i] || 0; - const b = v2[i] || 0; - if (a > b) return true; - if (a < b) return false; - } - return false; + const a = semver.clean(version) || version; + const b = semver.clean(current) || current; + if (!semver.valid(a) || !semver.valid(b)) return false; + return semver.gt(a, b); } function parseChangelog( @@ -33,7 +29,8 @@ function parseChangelog( currentVersion: string, ): ChangelogSection[] { const sections: ChangelogSection[] = []; - const versionRegex = /^## \[(\d+\.\d+\.\d+)\]\s*-\s*(\d{4}-\d{2}-\d{2})/; + const versionRegex = + /^## \[v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\]\s*-\s*(\d{4}-\d{2}-\d{2})/; const lines = markdown.split("\n"); let current: ChangelogSection | null = null; @@ -168,21 +165,10 @@ export function VersionBadge({ currentVersion }: VersionBadgeProps) { .then((data) => { if (data.latest) { setLatestVersion(data.latest); - const clean1 = currentVersion - .replace(/^v/, "") - .split(".") - .map(Number); - const clean2 = data.latest.replace(/^v/, "").split(".").map(Number); - let updated = false; - for (let i = 0; i < Math.max(clean1.length, clean2.length); i++) { - const a = clean1[i] || 0; - const b = clean2[i] || 0; - if (b > a) { - updated = true; - break; - } - if (a > b) break; - } + const a = semver.clean(data.latest) || data.latest; + const b = semver.clean(currentVersion) || currentVersion; + const updated = + semver.valid(a) && semver.valid(b) ? semver.gt(a, b) : false; setHasUpdate(updated); } }) diff --git a/src/app/components/account/GitTokenManager.tsx b/src/app/components/account/GitTokenManager.tsx index 89ed14b..f9e5e21 100644 --- a/src/app/components/account/GitTokenManager.tsx +++ b/src/app/components/account/GitTokenManager.tsx @@ -7,13 +7,12 @@ import { Modal } from "@components/ui/Modal"; import { PlusIcon, TrashIcon, - GithubIcon, - GitlabIcon, ServerIcon, GitBranch, ChevronDown, Check, } from "lucide-react"; +import { FaGithub, FaGitlab } from "react-icons/fa"; import "./git-token-manager.css"; interface GitToken { @@ -138,9 +137,9 @@ export function GitTokenManager() { const getIcon = (provider: string) => { switch (provider) { case "github": - return ; + return ; case "gitlab": - return ; + return ; case "gitea": case "forgejo": return ; diff --git a/src/app/components/project/AddBlockModal.tsx b/src/app/components/project/AddBlockModal.tsx index b7b7c28..a6ef6a3 100644 --- a/src/app/components/project/AddBlockModal.tsx +++ b/src/app/components/project/AddBlockModal.tsx @@ -7,7 +7,6 @@ import { FileText, Link, File, - Github, Palette, Contact, Video, @@ -18,6 +17,7 @@ import { Kanban, Folder, } from "lucide-react"; +import { FaGithub } from "react-icons/fa"; import { VercelIcon } from "../icons/VercelIcon"; type AddableBlockType = @@ -50,7 +50,7 @@ const BLOCK_TYPES = [ { type: "text", icon: FileText, labelKey: "blockTypeText" }, { type: "link", icon: Link, labelKey: "blockTypeLink" }, { type: "file", icon: File, labelKey: "blockTypeFile" }, - { type: "github", icon: Github, labelKey: "blockTypeGit" }, + { type: "github", icon: FaGithub, labelKey: "blockTypeGit" }, { type: "palette", icon: Palette, labelKey: "blockTypePalette" }, { type: "contact", icon: Contact, labelKey: "blockTypeContact" }, { type: "video", icon: Video, labelKey: "blockTypeVideo" }, diff --git a/src/app/components/project/BlockFooter.tsx b/src/app/components/project/BlockFooter.tsx index 0c92f81..8532f2b 100644 --- a/src/app/components/project/BlockFooter.tsx +++ b/src/app/components/project/BlockFooter.tsx @@ -1,5 +1,6 @@ import { Lock, User } from "lucide-react"; import type { Dict } from "@providers/I18nProvider"; +import { formatDateParts } from "../../../lib/formatDate"; interface BlockFooterProps { updatedAt?: string; @@ -18,24 +19,10 @@ export function BlockFooter({ lang, children, }: BlockFooterProps) { - const formatDate = (isoString: string) => { + const formatDate = (isoString?: string) => { if (!isoString) return ""; - const date = new Date(isoString); - const options: Intl.DateTimeFormatOptions = { - day: "2-digit", - month: "2-digit", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }; - - const formatted = new Intl.DateTimeFormat( - lang === "fr" ? "fr-FR" : "en-US", - options, - ).format(date); - - return formatted.replace(",", "").replace(" ", ` ${dict.project.at} `); + const { date, time } = formatDateParts(isoString, lang); + return time ? `${date} ${dict.project.at} ${time}` : date; }; return ( diff --git a/src/app/components/project/CanvasBlock.tsx b/src/app/components/project/CanvasBlock.tsx index ca08f06..682561c 100644 --- a/src/app/components/project/CanvasBlock.tsx +++ b/src/app/components/project/CanvasBlock.tsx @@ -36,6 +36,8 @@ import { BlockReactions } from "./BlockReactions"; import { useBlockReactions } from "./hooks/useBlockReactions"; import { BlockFooter } from "./BlockFooter"; import { parseOptionalJsonRecord } from "@lib/metadata-parsers"; +import type { NoteModeShortcutHandler } from "./utils/interaction"; +import { focusProjectCanvas } from "./utils/focusCanvas"; export type BlockData = { title?: string; @@ -112,6 +114,10 @@ export type BlockData = { }, ) => void; onFolderToggle?: (folderId: string, isCollapsed: boolean) => void; + registerNoteModeShortcutHandler?: ( + blockId: string, + handler: NoteModeShortcutHandler | null, + ) => void; typingUsers?: UserPresence[]; movingUserColor?: string; projectOwnerId?: string | null; @@ -1086,6 +1092,14 @@ const CanvasBlockComponent = (props: CanvasBlockProps) => { { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + (e.target as HTMLElement)?.blur?.(); + focusProjectCanvas(); + } + }} className="block-title nodrag" placeholder={dict.blocks.title || "..."} readOnly={isReadOnly} diff --git a/src/app/components/project/CanvasEdge.tsx b/src/app/components/project/CanvasEdge.tsx index 42f1e57..cb6205e 100644 --- a/src/app/components/project/CanvasEdge.tsx +++ b/src/app/components/project/CanvasEdge.tsx @@ -11,6 +11,7 @@ import { } from "@xyflow/react"; import { useState, useEffect } from "react"; import "./canvas-edge.css"; +import { focusProjectCanvas } from "./utils/focusCanvas"; interface EdgeData extends Record { label?: string; @@ -167,10 +168,15 @@ export default function CanvasEdge({ onBlur={() => data?.onLabelSubmit?.(id, inputValue)} onKeyDown={(e) => { if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); data?.onLabelSubmit?.(id, inputValue); - } - if (e.key === "Escape") { + focusProjectCanvas(); + } else if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); data?.onLabelCancel?.(id); + focusProjectCanvas(); } }} /> diff --git a/src/app/components/project/ChecklistBlock.tsx b/src/app/components/project/ChecklistBlock.tsx index ad3537d..93f7930 100644 --- a/src/app/components/project/ChecklistBlock.tsx +++ b/src/app/components/project/ChecklistBlock.tsx @@ -27,6 +27,7 @@ import { BlockReactions } from "./BlockReactions"; import { useBlockReactions } from "./hooks/useBlockReactions"; import CustomNodeResizer from "./CustomNodeResizer"; import { parseChecklistMetadata, parseJsonRecord } from "@lib/metadata-parsers"; +import { focusProjectCanvas } from "./utils/focusCanvas"; type ChecklistBlockProps = NodeProps> & { isReadOnly?: boolean; @@ -85,6 +86,9 @@ const AutoResizeTextarea = ({ placeholder={placeholder} readOnly={readOnly} rows={1} + draggable={false} + onMouseDown={(e) => e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} onKeyDown={onKeyDown} onFocus={onFocus} onBlur={onBlur} @@ -155,6 +159,7 @@ const ChecklistBlock = memo(({ id, data, selected }: ChecklistBlockProps) => { blockElement.contains(activeElement) ) { (activeElement as HTMLElement).blur(); + focusProjectCanvas(); } } }, [selected, id]); @@ -319,7 +324,9 @@ const ChecklistBlock = memo(({ id, data, selected }: ChecklistBlockProps) => { if (e.key === "Escape") { e.preventDefault(); + e.stopPropagation(); (e.target as HTMLElement).blur(); + focusProjectCanvas(); return; } @@ -809,7 +816,7 @@ const ChecklistBlock = memo(({ id, data, selected }: ChecklistBlockProps) => { onResizeEnd={handleResizeEnd} /> -
+
@@ -832,6 +839,14 @@ const ChecklistBlock = memo(({ id, data, selected }: ChecklistBlockProps) => { { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + (e.target as HTMLElement)?.blur?.(); + focusProjectCanvas(); + } + }} className="block-title nodrag" placeholder={dict.blocks.title || "..."} readOnly={isReadOnly} @@ -865,9 +880,9 @@ const ChecklistBlock = memo(({ id, data, selected }: ChecklistBlockProps) => {
{dragTaskPreview && dropTargetIndex === index && - dragSourceIndex !== null && !( - dragSourceIndex === index || dragSourceIndex + 1 === index + typeof dragSourceIndex === "number" && + (dragSourceIndex === index || dragSourceIndex + 1 === index) ) && (
{ className={`checklist-item group ${ dragSourceIndex === index ? "is-dragging" : "" }`} - draggable={!isReadOnly} - onDragStart={(e) => handleDragStart(e, item, index)} - onDragEnd={handleDragEnd} onDrop={(e) => handleDrop(e, dropTargetIndex ?? index)} style={{ paddingLeft: `${(item.depth || 0) * 24}px` }} data-item-index={index} @@ -922,6 +934,9 @@ const ChecklistBlock = memo(({ id, data, selected }: ChecklistBlockProps) => {
handleDragStart(e, item, index)} + onDragEnd={handleDragEnd} >
@@ -960,8 +975,10 @@ const ChecklistBlock = memo(({ id, data, selected }: ChecklistBlockProps) => { {dragTaskPreview && dropTargetIndex === items.length && - dragSourceIndex !== null && - dragSourceIndex !== items.length - 1 && ( + !( + typeof dragSourceIndex === "number" && + dragSourceIndex === items.length - 1 + ) && (
> & { isReadOnly?: boolean; @@ -112,6 +113,20 @@ const ContactBlock = memo(({ id, data, selected }: ContactBlockProps) => { const handleGlobalKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape" || e.key === "Enter") { + const activeElement = document.activeElement as HTMLElement | null; + if ( + activeElement && + (["INPUT", "TEXTAREA", "SELECT"].includes(activeElement.tagName) || + activeElement.isContentEditable) + ) { + activeElement.blur(); + if (typeof e.preventDefault === "function") e.preventDefault(); + if (typeof e.stopPropagation === "function") e.stopPropagation(); + // restore focus to canvas after input blur + focusProjectCanvas(); + return; + } + setIsEditing(false); } }; diff --git a/src/app/components/project/CustomNodeResizer.tsx b/src/app/components/project/CustomNodeResizer.tsx index 89934b5..74227da 100644 --- a/src/app/components/project/CustomNodeResizer.tsx +++ b/src/app/components/project/CustomNodeResizer.tsx @@ -16,22 +16,29 @@ const DEFAULT_BLOCK_WIDTH = 320; const DEFAULT_BLOCK_HEIGHT = 240; const SNAP_THRESHOLD = 0.1; +type ResizeHandle = + | "top-left" + | "top-right" + | "bottom-left" + | "bottom-right" + | "top" + | "bottom" + | "left" + | "right"; + +interface NodeGeometry { + position: { x: number; y: number }; + width?: number; + height?: number; +} + interface ResizeState { - handle: - | "top-left" - | "top-right" - | "bottom-left" - | "bottom-right" - | "top" - | "bottom" - | "left" - | "right"; + handle: ResizeHandle; fixedCorner: { x: number; y: number }; + startPosition: { x: number; y: number }; originalDims: { width: number; height: number }; } -type ResizeHandle = ResizeState["handle"]; - interface ResizeParams { x: number; y: number; @@ -39,31 +46,29 @@ interface ResizeParams { height: number; } -interface NodeGeometry { - position: { x: number; y: number }; - width?: number; - height?: number; -} - -const extractHandleFromEvent = (event: unknown): ResizeHandle | null => { - if (event && typeof event === "object" && "target" in event) { - const target = event.target as HTMLElement; - const className = target.className; - if (!className) return null; - - if (className.includes("top") && className.includes("left")) - return "top-left"; - if (className.includes("top") && className.includes("right")) - return "top-right"; - if (className.includes("bottom") && className.includes("left")) - return "bottom-left"; - if (className.includes("bottom") && className.includes("right")) - return "bottom-right"; - if (className.includes("top")) return "top"; - if (className.includes("bottom")) return "bottom"; - if (className.includes("left")) return "left"; - if (className.includes("right")) return "right"; - } +const extractHandleFromResizeEvent = (event: unknown): ResizeHandle | null => { + if (!event || typeof event !== "object" || !("target" in event)) return null; + + const target = (event as { target?: EventTarget | null }).target; + if (!(target instanceof Element)) return null; + + const handleElement = target.closest(".react-flow__resize-control"); + if (!(handleElement instanceof HTMLElement)) return null; + + const classNames = handleElement.classList; + const isTop = classNames.contains("top"); + const isBottom = classNames.contains("bottom"); + const isLeft = classNames.contains("left"); + const isRight = classNames.contains("right"); + + if (isTop && isLeft) return "top-left"; + if (isTop && isRight) return "top-right"; + if (isBottom && isLeft) return "bottom-left"; + if (isBottom && isRight) return "bottom-right"; + if (isTop) return "top"; + if (isBottom) return "bottom"; + if (isLeft) return "left"; + if (isRight) return "right"; return null; }; @@ -111,8 +116,9 @@ const calculateFixedCorner = ( const calculateCorrectedPosition = ( node: NodeGeometry, - handle: string, + handle: ResizeHandle, fixedCorner: { x: number; y: number }, + startPosition: { x: number; y: number }, newWidth: number, newHeight: number, ): { x: number; y: number } => { @@ -130,13 +136,13 @@ const calculateCorrectedPosition = ( case "bottom-right": return { x: fixedCorner.x, y: fixedCorner.y }; case "top": - return { x: node.position.x, y: fixedCorner.y - newHeight }; + return { x: startPosition.x, y: fixedCorner.y - newHeight }; case "bottom": - return { x: node.position.x, y: fixedCorner.y }; + return { x: startPosition.x, y: fixedCorner.y }; case "left": - return { x: fixedCorner.x - newWidth, y: node.position.y }; + return { x: fixedCorner.x - newWidth, y: startPosition.y }; case "right": - return { x: fixedCorner.x, y: node.position.y }; + return { x: fixedCorner.x, y: startPosition.y }; default: return node.position; } @@ -159,6 +165,14 @@ const applySnapToGrid = ( }; }; +const resolveFallbackPosition = ( + params: { x?: number; y?: number }, + node: NodeGeometry, +): { x: number; y: number } => ({ + x: typeof params.x === "number" ? params.x : node.position.x, + y: typeof params.y === "number" ? params.y : node.position.y, +}); + const CustomNodeResizer = memo((props: NodeResizerProps) => { const { zoom } = useViewport(); const [resizeState, setResizeState] = useState(null); @@ -188,20 +202,30 @@ const CustomNodeResizer = memo((props: NodeResizerProps) => { NonNullable >( (event, params) => { - if (!resizingNode) return; - - const handle = extractHandleFromEvent(event); - if (!handle) return; + const handle = extractHandleFromResizeEvent(event); - const fixedCorner = calculateFixedCorner(resizingNode, handle); - setResizeState({ - handle, - fixedCorner, - originalDims: { - width: resizingNode.width || 0, - height: resizingNode.height || 0, - }, - }); + if (resizingNode && handle && params) { + const startRect: NodeGeometry = { + position: { x: params.x, y: params.y }, + width: params.width, + height: params.height, + }; + const fixedCorner = calculateFixedCorner(startRect, handle); + setResizeState({ + handle, + fixedCorner, + startPosition: { + x: params.x, + y: params.y, + }, + originalDims: { + width: params.width, + height: params.height, + }, + }); + } else { + setResizeState(null); + } props.onResizeStart?.(event, params); }, @@ -241,10 +265,11 @@ const CustomNodeResizer = memo((props: NodeResizerProps) => { const currentHandle = resizeState?.handle; if (!resizeState || !currentHandle) { + const fallbackPosition = resolveFallbackPosition(params, resizingNode); const corrected = { ...params, - x: resizingNode.position.x, - y: resizingNode.position.y, + x: fallbackPosition.x, + y: fallbackPosition.y, width, height, }; @@ -254,11 +279,12 @@ const CustomNodeResizer = memo((props: NodeResizerProps) => { return; } - const { fixedCorner } = resizeState; + const { fixedCorner, startPosition } = resizeState; const position = calculateCorrectedPosition( resizingNode, currentHandle, fixedCorner, + startPosition, width, height, ); diff --git a/src/app/components/project/DecisionHistory.tsx b/src/app/components/project/DecisionHistory.tsx index fc5b3b5..67ba988 100644 --- a/src/app/components/project/DecisionHistory.tsx +++ b/src/app/components/project/DecisionHistory.tsx @@ -8,6 +8,10 @@ import { useLayoutEffect, } from "react"; import { useI18n } from "@providers/I18nProvider"; +import { formatDateParts } from "../../../lib/formatDate"; +import clientLogger from "../../../lib/clientLogger"; +import { getMessage } from "../../../lib/getMessage"; +import { classifySaveError } from "../../../lib/classifySaveError"; import { Button } from "@components/ui/Button"; import { History, @@ -38,7 +42,9 @@ interface DecisionHistoryProps { projectId: string; onPreview: (stateId: string | null) => void; onApply: (stateId: string) => Promise; - onSave: (intent?: string) => Promise; + onSave: ( + intent?: string, + ) => Promise; onDelete?: (stateId: string) => Promise; onRename?: (stateId: string, newIntent: string) => Promise; isPreviewing: boolean; @@ -219,13 +225,57 @@ export function DecisionHistory({ const handleSave = async () => { setIsSaving(true); + const startTs = Date.now(); + try { - const success = await onSave(); + clientLogger.debug("DecisionHistory: starting manual save"); + + const result = await onSave(); + const durationMs = Date.now() - startTs; + + // Support both legacy boolean return and new { success, unchanged? } shape + const success = + typeof result === "boolean" + ? result + : (result && result.success) || false; + const unchanged = + typeof result === "object" && result?.unchanged === true; + if (success) { - toast.success(dict.modals.milestoneSuccess); - fetchHistory(); + if (unchanged) { + clientLogger.info("DecisionHistory: manual save skipped", { + reason: "identical_snapshot", + hint: "Snapshot identical to last saved state; nothing was saved.", + durationMs, + }); + toast.info(dict.modals.noChanges || "No changes to save"); + } else { + clientLogger.debug("DecisionHistory: manual save succeeded", { + durationMs, + }); + toast.success(dict.modals.milestoneSuccess); + fetchHistory(); + } + } else { + clientLogger.error("DecisionHistory: manual save rejected by server", { + reason: "server_rejected", + hint: "Server returned a non-success response or an internal error occurred.", + durationMs, + }); + toast.error(dict.modals.saveStateError); } - } catch { + } catch (err) { + const durationMs = Date.now() - startTs; + const classified = classifySaveError(err); + clientLogger.error("DecisionHistory: manual save error", { + reason: classified.reason, + hint: classified.hint, + message: getMessage(err), + durationMs, + }); + clientLogger.debug("DecisionHistory: manual save stack", { + stack: err instanceof Error ? err.stack || "" : "", + }); toast.error(dict.modals.saveStateError); } finally { setIsSaving(false); @@ -237,18 +287,10 @@ export function DecisionHistory({ setTimeout(checkScroll, 100); }; - const formatDate = (isoString: string) => { - const date = new Date(isoString); - return new Intl.DateTimeFormat(lang === "fr" ? "fr-FR" : "en-US", { - day: "2-digit", - month: "2-digit", - year: "2-digit", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }) - .format(date) - .replace(",", ""); // Remove comma if present + const formatDate = (isoString?: string) => { + if (!isoString) return ""; + const { date } = formatDateParts(isoString, lang); + return date; }; const handleSelectState = (stateId: string) => { diff --git a/src/app/components/project/FileBlock.tsx b/src/app/components/project/FileBlock.tsx index 74629f3..00cdeaf 100644 --- a/src/app/components/project/FileBlock.tsx +++ b/src/app/components/project/FileBlock.tsx @@ -15,11 +15,13 @@ import { Loader2, } from "lucide-react"; import { useI18n } from "@providers/I18nProvider"; +import { clientLogger } from "../../../lib/clientLogger"; import { CanvasBlockProps } from "./CanvasBlock"; import { BlockReactions } from "./BlockReactions"; import { useBlockReactions } from "./hooks/useBlockReactions"; import { BlockFooter } from "./BlockFooter"; import CustomNodeResizer from "./CustomNodeResizer"; +import { focusProjectCanvas } from "./utils/focusCanvas"; interface BlockMetadata { name?: string; @@ -277,10 +279,10 @@ const FileBlock = (props: CanvasBlockProps) => { } } else { const err = await res.json(); - console.error("Upload failed:", err); + clientLogger.error("Upload failed", err); } } catch (error) { - console.error("Upload error:", error); + clientLogger.error("Upload error", error); } }; @@ -297,7 +299,7 @@ const FileBlock = (props: CanvasBlockProps) => { link.click(); document.body.removeChild(link); } catch (error) { - console.error("Download error:", error); + clientLogger.error("Download error", error); } }; @@ -316,7 +318,7 @@ const FileBlock = (props: CanvasBlockProps) => { }, ); } catch (error) { - console.error("Delete physical file error:", error); + clientLogger.error("Delete physical file error", error); } } @@ -402,6 +404,14 @@ const FileBlock = (props: CanvasBlockProps) => { { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + (e.target as HTMLElement)?.blur?.(); + focusProjectCanvas(); + } + }} className="block-title nodrag" placeholder={dict.blocks.title || "..."} readOnly={isReadOnly} diff --git a/src/app/components/project/FolderBlock.tsx b/src/app/components/project/FolderBlock.tsx index 38a97c1..9eb72b1 100644 --- a/src/app/components/project/FolderBlock.tsx +++ b/src/app/components/project/FolderBlock.tsx @@ -189,67 +189,68 @@ const FolderBlock = memo(({ id, data, selected }: FolderBlockProps) => { onResizeEnd={handleResizeEnd} /> -
-
- - {dict.blocks.blockTypeFolder || "Folder"} -
- -
- -
+
+
+
+ + {dict.blocks.blockTypeFolder || "Folder"} +
-
- - {dict.blocks.folderChildrenCount - ? dict.blocks.folderChildrenCount.replace( - "{count}", - String(directChildrenCount), - ) - : `${directChildrenCount} children`} - +
+ +
- -
-
- + +
+
+ - + +
{ const ProviderIcon = provider === "gitlab" - ? Gitlab + ? FaGitlab : provider === "github" - ? Github + ? FaGithub : GitGraph; return ( @@ -668,7 +668,7 @@ const GitBlock = (props: CanvasBlockProps) => { onResizeEnd={handleResizeEnd} /> -
+
@@ -681,6 +681,14 @@ const GitBlock = (props: CanvasBlockProps) => { { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + (e.target as HTMLElement)?.blur?.(); + focusProjectCanvas(); + } + }} className="block-title nodrag" placeholder={dict.blocks.title || "..."} readOnly={isReadOnly} diff --git a/src/app/components/project/KanbanBlock.tsx b/src/app/components/project/KanbanBlock.tsx index 35b0078..a32f7bb 100644 --- a/src/app/components/project/KanbanBlock.tsx +++ b/src/app/components/project/KanbanBlock.tsx @@ -24,6 +24,7 @@ import "./kanban-block.css"; import { BlockReactions } from "./BlockReactions"; import { useBlockReactions } from "./hooks/useBlockReactions"; import CustomNodeResizer from "./CustomNodeResizer"; +import { focusProjectCanvas } from "./utils/focusCanvas"; interface Task { id: string; @@ -983,7 +984,7 @@ const KanbanBlock = memo(({ id, data, selected }: KanbanBlockProps) => { />
-
+
@@ -994,6 +995,14 @@ const KanbanBlock = memo(({ id, data, selected }: KanbanBlockProps) => { { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + (e.target as HTMLElement)?.blur?.(); + focusProjectCanvas(); + } + }} className="block-title nodrag" placeholder={tr("blocks.title", "...")} readOnly={isReadOnly} diff --git a/src/app/components/project/MarkdownEditor.tsx b/src/app/components/project/MarkdownEditor.tsx index baea421..7414a12 100644 --- a/src/app/components/project/MarkdownEditor.tsx +++ b/src/app/components/project/MarkdownEditor.tsx @@ -142,6 +142,7 @@ interface MarkdownEditorProps { onBlur?: () => void; onEditorReady?: (editor: Editor) => void; onLinkShortcut?: () => void; + onPreviewShortcut?: () => void; } interface MarkdownStorage { @@ -160,6 +161,7 @@ const MarkdownEditor = ({ onBlur, onEditorReady, onLinkShortcut, + onPreviewShortcut, }: MarkdownEditorProps) => { const [, setIsFocused] = useState(false); const isSyncingRef = useRef(false); @@ -403,6 +405,18 @@ const MarkdownEditor = ({ : "" }`} onClick={handleContainerClick} + onKeyDownCapture={(event) => { + if ( + !isReadOnly && + onPreviewShortcut && + (event.ctrlKey || event.metaKey) && + event.key.toLowerCase() === "p" + ) { + event.preventDefault(); + event.stopPropagation(); + onPreviewShortcut(); + } + }} > diff --git a/src/app/components/project/NoteBlock.tsx b/src/app/components/project/NoteBlock.tsx index d472f2d..aef932e 100644 --- a/src/app/components/project/NoteBlock.tsx +++ b/src/app/components/project/NoteBlock.tsx @@ -46,6 +46,12 @@ import { BlockFooter } from "./BlockFooter"; import { BlockReactions } from "./BlockReactions"; import { useBlockReactions } from "./hooks/useBlockReactions"; import CustomNodeResizer from "./CustomNodeResizer"; +import { focusProjectCanvas } from "./utils/focusCanvas"; +import { + resolveNoteModeShortcutAction, + shouldStartNoteInEditMode, + type NoteModeShortcutHandler, +} from "./utils/interaction"; import dynamic from "next/dynamic"; import { markdown } from "@codemirror/lang-markdown"; import "./markdown-editor.css"; @@ -154,9 +160,14 @@ const BubbleMenuComponent = forwardRef( onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); + e.stopPropagation(); applyLink(); + focusProjectCanvas(); } else if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); cancelLink(); + focusProjectCanvas(); } }} autoFocus @@ -336,560 +347,617 @@ const BubbleMenuComponent = forwardRef( BubbleMenuComponent.displayName = "BubbleMenuComponent"; -const NoteBlock = memo( - ({ - data, - selected, +const NoteBlock = memo(({ data, selected, id }: NoteBlockProps) => { + const { dict, lang } = useI18n(); + const { getEdges } = useReactFlow(); + + const currentUser = data.currentUser; + const projectOwnerId = data.projectOwnerId; + const ownerId = data.ownerId; + const isPreviewMode = data.isPreviewMode; + const isLocked = data.isLocked; + + const isProjectOwner = currentUser?.id && projectOwnerId === currentUser.id; + const isOwner = currentUser?.id && ownerId === currentUser.id; + const isViewer = data.userRole === "viewer"; + const isReadOnly = + isPreviewMode || + isViewer || + (isLocked ? !isOwner && !isProjectOwner : false); + const canReact = !isPreviewMode || isViewer; + + const { handleReact, handleRemoveReaction } = useBlockReactions({ id, - positionAbsoluteX, - positionAbsoluteY, - width, - height, - }: NoteBlockProps) => { - const { dict, lang } = useI18n(); - const { getEdges } = useReactFlow(); - const viewport = useViewport(); - - const currentUser = data.currentUser; - const projectOwnerId = data.projectOwnerId; - const ownerId = data.ownerId; - const isPreviewMode = data.isPreviewMode; - const isLocked = data.isLocked; - - const isProjectOwner = currentUser?.id && projectOwnerId === currentUser.id; - const isOwner = currentUser?.id && ownerId === currentUser.id; - const isViewer = data.userRole === "viewer"; - const isReadOnly = - isPreviewMode || - isViewer || - (isLocked ? !isOwner && !isProjectOwner : false); - const canReact = !isPreviewMode || isViewer; - - const { handleReact, handleRemoveReaction } = useBlockReactions({ - id, - data, - currentUser, - isReadOnly, - canReact, - }); - - const [editor, setEditor] = useState(null); - const [isEditing, setIsEditing] = useState(false); - const [showBubbleMenu, setShowBubbleMenu] = useState(false); - const [isTitleEditing, setIsTitleEditing] = useState(false); - const [isEditingLink, setIsEditingLink] = useState(false); - const [linkUrl, setLinkUrl] = useState(""); - const blockRef = useRef(null); - const menuRef = useRef(null); - const [blockRect, setBlockRect] = useState(null); - - useEffect(() => { - if (isReadOnly || !isEditing) { - setShowBubbleMenu(false); - } - }, [isReadOnly, isEditing]); - - useEffect(() => { - if (!currentUser?.vimMode && isEditing && !isReadOnly) { - setShowBubbleMenu(true); - } - }, [currentUser?.vimMode, isEditing, isReadOnly]); + data, + currentUser, + isReadOnly, + canReact, + }); + + const [editor, setEditor] = useState(null); + const [isEditing, setIsEditing] = useState(() => + shouldStartNoteInEditMode(data.content, isReadOnly), + ); + const [showBubbleMenu, setShowBubbleMenu] = useState(false); + const [isTitleEditing, setIsTitleEditing] = useState(false); + const [isEditingLink, setIsEditingLink] = useState(false); + const [linkUrl, setLinkUrl] = useState(""); + const blockRef = useRef(null); + + useEffect(() => { + if (isReadOnly || !isEditing) { + setShowBubbleMenu(false); + } + }, [isReadOnly, isEditing]); + + useEffect(() => { + if (!currentUser?.vimMode && isEditing && !isReadOnly) { + setShowBubbleMenu(true); + } + }, [currentUser?.vimMode, isEditing, isReadOnly]); + + useEffect(() => { + const isNonVimEdit = !currentUser?.vimMode && isEditing && !isReadOnly; + + if (!editor) { + if (!isNonVimEdit) setShowBubbleMenu(false); + return; + } + + const handleSelectionUpdate = () => { + if (isNonVimEdit) return; + const { from, head } = editor.state.selection; + const hasSelection = from !== head; + setShowBubbleMenu( + hasSelection && !isTitleEditing && !isReadOnly && isEditing, + ); + }; - useEffect(() => { - const isNonVimEdit = !currentUser?.vimMode && isEditing && !isReadOnly; + const handleFocus = () => { + if (isNonVimEdit) return; + if (isReadOnly || !isEditing) return; + const { from, head } = editor.state.selection; + if (from !== head) setShowBubbleMenu(true); + }; - if (!editor) { - if (!isNonVimEdit) setShowBubbleMenu(false); - return; - } + editor.on("selectionUpdate", handleSelectionUpdate); + editor.on("transaction", handleSelectionUpdate); + editor.on("focus", handleFocus); - const handleSelectionUpdate = () => { - if (isNonVimEdit) return; - const { from, head } = editor.state.selection; - const hasSelection = from !== head; - setShowBubbleMenu( - hasSelection && !isTitleEditing && !isReadOnly && isEditing, - ); - }; + return () => { + editor.off("selectionUpdate", handleSelectionUpdate); + editor.off("transaction", handleSelectionUpdate); + editor.off("focus", handleFocus); + }; + }, [editor, isTitleEditing, isReadOnly, isEditing, currentUser?.vimMode]); - const handleFocus = () => { - if (isNonVimEdit) return; - if (isReadOnly || !isEditing) return; - const { from, head } = editor.state.selection; - if (from !== head) setShowBubbleMenu(true); - }; + // Moved viewport listener to BubbleMenuContainer to avoid NoteBlock re-renders during zoom - const handleDomBlur = (e: FocusEvent) => { - if (isNonVimEdit) return; + const [title, setTitle] = useState(data.title || ""); - const relatedTarget = e.relatedTarget; - if ( - menuRef.current && - relatedTarget instanceof Node && - menuRef.current.contains(relatedTarget) - ) { - return; - } - setShowBubbleMenu(false); - }; + const edges = getEdges(); + const isHandleConnected = (handleId: string) => + edges.some( + (e) => + (e.source === id && e.sourceHandle === handleId) || + (e.target === id && e.targetHandle === handleId), + ); - editor.on("selectionUpdate", handleSelectionUpdate); - editor.on("transaction", handleSelectionUpdate); - editor.on("focus", handleFocus); - if (editor.view && !editor.isDestroyed) { - editor.view.dom.addEventListener("blur", handleDomBlur); - } + const isLeftSourceConnected = isHandleConnected("left"); + const isRightSourceConnected = isHandleConnected("right"); + const isTopSourceConnected = isHandleConnected("top"); + const isBottomSourceConnected = isHandleConnected("bottom"); - return () => { - editor.off("selectionUpdate", handleSelectionUpdate); - editor.off("transaction", handleSelectionUpdate); - editor.off("focus", handleFocus); - if (editor.view && !editor.isDestroyed && editor.view.dom) { - editor.view.dom.removeEventListener("blur", handleDomBlur); - } - }; - }, [editor, isTitleEditing, isReadOnly, isEditing, currentUser?.vimMode]); + const noteVimExtensions = useMemo(() => [markdown()], []); - useLayoutEffect(() => { - const updateRect = () => { - if (showBubbleMenu && blockRef.current) { - setBlockRect(blockRef.current.getBoundingClientRect()); - } - }; + const lastSyncedTextRef = useRef(null); + const syncTimeoutRef = useRef | null>(null); - const handleSidebarToggle = () => { - // Run updateRect for 350ms to cover the 300ms transition - const startTime = Date.now(); - const duration = 350; + const syncToYjs = useCallback( + (text: string) => { + if (!data.yText) return; - const loop = () => { - updateRect(); - if (Date.now() - startTime < duration) { - requestAnimationFrame(loop); - } - }; - requestAnimationFrame(loop); - }; - - updateRect(); - - window.addEventListener("resize", updateRect); - window.addEventListener("sidebar-toggle", handleSidebarToggle); - - return () => { - window.removeEventListener("resize", updateRect); - window.removeEventListener("sidebar-toggle", handleSidebarToggle); - }; - }, [ - showBubbleMenu, - viewport, - positionAbsoluteX, - positionAbsoluteY, - width, - height, - ]); - - const [title, setTitle] = useState(data.title || ""); - - const edges = getEdges(); - const isHandleConnected = (handleId: string) => - edges.some( - (e) => - (e.source === id && e.sourceHandle === handleId) || - (e.target === id && e.targetHandle === handleId), - ); - - const isLeftSourceConnected = isHandleConnected("left"); - const isRightSourceConnected = isHandleConnected("right"); - const isTopSourceConnected = isHandleConnected("top"); - const isBottomSourceConnected = isHandleConnected("bottom"); + if (text.length > 1000000) { + text = text.slice(0, 1000000) + "\n\n[Truncated for performance]"; + } - const noteVimExtensions = useMemo(() => [markdown()], []); + if (lastSyncedTextRef.current === text) return; - const lastSyncedTextRef = useRef(null); - const syncTimeoutRef = useRef | null>(null); + if (syncTimeoutRef.current) { + clearTimeout(syncTimeoutRef.current); + } - const syncToYjs = useCallback( - (text: string) => { + syncTimeoutRef.current = setTimeout(() => { + syncTimeoutRef.current = null; if (!data.yText) return; - if (text.length > 1000000) { - text = text.slice(0, 1000000) + "\n\n[Truncated for performance]"; - } - - if (lastSyncedTextRef.current === text) return; - - if (syncTimeoutRef.current) { - clearTimeout(syncTimeoutRef.current); + const currentText = data.yText.toString(); + if (currentText === text) { + lastSyncedTextRef.current = text; + return; } - syncTimeoutRef.current = setTimeout(() => { - syncTimeoutRef.current = null; - if (!data.yText) return; - - const currentText = data.yText.toString(); - if (currentText === text) { - lastSyncedTextRef.current = text; - return; - } - - data.yText.doc?.transact(() => { - data.yText?.delete(0, data.yText.length); - data.yText?.insert(0, text); - }); - lastSyncedTextRef.current = text; - }, 500); // 500ms debounce - }, - [data.yText], - ); + data.yText.doc?.transact(() => { + data.yText?.delete(0, data.yText.length); + data.yText?.insert(0, text); + }); + lastSyncedTextRef.current = text; + }, 500); // 500ms debounce + }, + [data.yText], + ); - useEffect(() => { - return () => { - if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current); - }; - }, []); - - useEffect(() => { - setTitle(data.title || ""); - }, [data.title]); - - const handleTitleChange = useCallback( - (e: React.ChangeEvent) => { - const newTitle = e.target.value; - setTitle(newTitle); - const now = new Date().toISOString(); - const editor = - currentUser?.displayName || - currentUser?.username || - dict.project.anonymous; - - data.onContentChange?.( - id, - data.content || "", - now, - editor, - data.metadata ? JSON.stringify(data.metadata) : undefined, - newTitle, - data.reactions, - ); - }, - [id, data, currentUser, dict], - ); + useEffect(() => { + return () => { + if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current); + }; + }, []); + + useEffect(() => { + setTitle(data.title || ""); + }, [data.title]); + + const handleTitleChange = useCallback( + (e: React.ChangeEvent) => { + const newTitle = e.target.value; + setTitle(newTitle); + const now = new Date().toISOString(); + const editor = + currentUser?.displayName || + currentUser?.username || + dict.project.anonymous; + + data.onContentChange?.( + id, + data.content || "", + now, + editor, + data.metadata ? JSON.stringify(data.metadata) : undefined, + newTitle, + data.reactions, + ); + }, + [id, data, currentUser, dict], + ); - const handleContentChange = useCallback( - (newContent: string) => { - syncToYjs(newContent); - data.onContentChange?.( - id, - newContent, - new Date().toISOString(), - data.lastEditor, - data.metadata ? JSON.stringify(data.metadata) : undefined, - title, - data.reactions, - ); - }, - [ + const handleContentChange = useCallback( + (newContent: string) => { + syncToYjs(newContent); + data.onContentChange?.( id, - data.onContentChange, + newContent, + new Date().toISOString(), data.lastEditor, - data.metadata, + data.metadata ? JSON.stringify(data.metadata) : undefined, title, - syncToYjs, - ], - ); - - const handleVimChange = useCallback( - (value: string) => { - syncToYjs(value); - data.onContentChange?.( - id, - value, - new Date().toISOString(), - data.lastEditor, - data.metadata ? JSON.stringify(data.metadata) : undefined, - title, - data.reactions, - ); - }, - [ + data.reactions, + ); + }, + [ + id, + data.onContentChange, + data.lastEditor, + data.metadata, + title, + syncToYjs, + ], + ); + + const handleVimChange = useCallback( + (value: string) => { + syncToYjs(value); + data.onContentChange?.( id, - data.onContentChange, + value, + new Date().toISOString(), data.lastEditor, - data.metadata, + data.metadata ? JSON.stringify(data.metadata) : undefined, title, - syncToYjs, - ], - ); - - const openLinkModal = useCallback(() => { - if (!editor || isReadOnly) return; - const previousUrl = editor.getAttributes("link").href; - setLinkUrl(previousUrl || ""); - setIsEditingLink(true); - setShowBubbleMenu(true); - }, [editor]); - - const applyLink = useCallback(() => { - if (!editor) return; - if (linkUrl) { - let finalUrl = linkUrl.trim(); - // If the URL doesn't start with a protocol (http://, https://, mailto:, etc.), prepend https:// - if ( - finalUrl && - !/^https?:\/\//i.test(finalUrl) && - !/^mailto:/i.test(finalUrl) && - !/^tel:/i.test(finalUrl) - ) { - finalUrl = `https://${finalUrl}`; - } - - editor - .chain() - .focus() - .extendMarkRange("link") - .setLink({ href: finalUrl }) - .run(); - } else { - editor.chain().focus().extendMarkRange("link").unsetLink().run(); + data.reactions, + ); + }, + [ + id, + data.onContentChange, + data.lastEditor, + data.metadata, + title, + syncToYjs, + ], + ); + + const openLinkModal = useCallback(() => { + if (!editor || isReadOnly) return; + const previousUrl = editor.getAttributes("link").href; + setLinkUrl(previousUrl || ""); + setIsEditingLink(true); + setShowBubbleMenu(true); + }, [editor]); + + const applyLink = useCallback(() => { + if (!editor) return; + if (linkUrl) { + let finalUrl = linkUrl.trim(); + // If the URL doesn't start with a protocol (http://, https://, mailto:, etc.), prepend https:// + if ( + finalUrl && + !/^https?:\/\//i.test(finalUrl) && + !/^mailto:/i.test(finalUrl) && + !/^tel:/i.test(finalUrl) + ) { + finalUrl = `https://${finalUrl}`; } - setIsEditingLink(false); - }, [editor, linkUrl]); - const removeLink = useCallback(() => { - if (!editor) return; + editor + .chain() + .focus() + .extendMarkRange("link") + .setLink({ href: finalUrl }) + .run(); + } else { editor.chain().focus().extendMarkRange("link").unsetLink().run(); - setIsEditingLink(false); - }, [editor]); - - const cancelLink = useCallback(() => { - setIsEditingLink(false); - setLinkUrl(""); - editor?.commands.focus(); - }, [editor]); - - const handleResize = useCallback( - ( - _evt: unknown, - params: { width: number; height: number; x: number; y: number }, - ) => { - const { width, height, x, y } = params; - const onResize = data.onResize; - onResize?.(id, { - width: Math.round(width), - height: Math.round(height), - x: Math.round(x), - y: Math.round(y), - }); - }, - [id, data], - ); + } + setIsEditingLink(false); + }, [editor, linkUrl]); + + const removeLink = useCallback(() => { + if (!editor) return; + editor.chain().focus().extendMarkRange("link").unsetLink().run(); + setIsEditingLink(false); + }, [editor]); + + const cancelLink = useCallback(() => { + setIsEditingLink(false); + setLinkUrl(""); + editor?.commands.focus(); + }, [editor]); + + const handleResize = useCallback( + ( + _evt: unknown, + params: { width: number; height: number; x: number; y: number }, + ) => { + const { width, height, x, y } = params; + const onResize = data.onResize; + onResize?.(id, { + width: Math.round(width), + height: Math.round(height), + x: Math.round(x), + y: Math.round(y), + }); + }, + [id, data], + ); + + const handleResizeEnd = useCallback( + ( + _evt: unknown, + params: { width: number; height: number; x: number; y: number }, + ) => { + const { width, height, x, y } = params; + const onResizeEnd = data.onResizeEnd; + onResizeEnd?.(id, { + width: Math.round(width), + height: Math.round(height), + x: Math.round(x), + y: Math.round(y), + }); + }, + [id, data], + ); + + const handleNoteModeShortcut = useCallback( + (key) => { + const action = resolveNoteModeShortcutAction({ + key, + isEditing, + isReadOnly, + vimMode: !!currentUser?.vimMode, + hasRichTextEditor: !!editor && !currentUser?.vimMode, + }); - const handleResizeEnd = useCallback( - ( - _evt: unknown, - params: { width: number; height: number; x: number; y: number }, - ) => { - const { width, height, x, y } = params; - const onResizeEnd = data.onResizeEnd; - onResizeEnd?.(id, { - width: Math.round(width), - height: Math.round(height), - x: Math.round(x), - y: Math.round(y), - }); - }, - [id, data], - ); + switch (action) { + case "switchToPreview": + setIsEditing(false); + return "handled"; + case "switchToEdit": + setIsEditing(true); + return "handled"; + case "toggleInlineCode": + editor?.chain().focus().toggleCode().run(); + return "handled"; + case "noop": + return "handled"; + case "passThrough": + default: + return "passThrough"; + } + }, + [currentUser?.vimMode, editor, isEditing, isReadOnly], + ); - return ( - <> - -
-
-
-
- - - {dict.blocks.blockTypeText || "Note"} - -
-
- setIsTitleEditing(true)} - onBlur={() => setIsTitleEditing(false)} - className="block-title nodrag" - placeholder={dict.blocks.title || "..."} - disabled={isReadOnly} - /> -
+ useEffect(() => { + data.registerNoteModeShortcutHandler?.(id, handleNoteModeShortcut); + + return () => { + data.registerNoteModeShortcutHandler?.(id, null); + }; + }, [data.registerNoteModeShortcutHandler, handleNoteModeShortcut, id]); + + const handleEditorPreviewShortcut = useCallback(() => { + handleNoteModeShortcut("p"); + }, [handleNoteModeShortcut]); + + return ( + <> + +
+
+
+
+ + + {dict.blocks.blockTypeText || "Note"} + +
+
+ setIsTitleEditing(true)} + onBlur={() => setIsTitleEditing(false)} + onKeyDown={(e) => { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + (e.target as HTMLElement)?.blur?.(); + focusProjectCanvas(); + } + }} + className="block-title nodrag" + placeholder={dict.blocks.title || "..."} + disabled={isReadOnly} + />
+
-
e.preventDefault()} - onWheel={(e) => e.stopPropagation()} - > - {isEditing && !isReadOnly ? ( - currentUser?.vimMode ? ( - - ) : ( - - ) +
e.preventDefault()} + onWheel={(e) => e.stopPropagation()} + > + {isEditing && !isReadOnly ? ( + currentUser?.vimMode ? ( + ) : ( - )} -
- - - {!isReadOnly && ( -
- - -
- )} -
+ ) + ) : ( + + )}
- - - {/* Handles for connections - Left Side */} - - {!isLeftSourceConnected &&
} - - - {/* Handles for connections - Right Side */} - - {!isRightSourceConnected &&
} - - - {/* Handles for connections - Top Side */} - - {!isTopSourceConnected &&
} - - - {/* Handles for connections - Bottom Side */} - - {!isBottomSourceConnected &&
} - + {!isReadOnly && ( +
+ + +
+ )} +
- {showBubbleMenu && - editor && - blockRect && - createPortal( - , - document.getElementById("app-main-container") || document.body, - )} - + + + {/* Handles for connections - Left Side */} + + {!isLeftSourceConnected &&
} + + + {/* Handles for connections - Right Side */} + + {!isRightSourceConnected &&
} + + + {/* Handles for connections - Top Side */} + + {!isTopSourceConnected &&
} + + + {/* Handles for connections - Bottom Side */} + + {!isBottomSourceConnected &&
} + +
+ + {showBubbleMenu && editor && ( + + )} + + ); +}); + +const NoteBubbleMenu = memo( + ({ + editor, + isEditingLink, + linkUrl, + setLinkUrl, + openLinkModal, + applyLink, + removeLink, + cancelLink, + blockRef, + showBubbleMenu, + }: { + editor: Editor; + isEditingLink: boolean; + linkUrl: string; + setLinkUrl: (url: string) => void; + openLinkModal: () => void; + applyLink: () => void; + removeLink: () => void; + cancelLink: () => void; + blockRef: React.RefObject; + showBubbleMenu: boolean; + }) => { + const viewport = useViewport(); + const [blockRect, setBlockRect] = useState(null); + const menuRef = useRef(null); + + useLayoutEffect(() => { + const updateRect = () => { + if (showBubbleMenu && blockRef.current) { + setBlockRect(blockRef.current.getBoundingClientRect()); + } + }; + + const handleSidebarToggle = () => { + const startTime = Date.now(); + const duration = 350; + const loop = () => { + updateRect(); + if (Date.now() - startTime < duration) { + requestAnimationFrame(loop); + } + }; + requestAnimationFrame(loop); + }; + + updateRect(); + window.addEventListener("resize", updateRect); + window.addEventListener("sidebar-toggle", handleSidebarToggle); + + return () => { + window.removeEventListener("resize", updateRect); + window.removeEventListener("sidebar-toggle", handleSidebarToggle); + }; + }, [showBubbleMenu, viewport, blockRef]); + + if (!blockRect) return null; + + return createPortal( + , + document.getElementById("app-main-container") || document.body, ); }, ); +NoteBubbleMenu.displayName = "NoteBubbleMenu"; + NoteBlock.displayName = "NoteBlock"; export default NoteBlock; diff --git a/src/app/components/project/PaletteBlock.tsx b/src/app/components/project/PaletteBlock.tsx index 8a27900..89344d1 100644 --- a/src/app/components/project/PaletteBlock.tsx +++ b/src/app/components/project/PaletteBlock.tsx @@ -19,6 +19,7 @@ import "./palette-block.css"; import { BlockReactions } from "./BlockReactions"; import { useBlockReactions } from "./hooks/useBlockReactions"; import CustomNodeResizer from "./CustomNodeResizer"; +import { focusProjectCanvas } from "./utils/focusCanvas"; import { parsePaletteMetadata } from "@lib/metadata-parsers"; type PaletteBlockProps = NodeProps> & { @@ -282,6 +283,14 @@ const PaletteBlock = memo(({ id, data, selected }: PaletteBlockProps) => { { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + (e.target as HTMLElement)?.blur?.(); + focusProjectCanvas(); + } + }} className="block-title nodrag" placeholder={dict.blocks.title || "..."} readOnly={isReadOnly} diff --git a/src/app/components/project/ProjectCanvas.tsx b/src/app/components/project/ProjectCanvas.tsx index fd9e5e5..3942967 100644 --- a/src/app/components/project/ProjectCanvas.tsx +++ b/src/app/components/project/ProjectCanvas.tsx @@ -6,8 +6,6 @@ import { ReactFlowProvider, ControlButton, Panel, - Background, - BackgroundVariant, ConnectionMode, useReactFlow as useReactFlowHook, Node, @@ -46,6 +44,9 @@ import { IndexeddbPersistence } from "y-indexeddb"; import { useUser } from "@providers/UserProvider"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; +import { clientLogger } from "../../../lib/clientLogger"; +import { getMessage } from "../../../lib/getMessage"; +import { classifyIndexedDbError } from "../../../lib/classifyIndexedDbError"; import { useState, useEffect, @@ -65,12 +66,12 @@ import { File as FileIcon, Undo2, Redo2, - Figma, Share2, - Github, Loader2, Menu, } from "lucide-react"; +import { FaGithub } from "react-icons/fa"; +import { SiFigma } from "react-icons/si"; import { DecisionHistory } from "./DecisionHistory"; import { ShareModal } from "./ShareModal"; import { DownloadButton } from "./DownloadButton"; @@ -83,7 +84,14 @@ import { useProjectCanvasState, UserPresence, } from "./hooks/useProjectCanvasState"; +import { focusProjectCanvas } from "./utils/focusCanvas"; import { DEFAULT_VIEWPORT } from "./utils/constants"; +import { + getSelectedNoteBlockIdForShortcut, + shouldIgnoreNodeContextMenuShortcut, + type NoteModeShortcutHandler, + type NoteModeShortcutKey, +} from "./utils/interaction"; import { useTouchGestures } from "./hooks/useTouchGestures"; import { useCanvasTouchViewport } from "./hooks/useCanvasTouchViewport"; const FIXED_EXTENT: [[number, number], [number, number]] = [ @@ -343,57 +351,146 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { useEffect(() => { if (typeof window === "undefined" || !initialProjectId) return; - const doc = new Y.Doc(); - const wsProvider = new WebsocketProvider( - `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${ - window.location.host - }/yjs`, - `project-${initialProjectId}`, - doc, - { connect: true }, - ); + let doc: Y.Doc | null = new Y.Doc(); + let wsProvider: WebsocketProvider | null = null; + let indexeddbProvider: IndexeddbPersistence | null = null; - wsProvider.on("sync", (data: boolean) => { - setIsRemoteSynced(data); - }); + try { + clientLogger.debug("yjs:init:start"); - wsProvider.on("status", (event: { status: string }) => { - setIsSocketConnected(event.status === "connected"); - }); - - wsProvider.on("connection-close", (event: { code?: number } | null) => { - if (event?.code === 4003) { - wsProvider.disconnect(); - toast.error(dictRef.current.common.accessRevoked || "Access revoked"); - routerRef.current.push("/home"); + try { + const update = Y.encodeStateAsUpdate(doc!); + const docSizeBytes = (update && (update as Uint8Array).byteLength) || 0; + clientLogger.debug("yjs:doc:estimated_size_bytes", { docSizeBytes }); + } catch (e) { + clientLogger.debug("yjs:doc:size_estimate_failed", String(e)); } - }); - const indexeddbProvider = new IndexeddbPersistence( - `project-${initialProjectId}`, - doc, - ); + wsProvider = new WebsocketProvider( + `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${ + window.location.host + }/yjs`, + `project-${initialProjectId}`, + doc, + { connect: true }, + ); - indexeddbProvider.on("synced", () => { - setIsLocalSynced(true); - }); + wsProvider.on("sync", (data: boolean) => { + let size = 0; + try { + const u = Y.encodeStateAsUpdate(doc!); + size = (u && (u as Uint8Array).byteLength) || 0; + } catch { + void 0; + } + clientLogger.debug("yjs:sync", { + synced: Boolean(data), + docSizeBytes: size, + }); + setIsRemoteSynced(Boolean(data)); + }); - setYjsData({ yDoc: doc, provider: wsProvider }); + wsProvider.on("status", (event: { status: string }) => { + clientLogger.info("yjs:status", { status: event.status }); + setIsSocketConnected(event.status === "connected"); + }); - // Periodic check to ensure status is accurate even if events are missed - const checkInterval = setInterval(() => { - setIsSocketConnected(wsProvider.wsconnected); - }, 3000); + wsProvider.on("connection-close", (event: { code?: number } | null) => { + clientLogger.warn("yjs:connection-close", { + code: event?.code ?? null, + }); + if (event?.code === 4003) { + wsProvider?.disconnect(); + toast.error(dictRef.current.common.accessRevoked || "Access revoked"); + routerRef.current.push("/home"); + } + }); - return () => { - clearInterval(checkInterval); - wsProvider.on("sync", () => {}); - wsProvider.on("status", () => {}); - wsProvider.on("connection-close", () => {}); - wsProvider.destroy(); - indexeddbProvider.destroy(); - doc.destroy(); - }; + try { + indexeddbProvider = new IndexeddbPersistence( + `project-${initialProjectId}`, + doc!, + ); + indexeddbProvider.on("synced", () => { + setIsLocalSynced(true); + clientLogger.debug("indexeddb:synced"); + }); + } catch (err) { + const classified = classifyIndexedDbError(err); + clientLogger.error("indexeddb:init:error", { + reason: classified.reason, + hint: classified.hint, + message: getMessage(err), + }); + + try { + if (navigator?.storage?.estimate) { + navigator.storage + .estimate() + .then((estimate: { usage?: number; quota?: number }) => { + clientLogger.debug("indexeddb:storage:estimate", { + usage: estimate?.usage ?? null, + quota: estimate?.quota ?? null, + usageRatio: + estimate?.usage && estimate?.quota + ? estimate.usage / estimate.quota + : null, + }); + }) + .catch((e) => + clientLogger.debug( + "indexeddb:storage:estimate:error", + String(e), + ), + ); + } + } catch (e) { + clientLogger.debug("indexeddb:storage:estimate:error", String(e)); + } + } + + setYjsData({ yDoc: doc!, provider: wsProvider }); + clientLogger.debug("yjs:init:complete"); + + const checkInterval = setInterval(() => { + try { + setIsSocketConnected(Boolean(wsProvider?.wsconnected)); + } catch { + void 0; + } + }, 3000); + + return () => { + clearInterval(checkInterval); + try { + wsProvider?.on("sync", () => {}); + wsProvider?.on("status", () => {}); + wsProvider?.on("connection-close", () => {}); + wsProvider?.destroy?.(); + } catch { + void 0; + } + try { + indexeddbProvider?.destroy?.(); + } catch { + void 0; + } + try { + doc?.destroy?.(); + } catch { + void 0; + } + }; + } catch (err) { + clientLogger.error("yjs:init:failed", { + message: getMessage(err), + }); + try { + doc?.destroy?.(); + } catch { + void 0; + } + } }, [initialProjectId]); const { yDoc, provider } = yjsData || { yDoc: null, provider: null }; @@ -419,7 +516,7 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { overrideBlocks?: Node[], overrideLinks?: Edge[], options?: { isAuto?: boolean }, - ) => Promise) + ) => Promise) | null >(null); @@ -485,6 +582,7 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { onPaneClick: originalOnPaneClick, onLinkClick, handleCreateBlock, + handleDuplicateBlock, onExternalDragEnter, onExternalDragLeave, onExternalDragOver, @@ -506,7 +604,6 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { canRedo, hasSeenOnboarding, helperLines, - isReady, } = useProjectCanvasState( initialProjectId, currentUser, @@ -539,6 +636,20 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { position: { x: number; y: number }; } | null>(null); const mobileActionsRef = useRef(null); + const noteModeShortcutHandlersRef = useRef( + new Map(), + ); + + const registerNoteModeShortcutHandler = useCallback( + (blockId: string, handler: NoteModeShortcutHandler | null) => { + if (handler) { + noteModeShortcutHandlersRef.current.set(blockId, handler); + } else { + noteModeShortcutHandlersRef.current.delete(blockId); + } + }, + [], + ); useEffect(() => { if (typeof window === "undefined") return; @@ -599,20 +710,45 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { } if (e.ctrlKey || e.metaKey) { - const target = e.target as HTMLElement; + const target = e.target as HTMLElement | null; + const activeElement = + document.activeElement instanceof HTMLElement + ? document.activeElement + : target; + const shortcutKey = e.key.toLowerCase(); const isEditing = - ["INPUT", "TEXTAREA"].includes(target.tagName) || - target.isContentEditable; + !!activeElement && + (["INPUT", "TEXTAREA", "SELECT"].includes(activeElement.tagName) || + activeElement.isContentEditable); + if (!isEditing) { - if (e.key === "p") { + if (shortcutKey === "p" || shortcutKey === "e") { + const noteBlockId = getSelectedNoteBlockIdForShortcut({ + blocks, + activeElement, + }); + const shortcutResult = noteBlockId + ? noteModeShortcutHandlersRef.current.get(noteBlockId)?.( + shortcutKey as NoteModeShortcutKey, + ) + : undefined; + + if (shortcutResult === "handled") { + e.preventDefault(); + e.stopPropagation(); + return; + } + } + + if (shortcutKey === "p") { e.preventDefault(); e.stopPropagation(); setIsPaletteOpen((v) => !v); - } else if (e.key === "a") { + } else if (shortcutKey === "a") { e.preventDefault(); e.stopPropagation(); setIsAddBlockOpen((v) => !v); - } else if (e.key === "h") { + } else if (shortcutKey === "h") { e.preventDefault(); e.stopPropagation(); setIsHistoryOpen((v) => !v); @@ -622,7 +758,7 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { }; window.addEventListener("keydown", handleKeyDown, true); return () => window.removeEventListener("keydown", handleKeyDown, true); - }, [pendingConnection]); + }, [blocks, pendingConnection]); const [pendingRequestsCount, setPendingRequestsCount] = useState(0); useEffect(() => { @@ -647,7 +783,7 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { setPendingRequestsCount(pending); } } catch (e) { - console.error("Failed to fetch requests count", e); + clientLogger.error("Failed to fetch requests count", e); } }; @@ -832,6 +968,11 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { // Clear context menu as per original onBlockClick originalOnPaneClick(); + if (shouldIgnoreNodeContextMenuShortcut(event.target)) { + lastNodeClickRef.current = null; + return; + } + const now = Date.now(); if (lastNodeClickRef.current && lastNodeClickRef.current.id === node.id) { const diff = now - lastNodeClickRef.current.time; @@ -1160,6 +1301,7 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { ...block.data, isPreviewMode, currentUser: currentUser || undefined, + registerNoteModeShortcutHandler, userRole: currentUserRole || undefined, }, })); @@ -1169,9 +1311,39 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { currentUser, currentUserRole, newBlockId, + registerNoteModeShortcutHandler, visibleBlockIds, ]); + // Focus newly created / duplicated block. + useEffect(() => { + if (!newBlockId) return; + const id = newBlockId; + + const timer = setTimeout(() => { + try { + // Ensure the node is present in the DOM, then place keyboard focus + // back on the canvas container so the block is selected. + const blockEl = document.querySelector( + `[data-id="${id}"]`, + ) as HTMLElement | null; + if (blockEl) { + try { + blockEl.scrollIntoView({ block: "center", inline: "center" }); + } catch { + // ignore + } + } + + focusProjectCanvas(); + } catch { + // ignore + } + }, 50); + + return () => clearTimeout(timer); + }, [newBlockId, blocks]); + if (!isAccessValidated) { return (
@@ -1274,470 +1446,458 @@ function ProjectCanvasContent({ initialProjectId }: ProjectCanvasProps) { deleteDraft, }} > -
+ !blocks.find((b) => b.id === edge.source && b.hidden) && + !blocks.find((b) => b.id === edge.target && b.hidden) && + visibleBlockIds.has(edge.source) && + visibleBlockIds.has(edge.target), + )} + onNodesChange={isPreviewMode ? undefined : onBlocksChange} + onEdgesChange={isPreviewMode ? undefined : onLinksChange} + onNodeDragStart={isPreviewMode ? undefined : onBlockDragStart} + onNodeDrag={isPreviewMode ? undefined : onBlockDrag} + onNodeDragStop={isPreviewMode ? undefined : onBlockDragStop} + onConnect={isPreviewMode ? undefined : onConnectWithSnapshot} + onConnectStart={isPreviewMode ? undefined : onConnectStart} + onConnectEnd={isPreviewMode ? undefined : onConnectEnd} + isValidConnection={isValidConnection} + onPointerMove={onPointerMove} + onPointerLeave={onPointerLeave} + onPaneContextMenu={(e) => { + if ( + pointerTypeRef.current === "touch" || + pointerTypeRef.current === "pen" + ) { + e.preventDefault(); + return; + } + onPaneContextMenu(e); + }} + onNodeContextMenu={onBlockContextMenu} + onEdgeContextMenu={onEdgeContextMenu} + onPaneClick={handlePaneClick} + onNodeClick={handleNodeClick} + onEdgeClick={onLinkClick} + onEdgeDoubleClick={onLinkDoubleClick} + onMove={onMove} + onViewportChange={onViewportChange} + zoomOnPinch={true} + zoomOnDoubleClick={false} + nodeTypes={blockTypes} + edgeTypes={linkTypes} + defaultViewport={DEFAULT_VIEWPORT} + connectionMode={ConnectionMode.Loose} + connectionRadius={30} + translateExtent={FIXED_EXTENT} + minZoom={0.1} + maxZoom={4} + deleteKeyCode={null} + disableKeyboardA11y + selectionOnDrag={!isReadOnly} + selectionKeyCode={null} + nodesDraggable={!isReadOnly} + nodesConnectable={!isReadOnly} + elementsSelectable={true} + edgesReconnectable={!isReadOnly} + panOnScroll + panOnDrag={true} + multiSelectionKeyCode="Control" + fitView + onlyRenderVisibleElements={true} + className={`project-canvas ${isReadOnly ? "read-only" : ""}`} + proOptions={{ hideAttribution: true }} > - - !blocks.find((b) => b.id === edge.source && b.hidden) && - !blocks.find((b) => b.id === edge.target && b.hidden) && - visibleBlockIds.has(edge.source) && - visibleBlockIds.has(edge.target), - )} - onNodesChange={isPreviewMode ? undefined : onBlocksChange} - onEdgesChange={isPreviewMode ? undefined : onLinksChange} - onNodeDragStart={isPreviewMode ? undefined : onBlockDragStart} - onNodeDrag={isPreviewMode ? undefined : onBlockDrag} - onNodeDragStop={isPreviewMode ? undefined : onBlockDragStop} - onConnect={isPreviewMode ? undefined : onConnectWithSnapshot} - onConnectStart={isPreviewMode ? undefined : onConnectStart} - onConnectEnd={isPreviewMode ? undefined : onConnectEnd} - isValidConnection={isValidConnection} - onPointerMove={onPointerMove} - onPointerLeave={onPointerLeave} - onPaneContextMenu={(e) => { - if ( - pointerTypeRef.current === "touch" || - pointerTypeRef.current === "pen" - ) { - e.preventDefault(); - return; - } - onPaneContextMenu(e); - }} - onNodeContextMenu={onBlockContextMenu} - onEdgeContextMenu={onEdgeContextMenu} - onPaneClick={handlePaneClick} - onNodeClick={handleNodeClick} - onEdgeClick={onLinkClick} - onEdgeDoubleClick={onLinkDoubleClick} - onMove={onMove} - onViewportChange={onViewportChange} - zoomOnPinch={true} - zoomOnDoubleClick={false} - nodeTypes={blockTypes} - edgeTypes={linkTypes} - defaultViewport={DEFAULT_VIEWPORT} - connectionMode={ConnectionMode.Loose} - connectionRadius={30} - translateExtent={FIXED_EXTENT} - minZoom={0.1} - maxZoom={4} - deleteKeyCode={null} - selectionOnDrag={!isReadOnly} - selectionKeyCode={null} - nodesDraggable={!isReadOnly} - nodesConnectable={!isReadOnly} - elementsSelectable={true} - edgesReconnectable={!isReadOnly} - panOnScroll - panOnDrag={true} - multiSelectionKeyCode="Control" - fitView - className={`project-canvas ${isReadOnly ? "read-only" : ""}`} - proOptions={{ hideAttribution: true }} +
+ -
+ + {!isReadOnly && ( - + - {!isReadOnly && ( - - - - )} - + )} + {/* Background disabled to prevent global rasterization blur */} - {!hasSeenOnboarding && isCoreOnly && !isPreviewMode && ( - -
-
- -
- -
- -
-
-

Magic Paste

-

{dict.project.onboardingHint}

-
-
- - )} + {!hasSeenOnboarding && isCoreOnly && !isPreviewMode && ( - {!isPreviewMode && !isMobileTopbar && ( -
- - {dict.project.shareCursor} - - { - e.stopPropagation(); - setShareCursor(e.target.checked); - }} - /> - +
+
+ +
+ +
+ +
+
+

Magic Paste

+

{dict.project.onboardingHint}

+
+
+ + )} + + {!isPreviewMode && !isMobileTopbar && ( +
+ + {dict.project.shareCursor} + + { + e.stopPropagation(); + setShareCursor(e.target.checked); + }} + /> + + +
+ )} +
+ + + {isPreviewMode && ( +
+ + {dict.canvas.previewMode} + +
-
- )} - - - - {isPreviewMode && ( -
- - {dict.canvas.previewMode} - -
+ {currentUser?.id === projectOwnerId && ( - {currentUser?.id === projectOwnerId && ( - - )} -
+ )}
- )} +
+ )} -
- {!isPreviewMode && ( -
- -
- {presenceUsers.map((u) => ( +
+ {!isPreviewMode && ( +
+ +
+ {presenceUsers.map((u) => ( +
-
- {u.displayName -
- -
- {u.displayName || u.username} -
-
+ {u.displayName
- ))} -
+ +
+ {u.displayName || u.username} +
+
+
+ ))}
- )} +
+ )} - {isMobileTopbar ? ( -
+ + + - {isMobileActionsOpen && ( -
- {!isPreviewMode && ( - - )} - - {!isPreviewMode && ( - - )} - - {!isPreviewMode && ( - - )} - - {currentUserRole !== "viewer" && ( - - )} - - {currentUser?.id === projectOwnerId && ( - - )} - - {!isPreviewMode && ( - - )} -
- )} -
- ) : ( -
- {currentUserRole !== "viewer" && ( -
- + )} + + {!isPreviewMode && ( + + )} + + {currentUserRole !== "viewer" && ( + - {pendingRequestsCount > 0 && ( - - {pendingRequestsCount} - - )} -
- )} - {currentUser?.id === projectOwnerId && ( + {dict.project.access || "Access"} + {pendingRequestsCount > 0 && ( + + {pendingRequestsCount} + + )} + + )} + + {currentUser?.id === projectOwnerId && ( + + )} + + {!isPreviewMode && ( + + )} +
+ )} +
+ ) : ( +
+ {currentUserRole !== "viewer" && ( +
- )} - -
- )} -
- - - {isMobileTopbar && ( - - )} + {pendingRequestsCount > 0 && ( + + {pendingRequestsCount} + + )} +
+ )} + {currentUser?.id === projectOwnerId && ( + + )} + +
+ )} +
+ + + {isMobileTopbar && ( + + )} -
-
- - {zoom}% - -
+
+
+ + {zoom}% +
+
- + - - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + {contextMenu && (
+