diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..35eb4cd --- /dev/null +++ b/.env.template @@ -0,0 +1,30 @@ +# ── Required ──────────────────────────────────────────────────────────────── +# Full URL of the BigBlueButton server API (must end with /bigbluebutton/) +BBB_URL="https://your.bbb.server/bigbluebutton/" + +# Shared secret of the BigBlueButton server +BBB_SECRET="yoursupersecretkey" + +# ── Optional ──────────────────────────────────────────────────────────────── +# Direct URL to the plugin manifest (skips the server-side availability check). +# Use this when the plugin is not deployed to the default BBB plugins path. +# Example: PICK_RANDOM_USER_PLUGIN_URL="https://your.bbb.server/plugins/pick-random-user-plugin/dist/manifest.json" +PICK_RANDOM_USER_PLUGIN_URL="" + +# Name of the local Docker container running BigBlueButton (only needed when +# deploying the built plugin into a local container with the copy script). +LOCAL_CONTAINER_NAME="bbb-local-container" + +# Multiply all Playwright timeouts by this factor. +# Defaults to 2 in CI and 1 locally when not set. +TIMEOUT_MULTIPLIER=1 + +# Set to "true" to enable CI mode (1 worker, retries, blob reporter). +CI="false" + +# Public URL of a clientSettingsOverride JSON that sets preventCloseDelaySeconds: 10. +# Required for the countdown / close-prevention behavioural tests. +# Must be reachable by the BBB server (a GitHub Gist raw URL works well). +# Leave empty to skip those tests. +# Example: PREVENT_CLOSE_DELAY_SETTINGS_URL="https://gist.githubusercontent.com///raw//bbb-pick-random-user-prevent-close-delay.json" +PREVENT_CLOSE_DELAY_SETTINGS_URL="https://bigbluebutton.nyc3.digitaloceanspaces.com/plugins/assets/bbb-plugin-pick-random-user/prevent-close-delay-10s.json" diff --git a/.github/workflows/build-plugin-deb-package.yml b/.github/workflows/build-plugin-deb-package.yml index 43bd609..942f4e1 100644 --- a/.github/workflows/build-plugin-deb-package.yml +++ b/.github/workflows/build-plugin-deb-package.yml @@ -2,11 +2,11 @@ name: "📦 Build .deb package for plugin" on: push: - branches: [ "main"] + branches: [ "v0.0.x"] paths-ignore: - debian/changelog pull_request: - branches: [ "main" ] + branches: [ "v0.0.x" ] paths-ignore: - debian/changelog diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml index 3f58930..c70b353 100644 --- a/.github/workflows/publish-tag.yml +++ b/.github/workflows/publish-tag.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '20' + node-version-file: '.nvmrc' - name: Get triggering user's email run: | @@ -34,11 +34,29 @@ jobs: git config user.name "${{ github.actor }}" git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com" - - - name: Bump version and push tag + - name: Bump version, update manifest, commit and tag run: | + set -euo pipefail + npm install - npm version ${{ github.event.inputs.release_type }} -m "chore: bump version to %s" + + # 1) Bump package.json + package-lock.json but DO NOT commit/tag yet + npm version ${{ github.event.inputs.release_type }} --no-git-tag-version + + # 2) Read the new version from package.json + NEW_VERSION=$(node -p "require('./package.json').version") + echo "New version is: $NEW_VERSION" + + # 3) Update manifest.json's "version" field to match + jq --arg v "$NEW_VERSION" '.version = $v' manifest.json > manifest.json.tmp + mv manifest.json.tmp manifest.json + + # 4) Commit everything and create an annotated tag + git add package.json package-lock.json manifest.json + git commit -m "chore: bump version to $NEW_VERSION" + git tag -a "v$NEW_VERSION" -m "v$NEW_VERSION" + + # 5) Push commit and tag git push --follow-tags env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-publish-do-spaces.yml b/.github/workflows/release-publish-do-spaces.yml new file mode 100644 index 0000000..89db09c --- /dev/null +++ b/.github/workflows/release-publish-do-spaces.yml @@ -0,0 +1,50 @@ +name: Release - publish to DO Spaces +on: + push: + tags: + - 'v*' +jobs: + publish-to-do-spaces: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Check DO Spaces secrets + id: check-secrets + env: + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} + S3_BUCKET: ${{ secrets.S3_BUCKET }} + S3_ENDPOINT_URL: ${{ secrets.S3_ENDPOINT_URL }} + run: | + if [ -z "$S3_ACCESS_KEY" ] || [ -z "$S3_SECRET_KEY" ] || [ -z "$S3_BUCKET" ] || [ -z "$S3_ENDPOINT_URL" ]; then + echo "DO Spaces secrets not configured, skipping." + echo "configured=false" >> $GITHUB_OUTPUT + else + echo "configured=true" >> $GITHUB_OUTPUT + fi + + - name: Install node + if: steps.check-secrets.outputs.configured == 'true' + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Install dependencies + if: steps.check-secrets.outputs.configured == 'true' + run: npm ci + + - name: Build dist + if: steps.check-secrets.outputs.configured == 'true' + run: npm run build-bundle + + - name: Publish dist to Digital Ocean Spaces + if: steps.check-secrets.outputs.configured == 'true' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }} + run: | + aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }}/${{ github.ref_name }}/ \ + --endpoint-url ${{ secrets.S3_ENDPOINT_URL }} diff --git a/.github/workflows/release-upload-assets.yml b/.github/workflows/release-upload-assets.yml new file mode 100644 index 0000000..88b8f32 --- /dev/null +++ b/.github/workflows/release-upload-assets.yml @@ -0,0 +1,33 @@ +name: Release - upload assets +on: + push: + tags: + - 'v*' +permissions: + contents: write +jobs: + upload-assets: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Install node + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Install dependencies + run: npm ci + + - name: Build dist + run: npm run build-bundle + + - name: Zip dist + run: zip -r dist.zip dist/ + + - name: Upload dist to GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload ${{ github.ref_name }} dist.zip diff --git a/.github/workflows/ts-code-compilation.yml b/.github/workflows/ts-code-compilation.yml index 8c351fc..8dbe1d4 100644 --- a/.github/workflows/ts-code-compilation.yml +++ b/.github/workflows/ts-code-compilation.yml @@ -14,7 +14,7 @@ jobs: - name: install node uses: actions/setup-node@v4 with: - node-version: 20.x + node-version-file: '.nvmrc' - name: run npm install run: npm install - name: typescript code compilation diff --git a/.github/workflows/ts-code-validation.yml b/.github/workflows/ts-code-validation.yml index b9fa8e8..3afd065 100644 --- a/.github/workflows/ts-code-validation.yml +++ b/.github/workflows/ts-code-validation.yml @@ -15,7 +15,7 @@ jobs: - name: install node uses: actions/setup-node@v4 with: - node-version: 20.x + node-version-file: '.nvmrc' - name: run npm install run: npm install - name: typescript code validation with eslint diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..87deec4 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,28 @@ +name: Unit tests +on: [pull_request] + +permissions: + contents: read +jobs: + unit-tests: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Merge branches + uses: ./.github/actions/merge-branches + - name: install node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - name: run npm install + run: npm install + - name: run unit tests + run: npm run test:unit:coverage + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ diff --git a/.gitignore b/.gitignore index ea80a77..b2df412 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ debian/**/copyright debian/**/changelog.gz debian/**/md5sums debian/**/var/www/* +memory +playwright-report +test-results +.env +coverage/* \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/README.md b/README.md index d1a6a5c..8ead993 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,149 @@ The Pick Random User Plugin is listed in the apps gallery for presenters to pick ![Gif of plugin demo](./public/assets/plugin.gif) +## Plugin Versioning + +Please be aware that we have a separate branch of this plugin for each version of the SDK. This ensures that everything merged into a branch is compatible with the corresponding version of the BigBlueButton core. As of now, here’s the correspondence between the branches, SDK versions, and BigBlueButton core versions: + +| Repository Branch | Plugin-SDK Version | BigBlueButton Core Version | +|------------------|--------------------|----------------------------| +| v0.0.x | v0.0.x | v3.x | +| v0.1.x | v0.1.x | v4.0.x | + +For more information about the plugin API features, see the documentation (`readme` files) within the specific branch you are interested in. We separate the branches because, going forward, `v0.1.x` is becoming more and more different from `v0.0.x`. + +If you have any suggestions, requirements, or questions, don’t hesitate to contact us. + +## Configurations + +Down below, we list all the possible configurations this plugin supports, and then a brief explanation: + +```yaml +- name: BbbPluginPickRandomUser + settings: + pingSoundEnabled: true + pingSoundUrl: resources/sounds/doorbell.mp3 + browserNotificationEnabled: true + pickedUserTimeWindow: 10 # seconds + preventCloseDelaySeconds: 3 # seconds + modalUiScale: 1 +``` + +| Name | Description | Default | +|------------------------|--------------------------------------|-----------------------------| +| `pingSoundEnabled` | Flag that decides whether the ping sound is enabled for picked user | `true` | +| `pingSoundUrl` | URL of the ping sound file | `resources/sounds/doorbell.mp3` | +| `browserNotificationEnabled` | Flag that decides whether to send browser notification when user is picked | `false` | +| `pickedUserTimeWindow` | Time window to consider a user as recently picked (users that join after that time will not see the last modal) | `10` | +| `preventCloseDelaySeconds` | Delay in seconds before the modal can be closed to prevent accidental closures | `3` | +| `modalUiScale` | Multiplier applied to all font sizes and spacing values in the modal UI | `1` | + + +### Size Multiplier + +By default, the modal uses a base set of font sizes and spacing values (`modalUiScale: 1`). You can scale all of them up or down uniformly by setting `modalUiScale` to any positive number. For example, `0.8` makes everything 20% smaller and `1.5` makes everything 50% larger. + +```yaml +public: + # ... + plugins: + - name: BbbPluginPickRandomUser + settings: + modalUiScale: 0.8 # shrink all fonts and spacing by 20% +``` + +The multiplier applies to: font sizes, padding, margins, gaps, and list max-heights throughout the picked-user modal and the presenter's sidekick panel. + +### Prevent Close Delay + +By default, when the modal appears showing the picked user (the view that displays the selected user's name and avatar), it cannot be closed for 3 seconds. This prevents accidental closures from misclicks. During this time: +- The close button (X) is disabled and grayed out +- Clicking outside the modal does nothing +- Pressing the Escape key does nothing +- Visual countdown indicator: + - **For viewers**: A text message below the user's name: "You can close this modal in X seconds" + - **For presenters**: A blue progress bar (similar to YouTube's) that shrinks as time passes + +**Note:** This delay only applies to the picked user modal. On BigBlueButton 4.0 the presenter view (used for selecting users) lives in the plugin's sidekick panel, opened from the apps gallery, and is not affected by this delay. + +After the countdown finishes, all close methods become available. To change this delay, add the following settings in the `/etc/bigbluebutton/bbb-html5.yml` file: + +```yaml +public: + # ... + plugins: + - name: BbbPluginPickRandomUser + settings: + preventCloseDelaySeconds: 5 # Set to 0 to disable the delay +``` + +### Notification + +By default, browser notification when user is randomly picked is not enabled. To enable it, add the following settings in the `/etc/bigbluebutton/bbb-html5.yml` file: + +```yaml +public: + # ... + plugins: + - name: BbbPluginPickRandomUser + settings: + browserNotificationEnabled: false +``` + +### Ping sound + +By default, ping sound is played for the randomly picked user. To remove this feature, one must add the following configurations in their `/etc/bigbluebutton/bbb-html5.yml` file. + +So within that file and in `public.plugins` add the following configurations: + +```yaml +- name: BbbPluginPickRandomUser + settings: + pingSoundEnabled: false +``` + +The result yaml will look like: + +```yaml +public: + # ... + plugins: + - name: BbbPluginPickRandomUser + settings: + pingSoundEnabled: false +``` + +Just a minor comment: This relative URLs can only be configured if the server on which BBB is running is not a cluster setup. If that's your case, you'll need to put the whole URL into the configuration. It's also worth mentioning that the default `pingSoundUrl` will work in cluster setups, so no worries on that. + +Some other possible `pingSoundUrl` are (and notice that this URL can be relative, if the `mp3` sound is being served from within the BBB server - and it's not a cluster setup - as showed right below): +- resources/sounds/alarm.mp3 +- resources/sounds/bbb-handRaise.mp3 +- resources/sounds/LeftCall.mp3 +- resources/sounds/RelaxingMusic.mp3 +- resources/sounds/aristocratDrums.mp3 +- resources/sounds/CalmMusic.mp3 +- resources/sounds/notify.mp3 +- resources/sounds/ScreenshareOff.mp3 +- resources/sounds/audioSample.mp3 +- resources/sounds/doorbell.mp3 +- resources/sounds/Poll.mp3 +- resources/sounds/userJoin.mp3 + +These are the possible `mp3` that already come within a BBB server, if you want a custom sound, just add the URL to the `mp3` file there and it will work out of the box. + ## Building the Plugin -First, double check what branch of the repository you are working with. At time of writing branch `main` produces a version of the plugin that is suitable with BigBlueButton 3.0. Branch `v3.1.x-release` is compatible with BigBlueButton 3.1. +First, double check what branch of the repository you are working with — see [Plugin Versioning](#plugin-versioning) above. This branch (`v0.1.x`) is the one compatible with BigBlueButton 4.0. To build the plugin for production use, follow these steps: ```bash -cd $HOME/src/plugin-pick-random-user-plugin +cd $HOME/src/bbb-plugin-pick-random-user npm ci npm run build-bundle ``` -The above command will generate the `dist` folder, containing the bundled JavaScript file named `PickRandomUserPlugin.js`. This file can be hosted on any HTTPS server along with its `manifest.json`. +The above command will generate the `dist` folder, containing the bundled JavaScript file named `BbbPluginPickRandomUser.js`, a directory of locale files and a license files. These files can be hosted on any HTTPS server along with the `manifest.json` which is also part of the `dist/` directory. If you install the Plugin separated to the manifest, remember to change the `javascriptEntrypointUrl` in the `manifest.json` to the correct endpoint. @@ -28,7 +158,7 @@ To use the plugin in BigBlueButton, send this parameter along in create call: pluginManifests=[{"url":"/path/to/manifest.json"}] ``` -Or additionally, you can add this same configuration in `bbb-web`'s properties in `/etc/bigbluebutton/bbb-web.properties` +Or additionally, you can add this same configuration in `bbb-web`'s properties in `/etc/bigbluebutton/bbb-web.properties`. ## Development mode diff --git a/debian/control b/debian/control index 9b18e8b..0405041 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: extra Maintainer: Anton Georgiev Build-Depends: debhelper (>= 13), nodejs (>= 18) Standards-Version: 4.1.4 -Homepage: https://github.com/bigbluebutton/plugin-pick-random-user +Homepage: https://github.com/bigbluebutton/bbb-plugin-pick-random-user Package: bbb-plugin-pick-random-user Architecture: all diff --git a/locales/en.json b/locales/en.json deleted file mode 100644 index 1fa8d8d..0000000 --- a/locales/en.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "app.pickRandomUser.title": "Pick Random User", - "app.pickRandomUser.options": "Options", - "app.pickRandomUser.includeModerators": "Include moderators", - "app.pickRandomUser.includePresenter": "Include presenter", - "app.pickRandomUser.includePickedUsers": "Include already picked users", - "app.pickRandomUser.availableForSelection": "Available for selection", - "app.pickRandomUser.previouslyPicked": "Previously picked", - "app.pickRandomUser.clearAll": "Clear all", - "app.pickRandomUser.noUsersAvailableWarning": "No {0} available to randomly pick from", - "app.pickRandomUser.pickRandomUserButtonLabelAgain": "Pick Again", - "app.pickRandomUser.pickRandomUserButtonLabelRole": "Pick {0}", - "app.pickRandomUser.viewer": "viewer", - "app.pickRandomUser.viewers": "viewers", - "app.pickRandomUser.user": "user", - "app.pickRandomUser.users": "users", - "app.pickRandomUser.youWerePicked": "You have been randomly picked", - "app.pickRandomUser.pickedUser": "Randomly picked user", - "app.pickRandomUser.modal.closeButton": "Close button" -} diff --git a/locales/pt-BR.json b/locales/pt-BR.json deleted file mode 100644 index 391f6c4..0000000 --- a/locales/pt-BR.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "app.pickRandomUser.title": "Sortear Participante", - "app.pickRandomUser.options": "Opções", - "app.pickRandomUser.includeModerators": "Incluir moderadores", - "app.pickRandomUser.includePresenter": "Incluir apresentador", - "app.pickRandomUser.includePickedUsers": "Incluir usuários já sorteados", - "app.pickRandomUser.availableForSelection": "Participando", - "app.pickRandomUser.previouslyPicked": "Já sorteados", - "app.pickRandomUser.clearAll": "Limpar", - "app.pickRandomUser.noUsersAvailableWarning": "Não há {0} no sorteio", - "app.pickRandomUser.pickRandomUserButtonLabelAgain": "Sortear Novamente", - "app.pickRandomUser.pickRandomUserButtonLabelRole": "Sortear {0}", - "app.pickRandomUser.viewer": "espectador", - "app.pickRandomUser.viewers": "espectadores", - "app.pickRandomUser.user": "participante", - "app.pickRandomUser.users": "participantes", - "app.pickRandomUser.youWerePicked": "Você foi sorteado", - "app.pickRandomUser.pickedUser": "Usuário sorteado", - "app.pickRandomUser.modal.closeButton": "Botão de Fechar" -} diff --git a/manifest.json b/manifest.json index ea9564c..05ee97e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,17 +1,38 @@ { - "requiredSdkVersion": "~0.1.5", - "name": "PickRandomUserPlugin", - "javascriptEntrypointUrl": "PickRandomUserPlugin.js", + "requiredSdkVersion": "~0.1.9", + "name": "BbbPluginPickRandomUser", + "version": "0.1.1", + "javascriptEntrypointUrl": "BbbPluginPickRandomUser.js", + "localesBaseUrl": "locales", "dataChannels": [ - { - "name": "panelInformationFromPresenter", - "pushPermission": ["presenter"], - "replaceOrDeletePermission": ["creator", "moderator"] - }, - { - "name": "pickRandomUser", - "pushPermission": ["presenter"], - "replaceOrDeletePermission": ["creator", "moderator"] - } + { + "name": "filterOptions", + "pushPermission": [ + "presenter" + ], + "replaceOrDeletePermission": [ + "creator", + "moderator" + ] + }, + { + "name": "pickRandomUser", + "pushPermission": [ + "presenter" + ], + "replaceOrDeletePermission": [ + "creator", + "moderator" + ] + }, + { + "name": "pickedUserSeenEntry", + "pushPermission": [ + "all" + ], + "replaceOrDeletePermission": [ + "moderator" + ] + } ] } diff --git a/package-lock.json b/package-lock.json index 0f71da1..0f192c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,20 @@ { - "name": "pick-random-user-plugin", + "name": "bbb-plugin-pick-random-user", "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "pick-random-user-plugin", + "name": "bbb-plugin-pick-random-user", "version": "0.1.1", "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.0", - "@mui/material": "^6.4.2", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/node": "^20.3.1", "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", - "@types/webpack-env": "^1.18.8", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.1.5", + "bigbluebutton-html-plugin-sdk": "0.1.22", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", @@ -31,16 +27,24 @@ "@babel/core": "^7.21.8", "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", + "@playwright/test": "^1.51.1", + "@testing-library/jest-dom": "^6.9.1", "@types/node": "^20.4.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-modal": "^3.16.0", + "@types/sha.js": "^2.4.4", "@types/styled-components": "^5.1.26", + "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", + "@vitejs/plugin-react": "^4.7.0", + "@vitest/coverage-v8": "^3.2.6", + "axios": "^1.16.0", "babel-loader": "^9.1.2", "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.7.4", + "dotenv": "^16.4.7", "eslint": "^8.45.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", @@ -49,23 +53,35 @@ "eslint-plugin-react": "^7.33.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-watch": "^8.0.0", + "jsdom": "^25.0.1", "lint-staged": "11.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "sha.js": "^2.4.11", "style-loader": "^3.3.3", "ts-loader": "^9.4.3", "typescript": "^5.1.6", + "vite": "^6.4.3", + "vitest": "^3.2.6", "watch": "^1.0.2", "webpack": "^5.95.0", "webpack-cli": "^5.1.1", - "webpack-dev-server": "^4.15.1" + "webpack-dev-server": "^4.15.1", + "xml2js": "^0.6.2" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -117,45 +133,66 @@ } } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", - "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.0.tgz", - "integrity": "sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -171,15 +208,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.0.tgz", - "integrity": "sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.0", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -213,13 +250,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -285,6 +322,15 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", @@ -300,27 +346,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -343,9 +389,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -416,27 +462,27 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -458,25 +504,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", - "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -1339,6 +1385,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz", @@ -1675,62 +1753,67 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@browser-bunyan/console-formatted-stream": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@browser-bunyan/console-formatted-stream/-/console-formatted-stream-1.8.0.tgz", @@ -1764,68 +1847,130 @@ "integrity": "sha512-f9oSDik8kAl+4rhVyHqIr012P1boHFUKc7D9nzA5+lDsFoP90UQnDwpseqBdF2mTaWYju10E7h+GdH8u+7MHOQ==", "license": "MIT" }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/@emotion/babel-plugin/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" + "engines": { + "node": ">=18" } }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } }, "node_modules/@emotion/is-prop-valid": { "version": "1.3.1", @@ -1842,78 +1987,6 @@ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", "license": "MIT" }, - "node_modules/@emotion/react": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/styled": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", - "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@emotion/stylis": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", @@ -1926,58 +1999,453 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", "license": "MIT" }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, @@ -2001,11 +2469,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2028,11 +2495,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2064,61 +2530,56 @@ } }, "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz", - "integrity": "sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==", - "license": "MIT", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", "dependencies": { - "@formatjs/fast-memoize": "2.2.6", - "@formatjs/intl-localematcher": "0.5.10", - "decimal.js": "10", - "tslib": "2" + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" } }, "node_modules/@formatjs/fast-memoize": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz", - "integrity": "sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==", - "license": "MIT", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", "dependencies": { - "tslib": "2" + "tslib": "^2.8.0" } }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.0.tgz", - "integrity": "sha512-Hp81uTjjdTk3FLh/dggU5NK7EIsVWc5/ZDWrIldmf2rBuPejuZ13CZ/wpVE2SToyi4EiroPTQ1XJcJuZFIxTtw==", - "license": "MIT", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.2", - "@formatjs/icu-skeleton-parser": "1.8.12", - "tslib": "2" + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.12", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.12.tgz", - "integrity": "sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg==", - "license": "MIT", + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.2", - "tslib": "2" + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" } }, "node_modules/@formatjs/intl": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.1.3.tgz", - "integrity": "sha512-yWtB1L4vOr17MLII3bcNRmjx6qBkSupJuA6nJz1zVxpWbJXKQL5vgvjRCehTO3z7HolxFjtLdfV0/RN+bC34Fg==", - "license": "MIT", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.1.8.tgz", + "integrity": "sha512-LWXgwI5zTMatvR8w8kCNh/priDTOF/ZssokMBHJ7ZWXFoYLVOYo0EJERD9Eajv+xsfQO1QkuAt77KWQ1OI4mOQ==", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.2", - "@formatjs/fast-memoize": "2.2.6", - "@formatjs/icu-messageformat-parser": "2.11.0", - "intl-messageformat": "10.7.14", - "tslib": "2" + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "intl-messageformat": "10.7.18", + "tslib": "^2.8.0" }, "peerDependencies": { - "typescript": "5" + "typescript": "^5.6.0" }, "peerDependenciesMeta": { "typescript": { @@ -2127,12 +2588,11 @@ } }, "node_modules/@formatjs/intl-localematcher": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", - "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", - "license": "MIT", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", "dependencies": { - "tslib": "2" + "tslib": "^2.8.0" } }, "node_modules/@graphql-typed-document-node/core": { @@ -2161,22 +2621,20 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2206,18 +2664,123 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -2229,36 +2792,26 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2272,257 +2825,425 @@ "dev": true, "license": "MIT" }, - "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.2.tgz", - "integrity": "sha512-Qmod9fHsFWrtLxdSkZ4iDLRz2AUKt3C2ZEimuY+qKlQGVKJDNS5DuSlNOAgqfHFDq8mzB17ATN6HFcThwJlvUw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/material": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.2.tgz", - "integrity": "sha512-9jKr53KbAJyyBRx8LRmX7ATXHlGtxVQdPgm1uyXMoEPMVkSJW1yO3vFgfYoDbGx4ZHcCNuWa4FkFIPWVt9fghA==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.4.2", - "@mui/system": "^6.4.2", - "@mui/types": "^7.2.21", - "@mui/utils": "^6.4.2", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^19.0.0", - "react-transition-group": "^4.4.5" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.4.2", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } + "node": ">= 8" } }, - "node_modules/@mui/private-theming": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.2.tgz", - "integrity": "sha512-2CkQT0gNlogM50qGTBJgWA7hPPx4AeH8RE2xJa+PHtIOowiVPX52ZsQ0e7Ho18DAqEbkngQ6Uju037ER+TCY5A==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.4.2", - "prop-types": "^15.8.1" - }, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">= 8" } }, - "node_modules/@mui/styled-engine": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.2.tgz", - "integrity": "sha512-cgjQK2bkllSYoWUBv93ALhCPJ0NhfO3NctsBf13/b4XSeQVfKPBAnR+P9mNpdFMa5a5RWwtWuBD3cZ5vktsN+g==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } + "node": ">= 8" } }, - "node_modules/@mui/system": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.2.tgz", - "integrity": "sha512-wQbaPCtsxNsM5nR+NZpkFJBKVKH03GQnAjlkKENM8JQqGdWcRyM3f4fJZgzzNdIFpSQw4wpAQKnhfHkjf3d6yQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.4.2", - "@mui/styled-engine": "^6.4.2", - "@mui/types": "^7.2.21", - "@mui/utils": "^6.4.2", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.2.21", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz", - "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">=14" } }, - "node_modules/@mui/utils": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.2.tgz", - "integrity": "sha512-5NkhzlJkmR5+5RSs/Irqin1GPy2Z8vbLk/UzQrH9FEAnm6OA9SvuXjzgklxUs7N65VwEkGpKK1jMZ5K84hRdzQ==", - "license": "MIT", + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.21", - "@types/prop-types": "^15.7.14", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" + "playwright": "1.59.1" }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + "bin": { + "playwright": "cli.js" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -2545,25 +3266,51 @@ } }, "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "license": "MIT", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", - "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", + "picocolors": "1.1.1", "pretty-format": "^27.0.2" }, "engines": { "node": ">=18" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/@testing-library/react": { "version": "13.4.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", @@ -2632,6 +3379,51 @@ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -2653,6 +3445,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -2674,10 +3477,37 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -2769,13 +3599,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.3.tgz", - "integrity": "sha512-tSQrmKKatLDGnG92h40GD7FzUt0MjahaHwOME4VAFeeA/Xopayq5qLyQRy7Jg/pjgKIFBXuKcGhJo+UdYG55jQ==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-forge": { @@ -2792,12 +3622,13 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "license": "MIT" }, "node_modules/@types/qs": { @@ -2843,15 +3674,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -2899,6 +3721,15 @@ "@types/send": "*" } }, + "node_modules/@types/sha.js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/sha.js/-/sha.js-2.4.4.tgz", + "integrity": "sha512-Qukd+D6S2Hm0wLVt2Vh+/eWBIoUt+wF8jWjBsG4F8EFQRwKtYvtXCPcNl2OEUQ1R+eTr3xuSaBYUyM3WD1x/Qw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -2921,12 +3752,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/webpack-env": { - "version": "1.18.8", - "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz", - "integrity": "sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==", - "license": "MIT" - }, "node_modules/@types/ws": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", @@ -2937,6 +3762,15 @@ "@types/node": "*" } }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -3181,164 +4015,318 @@ "dev": true, "license": "ISC" }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz", + "integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.6", + "vitest": "3.2.6" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, - "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -3441,15 +4429,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" + "dev": true }, "node_modules/accepts": { "version": "1.3.8", @@ -3466,11 +4452,10 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -3478,14 +4463,16 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, "peerDependencies": { - "acorn": "^8" + "acorn": "^8.14.0" } }, "node_modules/acorn-jsx": { @@ -3498,6 +4485,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -3513,11 +4510,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3548,11 +4544,10 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3571,16 +4566,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3670,7 +4655,6 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -3849,6 +4833,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -3856,6 +4850,23 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3866,6 +4877,12 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3891,6 +4908,18 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3919,21 +4948,6 @@ "webpack": ">=5" } }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -4005,6 +5019,17 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -4013,10 +5038,9 @@ "license": "MIT" }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.1.5.tgz", - "integrity": "sha512-Tzf5bIsO5CabgfRl3DcoNVRYe6RrL25z8rcBl8jc2rXKLewTtzJHmbJvLjIyluUWOqhn8LKvi2b9XNhGgBFU6Q==", - "license": "LGPL-3.0", + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.1.22.tgz", + "integrity": "sha512-0ts3ffGgTwx3t62zYl8fJeBcIiuMbbpDpyRYQ2X3tmo7wa1LVNNZui+CwVbFiHuhl8CAPJgDfveE0frO5B5OBQ==", "dependencies": { "@apollo/client": "^3.8.7", "@browser-bunyan/console-formatted-stream": "^1.8.0", @@ -4041,40 +5065,30 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4085,6 +5099,27 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4092,6 +5127,16 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -4104,11 +5149,10 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -4138,9 +5182,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -4155,12 +5199,12 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -4173,30 +5217,63 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -4209,6 +5286,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4224,9 +5302,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001674", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001674.tgz", - "integrity": "sha512-jOsKlZVRnzfhLojb+Ykb+gyUSp9Xb57So+fAiFlLzzTKpqg8xxSav0e40c8/4F/v9N8QSvrRRaLeVzQbLqomYw==", + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", "funding": [ { "type": "opencollective", @@ -4240,8 +5318,24 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } }, "node_modules/chalk": { "version": "4.1.2", @@ -4259,6 +5353,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4368,15 +5472,6 @@ "node": ">=6" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4402,6 +5497,18 @@ "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", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -4433,18 +5540,17 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, - "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { @@ -4468,12 +5574,14 @@ "dev": true, "license": "MIT" }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, - "license": "MIT" + "engines": { + "node": ">= 0.6" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -4642,6 +5750,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", @@ -4654,12 +5763,21 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4738,6 +5856,13 @@ "postcss-value-parser": "^4.0.2" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4751,6 +5876,27 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -4764,6 +5910,20 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -4819,9 +5979,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4836,11 +5996,21 @@ } }, "node_modules/decimal.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-equal": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", @@ -4937,6 +6107,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4952,7 +6131,6 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -5020,16 +6198,37 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "license": "MIT" }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5038,10 +6237,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.49", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.49.tgz", - "integrity": "sha512-ZXfs1Of8fDb6z7WEYZjXpgIRF6MEu8JdeGA0A40aZq6OQbS+eJpnnV49epZRna2DU/YsEjSQuGtQPPtvt6J65A==", - "license": "ISC" + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -5061,14 +6259,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -5088,6 +6285,19 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/envinfo": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", @@ -5105,6 +6315,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -5172,13 +6383,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -5239,18 +6446,15 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", - "dev": true, - "license": "MIT" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "license": "MIT", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dependencies": { "es-errors": "^1.3.0" }, @@ -5259,15 +6463,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5301,11 +6505,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", "engines": { "node": ">=6" } @@ -5321,6 +6565,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5513,11 +6758,10 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5547,11 +6791,10 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5600,22 +6843,20 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5670,11 +6911,10 @@ } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5694,11 +6934,10 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5783,11 +7022,10 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5810,11 +7048,10 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5889,6 +7126,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5966,47 +7213,61 @@ "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", "license": "BSD-3-Clause" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -6078,10 +7339,20 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { @@ -6196,12 +7467,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6245,16 +7510,15 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, - "license": "ISC" + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -6273,12 +7537,61 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "license": "MIT", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "dependencies": { - "is-callable": "^1.1.3" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/forwarded": { @@ -6376,16 +7689,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "license": "MIT", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -6401,6 +7718,18 @@ "dev": true, "license": "ISC" }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -6471,26 +7800,23 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" + "dev": true }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6502,6 +7828,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6546,12 +7873,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6572,10 +7898,9 @@ "license": "MIT" }, "node_modules/graphql": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", - "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", - "license": "MIT", + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -6637,6 +7962,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6646,10 +7972,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "license": "MIT", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -6752,6 +8077,19 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-entities": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", @@ -6769,6 +8107,12 @@ ], "license": "MIT" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -6815,12 +8159,25 @@ "node": ">=8.0.0" } }, - "node_modules/http-proxy-middleware": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", - "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -6840,6 +8197,20 @@ } } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -6890,6 +8261,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -7055,15 +8427,14 @@ } }, "node_modules/intl-messageformat": { - "version": "10.7.14", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.14.tgz", - "integrity": "sha512-mMGnE4E1otdEutV5vLUdCxRJygHB5ozUBxsPB5qhitewssrS/qGruq9bmvIRkkGsNeK5ZWLfYRld18UHGTIifQ==", - "license": "BSD-3-Clause", + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.2", - "@formatjs/fast-memoize": "2.2.6", - "@formatjs/icu-messageformat-parser": "2.11.0", - "tslib": "2" + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" } }, "node_modules/ipaddr.js": { @@ -7112,6 +8483,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -7187,6 +8559,7 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -7403,6 +8776,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -7500,13 +8880,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, - "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -7592,6 +8971,56 @@ "node": ">=0.10.0" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", @@ -7609,12 +9038,26 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7629,7 +9072,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7647,11 +9089,10 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -7659,6 +9100,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -7682,6 +9164,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -7802,6 +9285,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/lint-staged": { @@ -7885,13 +9369,16 @@ "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loaders.css": { @@ -7917,10 +9404,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -8027,22 +9513,85 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, "dependencies": { - "yallist": "^3.0.2" + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "license": "MIT", + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, "bin": { - "lz-string": "bin/bin.js" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" } }, "node_modules/media-typer": { @@ -8172,6 +9721,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -8205,6 +9764,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8226,9 +9794,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -8269,20 +9837,18 @@ "license": "MIT" }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "license": "MIT" + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -8307,6 +9873,13 @@ "node": ">=8" } }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8317,10 +9890,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "license": "MIT", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "engines": { "node": ">= 0.4" }, @@ -8459,11 +10031,10 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -8626,10 +10197,17 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -8642,6 +10220,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -8656,6 +10235,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8710,24 +10302,64 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, - "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "MIT" + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8735,10 +10367,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "engines": { "node": ">=8.6" }, @@ -8850,6 +10481,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -8870,9 +10545,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -8890,8 +10565,8 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -9090,6 +10765,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9101,13 +10785,13 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -9158,25 +10842,46 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -9235,23 +10940,22 @@ } }, "node_modules/react-intl": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-7.1.5.tgz", - "integrity": "sha512-cVvsVdaOnZ85XBXU0Lc2PVGNhGlzl4UBV+aWAGe/zrV5Xr+CEW7izUsAp/fIuwvCsJl9R+aokppm+P7cdhnpUA==", - "license": "BSD-3-Clause", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.2", - "@formatjs/icu-messageformat-parser": "2.11.0", - "@formatjs/intl": "3.1.3", - "@types/hoist-non-react-statics": "3", + "version": "7.1.14", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-7.1.14.tgz", + "integrity": "sha512-VE/0Wi/lHJlBC7APQpCzLUdIt3GB5B0GZrRW8Q+ACbkHI4j+Wwgg9J1TniN6zmLHmPH5gxXcMy+fkSPfw5p1WQ==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-messageformat-parser": "2.11.4", + "@formatjs/intl": "3.1.8", + "@types/hoist-non-react-statics": "^3.3.1", "@types/react": "16 || 17 || 18 || 19", - "hoist-non-react-statics": "3", - "intl-messageformat": "10.7.14", - "tslib": "2" + "hoist-non-react-statics": "^3.3.2", + "intl-messageformat": "10.7.18", + "tslib": "^2.8.0" }, "peerDependencies": { - "react": "^16.6.0 || 17 || 18 || 19", - "typescript": "5" + "react": "16 || 17 || 18 || 19", + "typescript": "^5.6.0" }, "peerDependenciesMeta": { "typescript": { @@ -9260,10 +10964,10 @@ } }, "node_modules/react-is": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", - "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", - "license": "MIT" + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "peer": true }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", @@ -9290,6 +10994,16 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" } }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-spinkit": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/react-spinkit/-/react-spinkit-3.0.0.tgz", @@ -9302,22 +11016,6 @@ "prop-types": "^15.5.8" } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -9359,6 +11057,20 @@ "node": ">= 10.13.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -9401,12 +11113,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -9512,6 +11218,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -9552,6 +11259,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -9625,6 +11333,58 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9724,6 +11484,28 @@ "dev": true, "license": "MIT" }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -9734,11 +11516,10 @@ } }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9746,7 +11527,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -9754,11 +11535,10 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10030,6 +11810,26 @@ "dev": true, "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -10072,35 +11872,85 @@ "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", - "dev": true, - "license": "MIT", + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shifty": { + "version": "2.20.4", + "resolved": "https://registry.npmjs.org/shifty/-/shifty-2.20.4.tgz", + "integrity": "sha512-4Y0qRkg8ME5XN8yGNAwmFOmsIURGFKT9UQfNL6DDJQErYtN5HsjyoBuJn41ZQfTkuu2rIbRMn9qazjKsDpO2TA==", + "license": "MIT", + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shifty": { - "version": "2.20.4", - "resolved": "https://registry.npmjs.org/shifty/-/shifty-2.20.4.tgz", - "integrity": "sha512-4Y0qRkg8ME5XN8yGNAwmFOmsIURGFKT9UQfNL6DDJQErYtN5HsjyoBuJn41ZQfTkuu2rIbRMn9qazjKsDpO2TA==", - "license": "MIT", - "optionalDependencies": { - "fsevents": "^2.3.2" + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "license": "MIT", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -10109,6 +11959,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -10178,7 +12035,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10189,7 +12045,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -10226,6 +12081,13 @@ "wbuf": "^1.7.3" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -10236,6 +12098,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -10283,6 +12152,27 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -10423,6 +12313,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10443,6 +12346,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10456,6 +12372,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-loader": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", @@ -10524,12 +12460,6 @@ "node": ">=4" } }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10546,6 +12476,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10563,25 +12494,34 @@ "node": ">=0.10" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -10593,17 +12533,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -10627,31 +12565,97 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", "dev": true, - "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "MIT" + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/text-table": { "version": "0.2.0", @@ -10680,6 +12684,132 @@ "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "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": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10693,14 +12823,40 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, "engines": { - "node": ">=0.6" + "node": ">=18" } }, "node_modules/ts-api-utils": { @@ -10835,15 +12991,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -10942,9 +13097,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -11016,9 +13171,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -11033,10 +13188,9 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -11107,6 +13261,240 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "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": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "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": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -11134,11 +13522,10 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, - "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -11157,36 +13544,47 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/webpack": { - "version": "5.95.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", - "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.106.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.0.tgz", + "integrity": "sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -11381,11 +13779,10 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -11414,25 +13811,6 @@ "node": ">=4.0" } }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -11458,6 +13836,57 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11536,15 +13965,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "license": "MIT", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -11554,6 +13984,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -11589,6 +14036,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -11618,21 +14083,51 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 2269c88..0bd3dcc 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,16 @@ { - "name": "pick-random-user-plugin", + "name": "bbb-plugin-pick-random-user", "version": "0.1.1", "private": true, "main": "./src/index.tsx", "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.0", - "@mui/material": "^6.4.2", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/node": "^20.3.1", "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", - "@types/webpack-env": "^1.18.8", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.1.5", + "bigbluebutton-html-plugin-sdk": "0.1.22", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", @@ -29,7 +25,14 @@ "build:watch": "rm -rf dist && tsc -w --module CommonJS", "lint": "eslint ./src/*", "lint:fix": "npm run lint -- --fix", - "lint:watch": "watch 'yarn lint'" + "lint:watch": "watch 'yarn lint'", + "test:unit": "vitest run", + "test:unit:coverage": "vitest run --coverage", + "test:unit:watch": "vitest", + "publish-plugin:dev": "bash scripts/copy-plugin-to-container.sh", + "test": "npx playwright test", + "test:isolated": "TEST_MEETINGS=isolated npx playwright test", + "test-chromium-ci": "export CI='true' && npx playwright test --project=chromium" }, "browserslist": { "production": [ @@ -47,16 +50,24 @@ "@babel/core": "^7.21.8", "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", + "@playwright/test": "^1.51.1", + "@testing-library/jest-dom": "^6.9.1", "@types/node": "^20.4.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-modal": "^3.16.0", + "@types/sha.js": "^2.4.4", "@types/styled-components": "^5.1.26", + "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", + "@vitejs/plugin-react": "^4.7.0", + "@vitest/coverage-v8": "^3.2.6", + "axios": "^1.16.0", "babel-loader": "^9.1.2", "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.7.4", + "dotenv": "^16.4.7", "eslint": "^8.45.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", @@ -65,15 +76,20 @@ "eslint-plugin-react": "^7.33.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-watch": "^8.0.0", + "jsdom": "^25.0.1", "lint-staged": "11.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "sha.js": "^2.4.11", "style-loader": "^3.3.3", "ts-loader": "^9.4.3", "typescript": "^5.1.6", + "vite": "^6.4.3", + "vitest": "^3.2.6", "watch": "^1.0.2", "webpack": "^5.95.0", "webpack-cli": "^5.1.1", - "webpack-dev-server": "^4.15.1" + "webpack-dev-server": "^4.15.1", + "xml2js": "^0.6.2" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..4b11323 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,55 @@ +import { defineConfig, devices } from '@playwright/test'; +import { CI, ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from './tests/core/constants'; +import { server } from './tests/core/parameters'; + +export default defineConfig({ + testDir: process.cwd(), + // Vitest unit tests live under tests/unit and must not be collected by Playwright. + testIgnore: ['**/tests/unit/**'], + workers: CI ? 1 : undefined, + retries: CI ? 1 : 0, + fullyParallel: true, + forbidOnly: CI, + reporter: CI + ? [['list'], ['blob']] + : [['list'], ['html', { open: 'never' }]], + use: { + baseURL: server, + headless: true, + trace: 'on', + screenshot: 'on', + video: CI ? 'retain-on-failure' : 'on', + actionTimeout: ELEMENT_WAIT_LONGER_TIME, + viewport: { width: 1280, height: 720 }, + launchOptions: { + slowMo: 0, + }, + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + launchOptions: { + args: [ + '--no-sandbox', + '--ignore-certificate-errors', + '--use-fake-ui-for-media-stream', + '--use-fake-device-for-media-stream', + '--allow-file-access-from-files', + ], + }, + }, + }, + ], + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.05, + }, + toMatchSnapshot: { + maxDiffPixelRatio: 0.05, + }, + timeout: ELEMENT_WAIT_TIME, + }, +}); diff --git a/public/index.html b/public/index.html deleted file mode 100644 index bdf3545..0000000 --- a/public/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - Pick random user - - -
- - diff --git a/public/locales/de.json b/public/locales/de.json new file mode 100644 index 0000000..a9fc4e9 --- /dev/null +++ b/public/locales/de.json @@ -0,0 +1,37 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Ergebnis", + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Sie wurden zufällig ausgewählt", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Zufällig gewählter Teilnehmer", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "zurück", + "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Avatar von Teilnehmer {0}", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Optionen", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Moderatoren einbeziehen", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Präsentator einbeziehen", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Wähle bereits gewählte Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Auch einbeziehen:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderatoren", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Präsentator", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Bereits gewählt", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Zur Auswahl stehen", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Zuvor gewählt", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Alle löschen", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Keine {0} zum Auswählen verfügbar", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Wähle zufälligen Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Nächsten zufälligen Teilnehmer wählen", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Anderen zufälligen Teilnehmer wählen", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Wähle zufälligen Teilnehmer", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Zeige zuletzt gewählten Teilnehmer", + "pickRandomUserPlugin.modal.title": "Zufälligen Teilnehmer wählen", + "pickRandomUserPlugin.sidekickArea.title": "Zufälligen Teilnehmer wählen", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Schließen", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Keine {0} zur Auswahl verfügbar", + "pickRandomUserPlugin.modal.presenterView.availableSection.loading": "Wird geladen…", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Noch kein Teilnehmer ausgewählt", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "Moderator", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "Präsentator", + "pickRandomUserPlugin.modal.closeDelayMessageMs": "Sie können dieses Fenster in {ms}ms schließen" +} diff --git a/public/locales/en.json b/public/locales/en.json new file mode 100644 index 0000000..d11cbd8 --- /dev/null +++ b/public/locales/en.json @@ -0,0 +1,39 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Result", + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "You have been randomly picked", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Randomly picked user", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "back", + "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Avatar image of user {0}", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Include moderators", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Include presenter", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked users", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Also include:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderators", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Presenter", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Already picked", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Available for selection", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "user", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "users", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "viewer", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "viewers", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Previously picked", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Clear All", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "No {0} available to randomly pick from", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Pick random user", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Pick next random user", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Pick another random user", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Pick random user", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Display last randomly picked user", + "pickRandomUserPlugin.modal.title": "Pick random user", + "pickRandomUserPlugin.sidekickArea.title": "Pick random user", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Close", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "No {0} available for selection", + "pickRandomUserPlugin.modal.presenterView.availableSection.loading": "Loading…", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "No user selected yet", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "moderator", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "presenter", + "pickRandomUserPlugin.modal.closeDelayMessage": "You can close this modal in {seconds} seconds", + "pickRandomUserPlugin.modal.closeDelayMessageSingular": "You can close this modal in {seconds} second", + "pickRandomUserPlugin.modal.closeDelayMessageMs": "You can close this modal in {ms}ms" +} diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json new file mode 100644 index 0000000..ba6ddd1 --- /dev/null +++ b/public/locales/fr-FR.json @@ -0,0 +1,36 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Résultat", + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Vous avez été sélectionné au hasard", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Participant sélectionné au hasard", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "retour", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Inclure les modérateurs", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Inclure le présentateur", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure des participants déjà sélectionnés", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Inclure aussi :", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Modérateurs", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Présentateur", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Déjà sélectionnés", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponible pour la sélection", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "participant", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "participants", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "spectateur", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "spectateurs", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Sélectionné précédemment", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Vider la sélection", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Aucun {0} disponible pour être sélectionné au hasard", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Sélectionner un participant au hasard", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Sélectionner le prochain participant au hasard", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Sélectionner un autre participant au hasard", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Sélectionner un participant au hasard", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Afficher le dernier participant sélectionné au hasard", + "pickRandomUserPlugin.modal.title": "Sélectionner un participant au hasard", + "pickRandomUserPlugin.sidekickArea.title": "Sélectionner un participant au hasard", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Fermer", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Aucun {0} disponible pour la sélection", + "pickRandomUserPlugin.modal.presenterView.availableSection.loading": "Chargement…", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Aucun participant sélectionné pour l'instant", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "modérateur", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "présentateur", + "pickRandomUserPlugin.modal.closeDelayMessageMs": "Vous pouvez fermer cette fenêtre dans {ms}ms" +} diff --git a/public/locales/index.json b/public/locales/index.json new file mode 100644 index 0000000..6c72eae --- /dev/null +++ b/public/locales/index.json @@ -0,0 +1,9 @@ +[ + "de.json", + "en.json", + "fr-FR.json", + "index.json", + "it.json", + "ja.json", + "pt-BR.json" +] diff --git a/public/locales/it.json b/public/locales/it.json new file mode 100644 index 0000000..882a4b2 --- /dev/null +++ b/public/locales/it.json @@ -0,0 +1,36 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Risultato", + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Sei stato scelto a caso", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Utente scelto a caso", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "indietro", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opzioni", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Includi moderatori", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Includi il presentatore", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi gli utenti già selezionati", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Includi anche:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderatori", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Presentatore", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Già selezionati", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponibile per la selezione", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "utente", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "utenti", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "partecipante", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "partecipanti", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Precedentemente selezionato", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Cancella tutto", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Nessun {0} disponibile da cui scegliere casualmente", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Scegli un utente casuale", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Scegli il prossimo utente casuale", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Scegli un altro utente casuale", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Scegli un utente casuale", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Visualizza l'ultimo utente scelto casualmente", + "pickRandomUserPlugin.modal.title": "Scegli un utente casuale", + "pickRandomUserPlugin.sidekickArea.title": "Scegli un utente casuale", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Chiudi", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Nessun {0} disponibile per la selezione", + "pickRandomUserPlugin.modal.presenterView.availableSection.loading": "Caricamento…", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Nessun utente selezionato ancora", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "moderatore", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "presentatore", + "pickRandomUserPlugin.modal.closeDelayMessageMs": "Puoi chiudere questa finestra in {ms}ms" +} diff --git a/public/locales/ja.json b/public/locales/ja.json new file mode 100644 index 0000000..02bd4da --- /dev/null +++ b/public/locales/ja.json @@ -0,0 +1,37 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "結果", + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "あなたが指名されました", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "指名された人", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "戻る", + "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "ユーザー {0} のアバター画像", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "設定", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "モデレーターを含める", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "発表者を含める", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "既に指名した人を含める", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "含める:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "モデレーター", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "発表者", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "指名済みの人", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "指名可能な人", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "人のユーザー", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "人のユーザー", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "人の視聴者", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "人の視聴者", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "既に指名された人", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "全消去", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "{0}の中で指名できる人はいません", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "ランダムに指名する", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "次のランダムユーザーを指名する", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "別のランダムユーザーを指名する", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "ランダムに指名する", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "最後に指名されたユーザーを表示", + "pickRandomUserPlugin.modal.title": "ランダムにユーザーを選択", + "pickRandomUserPlugin.sidekickArea.title": "ランダムにユーザーを選択", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "閉じる", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "選択可能な{0}はいません", + "pickRandomUserPlugin.modal.presenterView.availableSection.loading": "読み込み中…", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "まだユーザーが選択されていません", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "モデレーター", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "発表者", + "pickRandomUserPlugin.modal.closeDelayMessageMs": "このモーダルは{ms}ms後に閉じることができます" +} diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json new file mode 100644 index 0000000..2dab514 --- /dev/null +++ b/public/locales/pt-BR.json @@ -0,0 +1,37 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Resultado", + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Você foi selecionado aleatoriamente", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Usuário selecionado aleatoriamente", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "voltar", + "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Iamgem avatar do usuário {0}", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opções", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Incluir moderadores", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Incluir apresentador", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuários já selecionados", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Incluir também:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderadores", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Apresentador", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Já selecionados", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponíveis para seleção", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "usuário", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "usuários", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "espectador", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "espectadores", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Selecionados anteriormente", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Limpar tudo", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Não há disponibilidade de {0} para seleção", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Selecionar usuário aleatório", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Selecionar próximo usuário aleatório", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Selecionar outro usuário aleatório", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Selecionar usuário aleatório", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Exibir último usuário selecionado aleatoriamente", + "pickRandomUserPlugin.modal.title": "Selecionar usuário aleatório", + "pickRandomUserPlugin.sidekickArea.title": "Selecionar usuário aleatório", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Fechar", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Nenhum {0} disponível para seleção", + "pickRandomUserPlugin.modal.presenterView.availableSection.loading": "Carregando…", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Nenhum usuário selecionado ainda", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "moderador", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "apresentador", + "pickRandomUserPlugin.modal.closeDelayMessageMs": "Você pode fechar este modal em {ms}ms" +} diff --git a/scripts/.env.template b/scripts/.env.template new file mode 100644 index 0000000..6054d0f --- /dev/null +++ b/scripts/.env.template @@ -0,0 +1,2 @@ +DESTINATION_SERVER="your.bbb.server.com" +SSH_USERNAME="bigbluebutton" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..7e55d95 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,39 @@ +# Scripts + +## deploy-remote.sh + +Builds the plugin locally and deploys it to a remote BBB server over SSH. + +```bash +./scripts/deploy-remote.sh +``` + +It will: +1. Delete `dist/` and `node_modules/`, then do a clean `npm ci` + `npm run build-bundle` +2. Clear the plugin directory on the remote server +3. Copy the build artifacts via `scp` + +**Configuration** — copy `.env.template` to `.env` in this folder and fill in your values: + +| Variable | Description | +|---|---| +| `DESTINATION_SERVER` | Hostname or IP of the target BBB server | +| `SSH_USERNAME` | SSH user on that server | + +`scripts/.env` is gitignored; `.env.template` is the committed reference. + +--- + +## copy-plugin-to-container.sh + +Copies an already-built `dist/` into a locally running BBB Docker container. + +```bash +./scripts/copy-plugin-to-container.sh [containerName] +``` + +`containerName` defaults to `$LOCAL_CONTAINER_NAME` from the root `.env` when omitted. + +Run `npm run build-bundle` first — this script only copies, it does not build. + +It's mostly used for testing the plugin in local containers. diff --git a/scripts/copy-plugin-to-container.sh b/scripts/copy-plugin-to-container.sh new file mode 100755 index 0000000..93462ea --- /dev/null +++ b/scripts/copy-plugin-to-container.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Copies the built plugin (dist/) into the running BBB Docker container. +# Usage: ./scripts/copy-plugin-to-container.sh [containerName] +# +# containerName defaults to $LOCAL_CONTAINER_NAME from .env when omitted. + +set -e + +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +PLUGIN_ROOT="$SCRIPT_DIR/.." + +# Load .env from the plugin root +ENV_FILE="$PLUGIN_ROOT/.env" +if [ -f "$ENV_FILE" ]; then + set -o allexport + source "$ENV_FILE" + set +o allexport +fi + +PLUGIN_NAME="pick-random-user-plugin" +CONTAINER_NAME=${1:-$LOCAL_CONTAINER_NAME} + +if [ -z "$CONTAINER_NAME" ]; then + echo "Container name is not provided. Pass it as an argument or set LOCAL_CONTAINER_NAME in .env." + exit 1 +fi + +PLUGINS_FOLDER_CONTAINER_PATH="/var/www/bigbluebutton-default/assets/plugins" +PLUGIN_CONTAINER_PATH="$PLUGINS_FOLDER_CONTAINER_PATH/$PLUGIN_NAME" + +# Verify the container is running +if ! docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then + echo "Container '$CONTAINER_NAME' is not running. Exiting." + exit 1 +fi + +# Verify the dist/ folder exists locally +if [ ! -d "$PLUGIN_ROOT/dist" ]; then + echo "dist/ folder not found. Run 'npm run build-bundle' first." + exit 1 +fi + +# Remove stale files in the container and recreate the target directory +if docker exec "$CONTAINER_NAME" [ -d "$PLUGIN_CONTAINER_PATH" ]; then + docker exec "$CONTAINER_NAME" rm -rf "$PLUGIN_CONTAINER_PATH" +fi + +echo "Creating container path: $PLUGIN_CONTAINER_PATH ..." +docker exec "$CONTAINER_NAME" mkdir -p "$PLUGIN_CONTAINER_PATH" + +echo "Copying dist/ to $CONTAINER_NAME:$PLUGIN_CONTAINER_PATH/dist ..." +docker cp "$PLUGIN_ROOT/dist/." "$CONTAINER_NAME:$PLUGIN_CONTAINER_PATH/dist" + +echo "Done. Plugin deployed to $CONTAINER_NAME at $PLUGIN_CONTAINER_PATH/dist" diff --git a/scripts/deploy-remote.sh b/scripts/deploy-remote.sh new file mode 100755 index 0000000..d399653 --- /dev/null +++ b/scripts/deploy-remote.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# git clone ... +# guide to pull, build and deploy a plugin (to be run on local machine, not on a server) +# check the right branch etc + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [ -f "${SCRIPT_DIR}/.env" ]; then + source "${SCRIPT_DIR}/.env" +fi + +PLUGIN_NAME="$(node -pe "require('./package.json').name")" + +rm -r dist/ +rm -r node_modules/ +npm ci +npm run build-bundle + +# Remove existing plugin on remote +ssh "${SSH_USERNAME}@${DESTINATION_SERVER}" "rm -rf /var/www/bigbluebutton-default/assets/plugins/${PLUGIN_NAME}" + +# Recreate plugin directory +ssh "${SSH_USERNAME}@${DESTINATION_SERVER}" "mkdir -p /var/www/bigbluebutton-default/assets/plugins/${PLUGIN_NAME}" + +# Copy new build +scp -r dist/* "${SSH_USERNAME}@${DESTINATION_SERVER}:/var/www/bigbluebutton-default/assets/plugins/${PLUGIN_NAME}" + +echo "Make sure to add the plugin to /etc/bigbluebutton/bbb-web.properties (or api mate)" +echo "pluginManifests=[{\"url\": \"https://$DESTINATION_SERVER/plugins/$PLUGIN_NAME/manifest.json\"}]" diff --git a/src/commons/constants.ts b/src/commons/constants.ts new file mode 100644 index 0000000..9696b6f --- /dev/null +++ b/src/commons/constants.ts @@ -0,0 +1,11 @@ +export const PICKED_USER_TIME_WINDOW = 10; // seconds + +export const TIMEOUT_CLOSE_NOTIFICATION = 5000; + +export const DEFAULT_PING_SOUND_URL = 'resources/sounds/doorbell.mp3'; + +export const DEFAULT_PREVENT_CLOSE_DELAY_SECONDS = 1; // seconds + +export const MIN_PREVENT_CLOSE_DELAY_FOR_TOAST_SECONDS = 2; // seconds + +export const DEFAULT_MODAL_UI_SCALE = 1; diff --git a/src/commons/hooks.ts b/src/commons/hooks.ts new file mode 100644 index 0000000..6465474 --- /dev/null +++ b/src/commons/hooks.ts @@ -0,0 +1,45 @@ +import { PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import { useEffect, useMemo, useRef } from 'react'; +import { createIntl, createIntlCache } from 'react-intl'; + +const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') + ? { + headers: { + 'ngrok-skip-browser-warning': 'any', + }, + } : undefined; + +export const useGetInternationalization = (pluginApi: PluginApi) => { + const { + messages: localeMessages, + currentLocale, + loading: localeMessagesLoading, + } = pluginApi.useLocaleMessages!(LOCALE_REQUEST_OBJECT); + + // Memoized on purpose: `intl` is a dependency of the effects that register this + // plugin's extensible-area items with the client. Building a new intl object on every + // render makes those effects re-run on every render, and re-registering an item makes + // the client re-render, which renders this component again — an infinite loop that + // surfaces as "Maximum update depth exceeded" in the client's layout engine. + const intl = useMemo(() => { + if (localeMessagesLoading || !localeMessages) return null; + return createIntl({ + locale: currentLocale, + messages: localeMessages, + fallbackOnEmptyString: true, + }, createIntlCache()); + }, [currentLocale, localeMessages, localeMessagesLoading]); + + return { + intl, + localeMessagesLoading, + }; +}; + +export const usePreviousValue = (value: T) => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; diff --git a/src/commons/types.ts b/src/commons/types.ts new file mode 100644 index 0000000..8133e8f --- /dev/null +++ b/src/commons/types.ts @@ -0,0 +1,8 @@ +export interface PickRandomUserSettings { + pingSoundEnabled: boolean; + pingSoundUrl: string; + browserNotificationEnabled: boolean; + pickedUserTimeWindow: number; + preventCloseDelaySeconds: number; + modalUiScale: number; +} diff --git a/src/commons/utils.ts b/src/commons/utils.ts new file mode 100644 index 0000000..24ca78a --- /dev/null +++ b/src/commons/utils.ts @@ -0,0 +1,23 @@ +import { DataChannelEntryResponseType, GraphqlResponseWrapper } from 'bigbluebutton-html-plugin-sdk'; +import { PickedUserSeenEntryDataChannel } from '../components/pick-random-user/types'; + +/** + * It process data-channel information on the picked-users already seen + * and returns a boolean indicating if the current user has seen the picked-user + * @param pickedUserSeenEntries DataChannelEntry result + * @param currentUserId userId from current user on which the plugin is running + * @param pickedUserId userId from the picked user + * @returns boolean indicating if the current user has seen the picked-user + */ +export const hasCurrentUserSeenPickedUser = ( + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentUserId: string, + pickedUserId?: string, +) => !!(pickedUserSeenEntries?.data + && pickedUserSeenEntries?.data.length > 0 + && pickedUserSeenEntries.data.some((view) => view.payloadJson + && view.payloadJson.seenByUserId === currentUserId + && view.payloadJson.pickedUserId === pickedUserId)); + +export const isNumber = (obj: unknown): boolean => typeof obj === 'number' && !Number.isNaN(obj); diff --git a/src/components/extensible-areas/generic-content-sidekick-area/component.tsx b/src/components/extensible-areas/generic-content-sidekick-area/component.tsx new file mode 100644 index 0000000..78beb7f --- /dev/null +++ b/src/components/extensible-areas/generic-content-sidekick-area/component.tsx @@ -0,0 +1,99 @@ +import * as React from 'react'; +import { useEffect, useRef } from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { defineMessages } from 'react-intl'; + +import { GenericContentSidekickArea } from 'bigbluebutton-html-plugin-sdk'; +import { GenericContentSidekickAreaManagerProps } from './types'; +import { PickRandomUserPanel } from '../../pick-random-user-panel/component'; + +const NAVIGATION_SIDEBAR_BUTTON_ICON = 'random'; +// The client derives its own data-test attributes from this (sidekick_menu_item_, +// sidekick_header_), which is how the e2e suite finds the apps gallery entry without +// having to match on the translated name. +const SIDEKICK_AREA_DATA_TEST = 'pickRandomUser'; + +const intlMessages = defineMessages({ + sidekickAreaTitle: { + id: 'pickRandomUserPlugin.sidekickArea.title', + description: 'Name of the plugin entry in the apps gallery / sidekick area', + defaultMessage: 'Pick random user', + }, +}); + +/** + * Registers the plugin's sidekick area. This is the plugin's only entry point on + * BigBlueButton 4.0: the actions-button dropdown the v0.0.x branch relies on does not + * exist in the 0.1.x SDK, so the presenter view lives in this panel instead of inside + * the modal. The modal is left with the picked-user view only. + * + * As on v0.0.x, the presenter view is presenter-only, so the entry is registered while + * the current user is the presenter and removed again as soon as they are not. + */ +function GenericContentSidekickAreaManager( + props: GenericContentSidekickAreaManagerProps, +): React.ReactNode { + const { + pluginApi, + intl, + currentUser, + pickRandomUserSettings, + } = props; + + const genericContentId = useRef(''); + // The client may call contentFunction more than once for the same container (for + // instance when the item is re-registered). Calling createRoot twice on one element + // detaches the previously rendered tree and leaves the panel blank, so the root is + // kept and reused for as long as the container is the same node. + const panelRoot = useRef<{ element: HTMLElement; root: ReactDOM.Root } | null>(null); + + const sidekickAreaName = intl.formatMessage(intlMessages.sidekickAreaTitle); + const { modalUiScale } = pickRandomUserSettings; + const isPresenter = !!currentUser?.presenter; + + useEffect(() => { + if (isPresenter) { + const generatedIds = pluginApi.setGenericContentItems([ + new GenericContentSidekickArea({ + contentFunction: (element: HTMLElement) => { + if (!panelRoot.current || panelRoot.current.element !== element) { + panelRoot.current = { element, root: ReactDOM.createRoot(element) }; + } + panelRoot.current.root.render( + , + ); + return panelRoot.current.root; + }, + name: sidekickAreaName, + section: '', + open: false, + buttonIcon: NAVIGATION_SIDEBAR_BUTTON_ICON, + dataTest: SIDEKICK_AREA_DATA_TEST, + ...(genericContentId.current && { id: genericContentId.current }), + }), + ]); + genericContentId.current = generatedIds.pop(); + return; + } + if (genericContentId.current) { + pluginApi.setGenericContentItems([]); + genericContentId.current = ''; + } + // Every dependency here has to be a stable value. Registering an item makes the + // client re-render, which renders this component again: if a dependency had a fresh + // identity on each render (an intl object rebuilt by createIntl, the wrapper object + // returned by useCurrentUser, the settings object literal) the effect would re-run + // forever and the client's layout engine would report "Maximum update depth + // exceeded". `intl` is memoized in commons/hooks; the rest are primitives. + }, [pluginApi, intl, sidekickAreaName, modalUiScale, isPresenter]); + + return null; +} + +export default GenericContentSidekickAreaManager; diff --git a/src/components/extensible-areas/generic-content-sidekick-area/types.ts b/src/components/extensible-areas/generic-content-sidekick-area/types.ts new file mode 100644 index 0000000..15bc3a1 --- /dev/null +++ b/src/components/extensible-areas/generic-content-sidekick-area/types.ts @@ -0,0 +1,10 @@ +import { CurrentUserData, PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import { IntlShape } from 'react-intl'; +import { PickRandomUserSettings } from '../../../commons/types'; + +export interface GenericContentSidekickAreaManagerProps { + pluginApi: PluginApi; + intl: IntlShape; + currentUser: CurrentUserData; + pickRandomUserSettings: PickRandomUserSettings; +} diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 13ee0d0..460d400 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -1,91 +1,254 @@ +import { + useCallback, useEffect, useRef, useState, +} from 'react'; import * as React from 'react'; -import { useEffect, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import * as Styled from './styles'; import { PickUserModalProps } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; -import Styled from './styles'; -import './raw_styles.css'; -import { intlMessages } from '../../intlMessages'; +import { useHandleCurrentUserNotification, usePreventCloseModalCountdown } from './hooks'; +import { MIN_PREVENT_CLOSE_DELAY_FOR_TOAST_SECONDS } from '../../commons/constants'; +// Time a bot user is given to look at the picked user before the modal closes itself. +const BOT_AUTO_CLOSE_DURATION_MS = 5000; +const BOT_AUTO_CLOSE_TICK_MS = 30; + +const intlMessages = defineMessages({ + currentUserPicked: { + id: 'pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked', + description: 'Title to show that current user has been picked', + defaultMessage: 'You have been randomly picked', + }, + modalTitle: { + id: 'pickRandomUserPlugin.modal.title', + description: 'Title of the pick random user modal', + defaultMessage: 'Pick random user', + }, + closeButtonAriaLabel: { + id: 'pickRandomUserPlugin.modal.closeButton.ariaLabel', + description: 'Aria label for the modal close button', + defaultMessage: 'Close', + }, + modalCloseDelayMessage: { + id: 'pickRandomUserPlugin.modal.closeDelayMessage', + description: 'Message showing countdown before modal can be closed', + defaultMessage: 'You can close this modal in {seconds} seconds', + }, + modalCloseDelayMessageSingular: { + id: 'pickRandomUserPlugin.modal.closeDelayMessageSingular', + description: 'Message showing countdown before modal can be closed (singular)', + defaultMessage: 'You can close this modal in {seconds} second', + }, + modalCloseDelayMessageMs: { + id: 'pickRandomUserPlugin.modal.closeDelayMessageMs', + description: 'Message showing millisecond countdown before modal can be closed', + defaultMessage: 'You can close this modal in {ms}ms', + }, +}); + +function OverlayWithToast({ + overlayProps, + contentEl, + toast, +}: { + overlayProps: React.ComponentPropsWithRef<'div'>; + contentEl: React.ReactElement; + toast: React.ReactNode; +}) { + return ( +
+ + {contentEl} + {toast} + +
+ ); +} + +/** + * On BigBlueButton 4.0 the presenter view lives in the plugin's sidekick area (see + * `extensible-areas/generic-content-sidekick-area/component`), because the actions-button + * dropdown that opens the modal on the v0.0.x branch does not exist in the 0.1.x SDK. The + * modal is therefore always the picked-user view, for presenters and viewers alike. + */ export function PickUserModal(props: PickUserModalProps) { const { + pickRandomUserSettings, intl, showModal, handleCloseModal, - updatePickedRandomUser, - pickedUserWithEntryId, + currentPickedUser, currentUser, + pickedUserSeenEntries, + pushPickedUserSeen, isBot, + uuid, } = props; - const [progress, setProgress] = useState(0); + const modalAnchor = useRef(document.getElementById(uuid)); + + const [botProgress, setBotProgress] = useState(0); - const isYou = (pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId); + useHandleCurrentUserNotification( + currentUser, + pickedUserSeenEntries, + currentPickedUser, + pickRandomUserSettings, + intl.formatMessage(intlMessages.currentUserPicked), + ); + + const { preventCloseDelaySeconds } = pickRandomUserSettings; + + const { remainingSeconds, canClose } = usePreventCloseModalCountdown( + currentUser, + pickedUserSeenEntries, + currentPickedUser, + pickRandomUserSettings, + ); + // Bots cannot dismiss the modal themselves, so it closes on its own after a fixed + // window while a progress bar shows how much of that window is left. useEffect(() => { - if (isBot && showModal) { - setProgress(0); - const startTime = Date.now(); - const duration = 5000; // Modal will be displayed for 5 seconds - - const interval = setInterval(() => { - const elapsed = Date.now() - startTime; - const newProgress = (elapsed / duration) * 100; - - if (newProgress >= 100) { - setProgress(100); - clearInterval(interval); - handleCloseModal(); - } else { - setProgress(newProgress); - } - }, 30); - - return () => clearInterval(interval); + if (!isBot || !showModal) return undefined; + + setBotProgress(0); + const startTime = Date.now(); + + const interval = setInterval(() => { + const elapsed = Date.now() - startTime; + const newProgress = (elapsed / BOT_AUTO_CLOSE_DURATION_MS) * 100; + + if (newProgress >= 100) { + setBotProgress(100); + clearInterval(interval); + handleCloseModal(); + } else { + setBotProgress(newProgress); + } + }, BOT_AUTO_CLOSE_TICK_MS); + + return () => clearInterval(interval); + }, [isBot, showModal]); + + const toastPhaseRef = useRef<'hidden' | 'visible' | 'exiting'>('hidden'); + const [toastRendered, setToastRendered] = useState(false); + const [toastExiting, setToastExiting] = useState(false); + + useEffect(() => { + const phase = toastPhaseRef.current; + const show = !isBot && !canClose && remainingSeconds >= 0.3 + && preventCloseDelaySeconds >= MIN_PREVENT_CLOSE_DELAY_FOR_TOAST_SECONDS; + if (show && phase === 'hidden') { + toastPhaseRef.current = 'visible'; + setToastRendered(true); + setToastExiting(false); + } else if (!show && phase === 'visible') { + toastPhaseRef.current = 'exiting'; + setToastExiting(true); + const t = setTimeout(() => { + toastPhaseRef.current = 'hidden'; + setToastRendered(false); + setToastExiting(false); + }, 400); + return () => clearTimeout(t); } - return () => {}; - }, [isBot, showModal, handleCloseModal]); + return undefined; + }, [isBot, canClose, remainingSeconds]); + + const toastMessage = remainingSeconds < 1 + ? intl.formatMessage(intlMessages.modalCloseDelayMessageMs, { + ms: Math.round(remainingSeconds * 1000), + }) + : intl.formatMessage( + Math.ceil(remainingSeconds) === 1 + ? intlMessages.modalCloseDelayMessageSingular + : intlMessages.modalCloseDelayMessage, + { seconds: Math.ceil(remainingSeconds) }, + ); + + const toast = toastRendered ? ( + + + {toastMessage} + + ) : null; - const title = isYou - ? intl.formatMessage(intlMessages.youWerePicked) - : intl.formatMessage(intlMessages.pickedUser); + const renderOverlay = useCallback( + ( + overlayProps: React.ComponentPropsWithRef<'div'>, + contentEl: React.ReactElement, + ) => ( + + ), + [toast], + ); if (!showModal) return null; + const handleCloseAttempt = () => { + if (canClose && !isBot) { + handleCloseModal(); + } + }; + return ( document.querySelector('#modals-container') as HTMLElement} + appElement={modalAnchor.current} + parentSelector={() => document.querySelector('#modals-container')} isOpen={showModal} - onRequestClose={handleCloseModal} + onRequestClose={handleCloseAttempt} + shouldCloseOnOverlayClick={canClose && !isBot} + shouldCloseOnEsc={canClose && !isBot} + overlayElement={renderOverlay} + $modalUiScale={pickRandomUserSettings.modalUiScale} > - + + + {intl.formatMessage(intlMessages.modalTitle)} + {!isBot && ( - { - handleCloseModal(); - }} - aria-label={intl.formatMessage(intlMessages.closeButton)} + onClick={handleCloseModal} + aria-label={intl.formatMessage(intlMessages.closeButtonAriaLabel)} + data-test="pickRandomUserModalCloseButton" > - - + + )} - + - {isBot && showModal && ( + {isBot && ( - + )} diff --git a/src/components/modal/hooks.ts b/src/components/modal/hooks.ts new file mode 100644 index 0000000..32c7d9b --- /dev/null +++ b/src/components/modal/hooks.ts @@ -0,0 +1,255 @@ +import { + useEffect, useMemo, useRef, useState, +} from 'react'; +import { + CurrentUserData, + DataChannelEntryResponseType, + DataChannelTypes, + GraphqlResponseWrapper, + PluginApi, + PushEntryFunction, +} from 'bigbluebutton-html-plugin-sdk'; +import { PickedUserSeenEntryDataChannel, PickedUserWithEntryId } from '../pick-random-user/types'; +import { PickRandomUserSettings } from '../../commons/types'; +import { notifyRandomlyPickedUser, pingSoundForRandomlyPickedUser } from './utils'; +import { usePreviousValue } from '../../commons/hooks'; +import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; +import { + FilterOptionsType, +} from './types'; + +const UPDATE_COUNTDOWN_RATE = 100; // milliseconds +const COUNTDOWN_RATE_IN_SECONDS = UPDATE_COUNTDOWN_RATE / 1000; + +const useCurrentUserId = (currentUser: CurrentUserData) => { + const [currentUserId, setCurrentUserId] = useState(currentUser?.userId || ''); + + useEffect(() => { + if (currentUserId === '') { + setCurrentUserId(currentUser.userId); + } + }, [currentUser]); + + return currentUserId; +}; + +export const useObserveForNotification = ( + notifyAndPingCallback: () => void, + currentUser: CurrentUserData, + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentPickedUser: PickedUserWithEntryId | null, +) => { + const currentUserId = useCurrentUserId(currentUser); + + const [shouldNotify, setShouldNotify] = useState(false); + + const previousPickedUserEntryId = usePreviousValue(currentPickedUser?.entryId); + + const currentPickedUserId = currentPickedUser?.pickedUser?.userId; + + useEffect(() => { + if (currentPickedUser + && currentPickedUserId === currentUserId + && previousPickedUserEntryId !== currentPickedUser.entryId + ) { + setShouldNotify(true); + } + }, [currentUserId, currentPickedUser]); + + useEffect(() => { + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUserId, + currentPickedUser?.pickedUser.userId, + ); + const notifyUser = !pickedUserSeenEntries?.loading && !hasCurrentUserSeen && shouldNotify; + if (notifyUser) { + notifyAndPingCallback(); + } + setShouldNotify(false); + }, [ + shouldNotify, + ]); +}; + +export const useHandleCurrentUserNotification = ( + currentUser: CurrentUserData, + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentPickedUser: PickedUserWithEntryId | null, + pickRandomUserSettings: PickRandomUserSettings, + notificationMessage: string, +) => { + const { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled } = pickRandomUserSettings; + + function notifyAndPing() { + if (pingSoundEnabled) pingSoundForRandomlyPickedUser(pingSoundUrl); + if (browserNotificationEnabled) { + notifyRandomlyPickedUser( + notificationMessage, + ); + } + } + + useObserveForNotification( + notifyAndPing, + currentUser, + pickedUserSeenEntries, + currentPickedUser, + ); +}; + +export const usePreventCloseModalCountdown = ( + currentUser: CurrentUserData, + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentPickedUser: PickedUserWithEntryId | null, + pickRandomUserSettings: PickRandomUserSettings, +) => { + const { preventCloseDelaySeconds } = pickRandomUserSettings; + + const [remainingSeconds, setRemainingSeconds] = useState(preventCloseDelaySeconds); + const [canClose, setCanClose] = useState(true); + const intervalInstanceRef = useRef(null); + + const currentUserSeenPickedUser: boolean = useMemo( + () => hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUser.userId, + currentPickedUser?.pickedUser.userId, + ), + [pickedUserSeenEntries, currentPickedUser, preventCloseDelaySeconds], + ); + + const stopCountdown = (interval: NodeJS.Timeout) => { + intervalInstanceRef.current = null; + clearInterval(interval); + setCanClose(true); + }; + + const handleCountdownTick = (interval: NodeJS.Timeout) => { + setRemainingSeconds((prev) => { + const newValue = prev - COUNTDOWN_RATE_IN_SECONDS; + if (newValue >= 0) { + return newValue; + } + stopCountdown(interval); + return 0; + }); + }; + + const startCountdown = () => { + setCanClose(false); + const interval = setInterval(() => { + handleCountdownTick(interval); + }, UPDATE_COUNTDOWN_RATE); + intervalInstanceRef.current = interval; + }; + + const resetCountdown = () => { + setRemainingSeconds(preventCloseDelaySeconds); + if (intervalInstanceRef.current) { + clearInterval(intervalInstanceRef.current); + } + if (preventCloseDelaySeconds > 0) { + startCountdown(); + } + }; + + useEffect(() => { + if (!currentUserSeenPickedUser && currentPickedUser) { + resetCountdown(); + } else if (!currentPickedUser) stopCountdown(intervalInstanceRef.current); + }, [currentUserSeenPickedUser, currentPickedUser]); + + return { remainingSeconds, canClose }; +}; + +// Filter Options hooks and utilities +const useUpdateFilterOptionsOnDataChannel = ( + pushFilterOptionsToDataChannel: PushEntryFunction, + filterOptions: FilterOptionsType, + isPresenter: boolean, + dataChannelLoading: boolean, + hasDataChannelBeenApplied: boolean, +) => { + useEffect(() => { + if (hasDataChannelBeenApplied && isPresenter && !dataChannelLoading) { + pushFilterOptionsToDataChannel(filterOptions); + } + }, [isPresenter, filterOptions, dataChannelLoading, hasDataChannelBeenApplied]); +}; + +export const hasFilterOptionsChanged = ( + currentFilterOptions: FilterOptionsType, + filterOptionsFromDataChannel?: FilterOptionsType, +) => filterOptionsFromDataChannel?.includePickedUsers !== currentFilterOptions.includePickedUsers + || filterOptionsFromDataChannel?.includeModerators !== currentFilterOptions.includeModerators + || filterOptionsFromDataChannel?.includePresenter !== currentFilterOptions.includePresenter; + +const useObserveFilterOptionsFromDataChannel = ( + currentFilterOptions: FilterOptionsType, + filterOptionsFromDataChannel: FilterOptionsType | null, + setFilterOptions: React.Dispatch>, + dataChannelLoading: boolean, + setHasDataChannelBeenApplied: React.Dispatch>, +) => { + useEffect(() => { + if (dataChannelLoading) return; + setHasDataChannelBeenApplied(true); + if (filterOptionsFromDataChannel + && hasFilterOptionsChanged(currentFilterOptions, filterOptionsFromDataChannel)) { + setFilterOptions({ + includePickedUsers: filterOptionsFromDataChannel.includePickedUsers, + includeModerators: filterOptionsFromDataChannel.includeModerators, + includePresenter: filterOptionsFromDataChannel.includePresenter, + }); + } + }, [filterOptionsFromDataChannel, dataChannelLoading]); +}; + +export const getLatestFilterOptionsFromDataChannel = ( + filterOptionsFromDataChannelResponse: GraphqlResponseWrapper< + DataChannelEntryResponseType[] + >, +) => { + const persistedFilterOptionsList = filterOptionsFromDataChannelResponse.data; + const currentFilterOptionsFromDataChannel = persistedFilterOptionsList + ? persistedFilterOptionsList[0]?.payloadJson : null; + return currentFilterOptionsFromDataChannel; +}; + +export const useGetFilterOptions = ( + pluginApi: PluginApi, + currentUserPresenter: boolean, +): [FilterOptionsType, React.Dispatch>] => { + const [filterOptions, setFilterOptions] = useState({ + includeModerators: false, + includePresenter: false, + includePickedUsers: false, + }); + const [hasDataChannelBeenApplied, setHasDataChannelBeenApplied] = useState(false); + const { + data: filterOptionsFromDataChannel, + pushEntry: pushFilterOptionsToDataChannel, + } = pluginApi.useDataChannel('filterOptions', DataChannelTypes.LATEST_ITEM); + const latestFilterOptionFromDataChannel = getLatestFilterOptionsFromDataChannel( + filterOptionsFromDataChannel, + ); + useObserveFilterOptionsFromDataChannel( + filterOptions, + latestFilterOptionFromDataChannel, + setFilterOptions, + filterOptionsFromDataChannel.loading, + setHasDataChannelBeenApplied, + ); + useUpdateFilterOptionsOnDataChannel( + pushFilterOptionsToDataChannel, + filterOptions, + currentUserPresenter, + filterOptionsFromDataChannel.loading, + hasDataChannelBeenApplied, + ); + return [filterOptions, setFilterOptions]; +}; diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index 56b38aa..a44a68d 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -1,52 +1,73 @@ import * as React from 'react'; import { useEffect } from 'react'; +import { defineMessages } from 'react-intl'; import { PickedUserViewComponentProps } from './types'; -import Styled from './styles'; -import { Role } from '../../pick-random-user/enums'; +import * as Styled from './styles'; +import { hasCurrentUserSeenPickedUser } from '../../../commons/utils'; +import { UserAvatar } from '../user-avatar/component'; + +const intlMessages = defineMessages({ + resultSectionLabel: { + id: 'pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel', + description: 'Section label shown above the picked user result', + defaultMessage: 'Result', + }, + backButtonLabel: { + id: 'pickRandomUserPlugin.modal.pickedUserView.backButton.label', + description: 'Label of back button in picked-user view on the modal', + defaultMessage: 'back', + }, +}); export function PickedUserViewComponent(props: PickedUserViewComponentProps) { const { - title, + intl, pickedUserWithEntryId, currentUser, - updatePickedRandomUser, - isYou, + handleBack, + showBackButton, + pickedUserSeenEntries, + pushPickedUserSeen, } = props; useEffect(() => { - if (currentUser?.presenter && pickedUserWithEntryId) { - updatePickedRandomUser(pickedUserWithEntryId.entryId, { - ...pickedUserWithEntryId.pickedUser, - isPresenterViewing: true, + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUser?.userId, + pickedUserWithEntryId?.pickedUser?.userId, + ); + if (pickedUserWithEntryId && !hasCurrentUserSeen) { + pushPickedUserSeen({ + pickedUserId: pickedUserWithEntryId?.pickedUser.userId, + seenByUserId: currentUser.userId, }); } - return () => { - if (currentUser?.presenter && pickedUserWithEntryId) { - updatePickedRandomUser(pickedUserWithEntryId.entryId, { - ...pickedUserWithEntryId.pickedUser, - isPresenterViewing: false, - }); - } - }; - }, []); - + }, [pickedUserWithEntryId]); return ( - - {title} - { - (pickedUserWithEntryId) ? ( - <> - - {pickedUserWithEntryId.pickedUser?.name.slice(0, 2)} - - {pickedUserWithEntryId.pickedUser?.name} - - ) : null - } - + + + + {intl.formatMessage(intlMessages.resultSectionLabel)} + + { + (pickedUserWithEntryId) ? ( + <> + + {pickedUserWithEntryId?.pickedUser?.name} + + ) : null + } + + {currentUser?.presenter && showBackButton && ( + + + {intl.formatMessage(intlMessages.backButtonLabel)} + + + )} + ); } diff --git a/src/components/modal/picked-user-view/styles.ts b/src/components/modal/picked-user-view/styles.ts deleted file mode 100644 index 921215b..0000000 --- a/src/components/modal/picked-user-view/styles.ts +++ /dev/null @@ -1,50 +0,0 @@ -import styled from 'styled-components'; -import { colorUserModerator, colorUserViewer, colorUserYou } from '../../../styles/pallete'; - -const AvatarContainer = styled.div` - width: 100%; - align-items: center; - display: flex; - flex-direction: column; -`; - -const ModalAvatar = styled.div<{isYou: boolean, viewer: boolean, moderator: boolean}>` - height: 6rem; - width: 6rem; - ${({ moderator }) => moderator && ` - border-radius: 6px; - `} - - ${({ moderator }) => !moderator && ` - border-radius: 50%; - `} - display: flex; - justify-content: center; - align-items: center; - color: white; - font-size: 2.75rem; - font-weight: 400; - margin-top: 0.5rem; - margin-bottom: .25rem; - text-transform: capitalize; - ${({ viewer }) => viewer && `background-color: ${colorUserViewer};`} - ${({ moderator }) => moderator && `background-color: ${colorUserModerator};`} - ${({ isYou }) => isYou && `background-color: ${colorUserYou};`} -`; - -const UserName = styled.span` - font-size: 30px; - font-weight: 500; -`; - -const Title = styled.h1` - font-weight: 600; - font-size: 20px; -`; - -export default { - AvatarContainer, - ModalAvatar, - UserName, - Title, -}; diff --git a/src/components/modal/picked-user-view/styles.tsx b/src/components/modal/picked-user-view/styles.tsx new file mode 100644 index 0000000..3fef91c --- /dev/null +++ b/src/components/modal/picked-user-view/styles.tsx @@ -0,0 +1,67 @@ +import styled from 'styled-components'; + +const s = (val: number, unit = 'rem') => `calc(${val}${unit} * var(--pru-sm, 1))`; + +const PickedUserViewWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; +`; + +const PickedUserViewBody = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: ${s(1.5)}; +`; + +const PickedUserViewFooter = styled.div` + padding: 0 ${s(1.5)} ${s(1.25)}; + display: flex; + flex-direction: column; + gap: ${s(0.75)}; +`; + +const ResultSectionLabel = styled.span` + font-size: ${s(1.3)}; + font-weight: 800; + color: #8B9AAF; + text-transform: uppercase; + letter-spacing: 0.6px; + margin: ${s(1.25)} 0; +`; + +const PickedUserName = styled.p` + font-size: ${s(2.625)}; + font-weight: 500; + margin: ${s(1.25)} 0; +`; + +const BackButton = styled.button` + width: 100%; + padding: ${s(0.875)} 0; + background: #4E7FF8; + color: #fff; + border: none; + border-radius: 0.375rem; + font-size: ${s(1.125)}; + font-weight: 600; + font-family: inherit; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + &:hover { + background: #3D6DE0; + } +`; + +export { + PickedUserViewWrapper, + PickedUserViewBody, + PickedUserViewFooter, + ResultSectionLabel, + + PickedUserName, + BackButton, +}; diff --git a/src/components/modal/picked-user-view/types.ts b/src/components/modal/picked-user-view/types.ts index 9e676b9..2ffcf25 100644 --- a/src/components/modal/picked-user-view/types.ts +++ b/src/components/modal/picked-user-view/types.ts @@ -1,11 +1,23 @@ -import { CurrentUserData } from 'bigbluebutton-html-plugin-sdk'; -import { ReplaceEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; -import { PickedUser, PickedUserWithEntryId } from '../../pick-random-user/types'; +import { + CurrentUserData, + DataChannelEntryResponseType, + GraphqlResponseWrapper, + PushEntryFunction, +} from 'bigbluebutton-html-plugin-sdk'; +import { IntlShape } from 'react-intl'; +import { PickedUserWithEntryId, PickedUserSeenEntryDataChannel } from '../../pick-random-user/types'; export interface PickedUserViewComponentProps { - title: string; - updatePickedRandomUser: ReplaceEntryFunction; - pickedUserWithEntryId: PickedUserWithEntryId; + intl: IntlShape; + pickedUserWithEntryId: PickedUserWithEntryId | null; currentUser: CurrentUserData; - isYou: boolean; + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>; + pushPickedUserSeen: PushEntryFunction; + // On v0.0.x "back" returns to the presenter view inside the modal. On this branch the + // presenter view is the sidekick panel, so "back" simply dismisses the modal. + handleBack: () => void; + showBackButton: boolean; + remainingSeconds: number; + canClose: boolean; } diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx new file mode 100644 index 0000000..b41ddd0 --- /dev/null +++ b/src/components/modal/presenter-view/component.tsx @@ -0,0 +1,371 @@ +import * as React from 'react'; +import { RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; +import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; +import { defineMessages } from 'react-intl'; + +import * as Styled from './styles'; +import { PickedUser } from '../../pick-random-user/types'; +import { PresenterViewComponentProps } from './types'; +import { UserAvatar } from '../user-avatar/component'; +import { useGetPickRandomUserFunction } from './hooks'; +import { formatPickedTime } from './utils'; + +const intlMessages = defineMessages({ + filterChipsLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.label', + description: 'Label preceding the filter chip group', + defaultMessage: 'Also include:', + }, + moderatorsChipLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.moderators', + description: 'Chip label to include moderators', + defaultMessage: 'Moderators', + }, + presenterChipLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.presenter', + description: 'Chip label to include presenter', + defaultMessage: 'Presenter', + }, + pickedUsersChipLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers', + description: 'Chip label to include already picked users', + defaultMessage: 'Already picked', + }, + availableTitle: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.title', + description: 'Title of the "available users" section on modal`s presenter view', + defaultMessage: 'Available for selection', + }, + userLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.userLabel', + description: 'Label to count user in "available users" section on presenter view', + defaultMessage: 'user', + }, + userLabelPlural: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural', + description: 'Label to count users in "available users" section on presenter view', + defaultMessage: 'users', + }, + viewerLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel', + description: 'Label to count viewer in "available users" section on presenter view', + defaultMessage: 'viewer', + }, + viewerLabelPlural: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural', + description: 'Label to count viewers in "available users" section on presenter view', + defaultMessage: 'viewers', + }, + previouslyPickedTitle: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title', + description: 'Title of the "previously picked" section on presenter view', + defaultMessage: 'Previously picked', + }, + clearButtonLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel', + description: 'Label of button to clear list of already picked users', + defaultMessage: 'Clear All', + }, + noUsersWarning: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning', + description: 'Warning that there is no user to be picked', + defaultMessage: 'No {0} available to randomly pick from', + }, + pickButtonLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser', + description: 'Label of the button to pick another user', + defaultMessage: 'Pick random user', + }, + pickNextRandomUserButtonLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext', + description: 'Label of the button to pick next random user (when already picked users are included in pool)', + defaultMessage: 'Pick next random user', + }, + pickAnotherRandomUserButtonLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother', + description: 'Label of the button to pick another random user (when already picked users are excluded)', + defaultMessage: 'Pick another random user', + }, + emptyState: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState', + description: 'Empty state text shown when no user has been picked yet', + defaultMessage: 'No user selected yet', + }, + availableEmptyState: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.emptyState', + description: 'Empty state text shown when no user is available for selection', + defaultMessage: 'No {0} available for selection', + }, + availableLoading: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.loading', + description: 'Loading text shown while the list of users is still being fetched', + defaultMessage: 'Loading…', + }, + moderatorRoleLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.roleLabel.moderator', + description: 'Role badge label for moderators', + defaultMessage: 'moderator', + }, + presenterRoleLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.roleLabel.presenter', + description: 'Role badge label for presenters', + defaultMessage: 'presenter', + }, +}); + +function CheckboxSquare({ active }: { active: boolean }) { + const style: React.CSSProperties = active ? { + width: '0.875rem', + height: '0.875rem', + borderRadius: '3px', + background: '#4E7FF8', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + } : { + width: '0.875rem', + height: '0.875rem', + borderRadius: '3px', + background: '#fff', + border: '1.5px solid #C0C8D4', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + }; + return ( + + {active && ( + + + + )} + + ); +} + +function makePickedUserRows(list?: DataChannelEntryResponseType[]) { + return list?.filter((u) => !!u.payloadJson).map((u) => ( + + + {u.payloadJson.name} + {formatPickedTime(u.createdAt)} + + )); +} + +export function PresenterViewComponent(props: PresenterViewComponentProps) { + const { + intl, + deletionFunction, + dataChannelPickedUsers, + pickedUserWithEntryId, + pluginApi, + filterOptions, + setFilterOptions, + usersToBePicked, + isLoading, + } = props; + + const { + includeModerators, + includePresenter, + includePickedUsers, + } = filterOptions; + + const handlePickRandomUser = useGetPickRandomUserFunction(pluginApi, usersToBePicked); + + const usersCount = usersToBePicked?.length ?? 0; + const userRoleLabel = (() => { + if (!includeModerators) { + return usersCount !== 1 + ? intl.formatMessage(intlMessages.viewerLabelPlural, { 0: usersCount }) + : intl.formatMessage(intlMessages.viewerLabel, { 0: usersCount }); + } + return usersCount !== 1 + ? intl.formatMessage(intlMessages.userLabelPlural, { 0: usersCount }) + : intl.formatMessage(intlMessages.userLabel, { 0: usersCount }); + })(); + + const hasPickedUsers = dataChannelPickedUsers?.some((u) => !!u.payloadJson); + + return ( + + + + {/* FILTER CHIPS */} + + + + {intl.formatMessage(intlMessages.filterChipsLabel)} + + + + setFilterOptions((prev) => ({ + ...prev, includeModerators: !prev.includeModerators, + }))} + /> + + {intl.formatMessage(intlMessages.moderatorsChipLabel)} + + + + setFilterOptions((prev) => ({ + ...prev, includePresenter: !prev.includePresenter, + }))} + /> + + {intl.formatMessage(intlMessages.presenterChipLabel)} + + + + setFilterOptions((prev) => ({ + ...prev, includePickedUsers: !prev.includePickedUsers, + }))} + /> + + {intl.formatMessage(intlMessages.pickedUsersChipLabel)} + + + + + + {/* AVAILABLE USERS SECTION */} + + + + {intl.formatMessage(intlMessages.availableTitle)} + + + {usersCount} + {' '} + {userRoleLabel} + + + {isLoading && ( + + + + {intl.formatMessage(intlMessages.availableLoading)} + + + )} + {!isLoading && usersCount === 0 && ( + + + {intl.formatMessage(intlMessages.availableEmptyState, { 0: userRoleLabel })} + + + )} + {!isLoading && usersCount > 0 && ( + + {usersToBePicked?.map((user) => { + let roleBadgeLabel: string | null = null; + if (user.role === 'MODERATOR') { + roleBadgeLabel = intl.formatMessage(intlMessages.moderatorRoleLabel); + } else if (user.presenter) { + roleBadgeLabel = intl.formatMessage(intlMessages.presenterRoleLabel); + } + return ( + + + {user.name} + {roleBadgeLabel && ( + {roleBadgeLabel} + )} + + ); + })} + + )} + + + {/* PREVIOUSLY PICKED SECTION */} + + + + {intl.formatMessage(intlMessages.previouslyPickedTitle)} + + deletionFunction([RESET_DATA_CHANNEL])} + > + {intl.formatMessage(intlMessages.clearButtonLabel)} + + + {hasPickedUsers ? ( + + + {makePickedUserRows(dataChannelPickedUsers)} + + + ) : ( + <> + + + {intl.formatMessage(intlMessages.emptyState)} + + + {/* Empty list kept in DOM so [data-test] li selectors resolve correctly */} + + + )} + + + + + {/* FOOTER */} + + {isLoading && ( + + {intl.formatMessage(intlMessages.availableLoading)} + + )} + {!isLoading && usersCount > 0 && ( + + {pickedUserWithEntryId + ? intl.formatMessage(includePickedUsers + ? intlMessages.pickNextRandomUserButtonLabel + : intlMessages.pickAnotherRandomUserButtonLabel) + : intl.formatMessage(intlMessages.pickButtonLabel)} + + )} + {!isLoading && usersCount === 0 && ( + + {intl.formatMessage(intlMessages.noUsersWarning, { 0: userRoleLabel })} + + )} + + + ); +} diff --git a/src/components/modal/presenter-view/hooks.ts b/src/components/modal/presenter-view/hooks.ts new file mode 100644 index 0000000..23b8dee --- /dev/null +++ b/src/components/modal/presenter-view/hooks.ts @@ -0,0 +1,108 @@ +import { useEffect } from 'react'; +import { + PluginApi, + RESET_DATA_CHANNEL, + UsersBasicInfoData, + pluginLogger, +} from 'bigbluebutton-html-plugin-sdk'; +import { + FilterOptionsType, +} from '../types'; +import { PickedUser, PickedUserSeenEntryDataChannel } from '../../pick-random-user/types'; +import { filterPossibleUsersToBePicked } from './utils'; + +export function useGetPickRandomUserFunction( + pluginApi: PluginApi, + possibleUsersToBePicked: UsersBasicInfoData[], +) { + const currentUserInfo = pluginApi.useCurrentUser(); + const { data: currentUser } = currentUserInfo; + + const { + pushEntry: pushPickedUser, + } = pluginApi.useDataChannel('pickRandomUser'); + + const { + deleteEntry: deletePickedUserSeenEntries, + } = pluginApi.useDataChannel('pickedUserSeenEntry'); + + const handlePickRandomUser = () => { + if ( + possibleUsersToBePicked + && possibleUsersToBePicked.length > 0 + && currentUser?.presenter + ) { + deletePickedUserSeenEntries([RESET_DATA_CHANNEL]); + const randomIndex = Math.floor(Math.random() * possibleUsersToBePicked.length); + const randomlyPickedUser = possibleUsersToBePicked[randomIndex]; + pushPickedUser(randomlyPickedUser); + } + }; + + return handlePickRandomUser; +} + +export function useGetPossibleUsersToBePicked( + pluginApi: PluginApi, + filterOptions: FilterOptionsType, +) { + const allUsersInfo = pluginApi?.useUsersBasicInfo + ? pluginApi?.useUsersBasicInfo() + : { data: undefined as undefined, loading: false, error: undefined }; + const { data: allUsers, loading } = allUsersInfo; + + // This plugin relies on *live* data: the presenter must never pick someone who has + // already left. So we deliberately do NOT retain the previous snapshot — while the + // SDK is (re)fetching or has no data yet, we report loading instead of a possibly + // stale list. The footer hides the Pick button whenever isLoading is true, so the + // presenter cannot act on data we are not sure is current. + const isLoading = loading || allUsers === undefined; + + // TEMPORARY DEBUG INSTRUMENTATION — investigating an "unstable connection empties + // the available-users list" report. Remove once diagnosed. + useEffect(() => { + pluginLogger.debug({ + logCode: 'pick_random_user_debug_users_basic_info', + extraInfo: { + timestamp: new Date().toISOString(), + loading: allUsersInfo.loading, + hasError: !!allUsersInfo.error, + userCount: allUsers?.user?.length, + users: allUsers?.user?.map((u) => ({ + userId: u.userId, + name: u.name, + role: u.role, + isModerator: u.isModerator, + presenter: u.presenter, + bot: u.bot, + })), + }, + }, '[DEBUG pick-random-user] useUsersBasicInfo update'); + }, [allUsersInfo]); + + const { + data: pickedUserFromDataChannelResponse, + } = pluginApi.useDataChannel('pickRandomUser'); + const pickedUserFromDataChannel = pickedUserFromDataChannelResponse?.data || []; + + const result = filterPossibleUsersToBePicked( + allUsers, + pickedUserFromDataChannel, + filterOptions, + ).user; + + // TEMPORARY DEBUG INSTRUMENTATION — see above. + useEffect(() => { + pluginLogger.debug({ + logCode: 'pick_random_user_debug_filtered_result', + extraInfo: { + timestamp: new Date().toISOString(), + resultCount: result.length, + resultUserIds: result.map((u) => u.userId), + filterOptions, + }, + }, '[DEBUG pick-random-user] usersToBePicked recomputed'); + }, [result, filterOptions]); + + return { users: result, isLoading }; +} diff --git a/src/components/modal/presenter-view/styles.tsx b/src/components/modal/presenter-view/styles.tsx new file mode 100644 index 0000000..a6ca217 --- /dev/null +++ b/src/components/modal/presenter-view/styles.tsx @@ -0,0 +1,319 @@ +import styled, { keyframes } from 'styled-components'; + +const s = (val: number, unit = 'rem') => `calc(${val}${unit} * var(--pru-sm, 1))`; + +// ── Section labels ──────────────────────────────────────────────────────────── + +const SectionLabel = styled.span` + font-size: ${s(0.9375)}; + font-weight: 600; + color: #8B9AAF; + text-transform: uppercase; + letter-spacing: 0.6px; +`; + +// ── Filter chips section ────────────────────────────────────────────────────── + +const FilterRow = styled.div` + display: flex; + flex-direction: column; + gap: ${s(0.5)}; +`; + +const FilterLabel = styled.span` + font-size: ${s(1.0625)}; + font-weight: 600; + color: #8B9AAF; + white-space: nowrap; +`; + +const ChipGroup = styled.div` + display: flex; + align-items: center; + gap: ${s(0.5)}; + flex-wrap: wrap; +`; + +const ChipInput = styled.input` + display: none; +`; + +const FilterChip = styled.label<{ $active: boolean }>` + display: inline-flex; + align-items: center; + gap: ${s(0.375)}; + padding: ${s(0.375)} ${s(0.75)}; + border-radius: 999px; + font-size: ${s(1)}; + cursor: pointer; + user-select: none; + transition: all 0.15s; + + ${({ $active }) => ($active ? ` + background: #EBF1FF; + border: 1.5px solid #4E7FF8; + color: #4E7FF8; + font-weight: 600; + ` : ` + background: #F0F2F6; + border: 1.5px solid #D1D9E3; + color: #8B9AAF; + font-weight: 500; + `)} +`; + +// ── Section header row (label + count/action) ───────────────────────────────── + +const SectionHeaderRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: ${s(0.625)}; +`; + +// ── Available users section ─────────────────────────────────────────────────── + +const CountBadge = styled.span` + font-size: ${s(1)}; + color: #4E7FF8; + font-weight: 600; +`; + +const UserListContainer = styled.div` + background: #F7F9FB; + border-radius: 0.375rem; + padding: ${s(0.75)} ${s(1)}; + display: flex; + flex-direction: column; + gap: ${s(0.5)}; + max-height: ${s(10)}; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 4px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 999px; + } + &::-webkit-scrollbar-button { + display: none; + } + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; +`; + +const UserRow = styled.div` + display: flex; + align-items: center; + gap: ${s(0.625)}; +`; + +const UserNameText = styled.span` + font-size: ${s(1.0625)}; + color: #1C2B3A; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const RoleBadge = styled.span` + font-size: ${s(0.875)}; + color: #8B9AAF; + background: #E8EDF2; + padding: ${s(0.125)} ${s(0.5)}; + border-radius: 0.1875rem; + margin-left: auto; + flex-shrink: 0; + white-space: nowrap; +`; + +// ── Previously picked section ───────────────────────────────────────────────── + +const ClearAllButton = styled.button` + font-size: ${s(0.9375)}; + color: #8B9AAF; + background: none; + border: none; + text-decoration: underline; + text-underline-offset: 2px; + cursor: pointer; + padding: 0; + + &:hover { + color: #6b7d92; + } +`; + +const EmptyStateContainer = styled.div` + background: #F7F9FB; + border-radius: 0.375rem; + padding: ${s(1.125)}; + text-align: center; +`; + +const EmptyStateText = styled.span` + font-size: ${s(1.0625)}; + color: #A7B3C3; +`; + +// ── Loading state (shown while the first user snapshot has not arrived yet) ──── + +const spin = keyframes` + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +`; + +const LoadingContainer = styled.div` + background: #F7F9FB; + border-radius: 0.375rem; + padding: 0.875rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +`; + +const SpinnerRing = styled.span` + width: 1rem; + height: 1rem; + border-radius: 50%; + border: 2px solid #D1D9E3; + border-top-color: #4E7FF8; + animation: ${spin} 0.7s linear infinite; + flex-shrink: 0; +`; + +const LoadingText = styled.span` + font-size: 0.8125rem; + color: #A7B3C3; +`; + +const PickedUserListContainer = styled(UserListContainer)` +`; + +//
    that test selectors target with [data-test="pickRandomUserPreviouslyPickedList"] +const PickedList = styled.ul` + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: ${s(0.5)}; +`; + +const PickedUserRow = styled.li` + display: flex; + align-items: center; + gap: ${s(0.625)}; + list-style: none; +`; + +const PickedTimeText = styled.span` + font-size: ${s(0.9375)}; + color: #A7B3C3; + margin-left: auto; + flex-shrink: 0; +`; + +// ── Footer / action button ──────────────────────────────────────────────────── + +const FooterContainer = styled.div` + padding: 0 ${s(1.5)} ${s(1.25)}; + flex-shrink: 0; +`; + +const PickButton = styled.button` + width: 100%; + padding: ${s(0.875)} 0; + background: #4E7FF8; + color: #fff; + border: none; + border-radius: 0.375rem; + font-size: ${s(1.125)}; + font-weight: 600; + font-family: inherit; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: ${s(0.5)}; + + &:hover { + background: #3D6DE0; + } +`; + +const NoUsersWarning = styled.p` + font-size: ${s(1.0625)}; + color: #8B9AAF; + text-align: center; + margin: 0; + padding: ${s(0.5)} 0; +`; + +// ── Outer wrappers ──────────────────────────────────────────────────────────── + +const PresenterViewWrapper = styled.div` + font-family: 'Source Sans Pro', Arial, sans-serif; + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 0; +`; + +const ContentPadding = styled.div` + padding: ${s(1.25)} ${s(1.5)} ${s(1)}; + display: flex; + flex-direction: column; + gap: ${s(1.25)}; + flex: 1; + overflow-y: auto; + min-height: 0; +`; + +const OptionsSection = styled.div``; + +const AvailableSection = styled.div``; + +const PreviouslyPickedSection = styled.div``; + +export { + SectionLabel, + FilterRow, + FilterLabel, + ChipGroup, + ChipInput, + FilterChip, + SectionHeaderRow, + CountBadge, + UserListContainer, + UserRow, + + UserNameText, + RoleBadge, + ClearAllButton, + EmptyStateContainer, + EmptyStateText, + LoadingContainer, + SpinnerRing, + LoadingText, + PickedUserListContainer, + PickedList, + PickedUserRow, + PickedTimeText, + FooterContainer, + PickButton, + NoUsersWarning, + PresenterViewWrapper, + ContentPadding, + OptionsSection, + AvailableSection, + PreviouslyPickedSection, +}; diff --git a/src/components/modal/presenter-view/types.ts b/src/components/modal/presenter-view/types.ts new file mode 100644 index 0000000..12052f9 --- /dev/null +++ b/src/components/modal/presenter-view/types.ts @@ -0,0 +1,19 @@ +import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; +import { + DeleteEntryFunction, PluginApi, UsersBasicInfoData, +} from 'bigbluebutton-html-plugin-sdk'; +import { IntlShape } from 'react-intl'; +import { PickedUser, PickedUserWithEntryId } from '../../pick-random-user/types'; +import { FilterOptionsType } from '../types'; + +export interface PresenterViewComponentProps { + intl: IntlShape; + deletionFunction: DeleteEntryFunction; + dataChannelPickedUsers?: DataChannelEntryResponseType[]; + pickedUserWithEntryId: PickedUserWithEntryId | null; + pluginApi: PluginApi; + filterOptions: FilterOptionsType; + setFilterOptions: React.Dispatch>; + usersToBePicked: UsersBasicInfoData[]; + isLoading: boolean; +} diff --git a/src/components/modal/presenter-view/utils.ts b/src/components/modal/presenter-view/utils.ts new file mode 100644 index 0000000..be02510 --- /dev/null +++ b/src/components/modal/presenter-view/utils.ts @@ -0,0 +1,40 @@ +import { DataChannelEntryResponseType, UsersBasicInfoResponseFromGraphqlWrapper } from 'bigbluebutton-html-plugin-sdk'; +import { + PickedUser, +} from '../../pick-random-user/types'; +import { Role } from '../../pick-random-user/enums'; +import { FilterOptionsType } from '../types'; + +export const filterPossibleUsersToBePicked = ( + allUsers: UsersBasicInfoResponseFromGraphqlWrapper | undefined, + pickedUserFromDataChannel: DataChannelEntryResponseType[], + filterOptions: FilterOptionsType, +) => ({ + user: allUsers?.user.filter((user) => !user.bot).filter((user) => { + if (!filterOptions.includeModerators) return user.role === Role.VIEWER; + return true; + }).filter((user) => { + if (!filterOptions.includePresenter) return !user.presenter; + return true; + }).filter((user) => { + if (!filterOptions.includePickedUsers && pickedUserFromDataChannel) { + return pickedUserFromDataChannel.findIndex( + (u) => u?.payloadJson?.userId === user?.userId, + ) === -1; + } + return true; + }) || [], +}); + +/** + * Formats the moment a user was picked as a zero-padded local `HH:mm` label, + * used in the "previously picked" list of the presenter view. + * @param createdAt timestamp of the pick (anything accepted by the Date constructor) + * @returns the local time as `HH:mm` + */ +export const formatPickedTime = (createdAt: string | number | Date): string => { + const time = new Date(createdAt); + const hh = String(time.getHours()).padStart(2, '0'); + const mm = String(time.getMinutes()).padStart(2, '0'); + return `${hh}:${mm}`; +}; diff --git a/src/components/modal/raw_styles.css b/src/components/modal/raw_styles.css deleted file mode 100644 index 34e5763..0000000 --- a/src/components/modal/raw_styles.css +++ /dev/null @@ -1,12 +0,0 @@ -.modal-overlay { - z-index: 900; - display: flex; - align-items: center; - justify-content: center; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(6, 23, 42, 0.75); -} diff --git a/src/components/modal/styles.ts b/src/components/modal/styles.ts deleted file mode 100644 index 8e3df84..0000000 --- a/src/components/modal/styles.ts +++ /dev/null @@ -1,125 +0,0 @@ -import styled from 'styled-components'; -import ReactModal from 'react-modal'; -import { colorGrayLightest, colorPrimary } from '../../styles/pallete'; - -const PluginModal = styled(ReactModal)` - position: relative; - z-index: 900 !important; - - outline: transparent; - outline-width: 2px; - outline-style: solid; - display: flex; - flex-direction: column; - padding: 2rem; - box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.7); - background-color: #FFF !important; - max-width: 60vw; - max-height: 100%; - border-radius: .2rem; - - color: #4E5A66; - - overflow: auto; - - overflow-y: hidden; - - background-repeat: no-repeat; - background-color: transparent; - background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; - background-attachment: local, local, scroll, scroll; - - &::-webkit-scrollbar { - width: 5px; - height: 5px; - } - &::-webkit-scrollbar-button { - width: 0; - height: 0; - } - &::-webkit-scrollbar-thumb { - background: rgba(0,0,0,.25); - border: none; - border-radius: 50px; - } - &::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,.5); } - &::-webkit-scrollbar-thumb:active { background: rgba(0,0,0,.25); } - &::-webkit-scrollbar-track { - background: rgba(0,0,0,.25); - border: none; - border-radius: 50px; - } - &::-webkit-scrollbar-track:hover { background: rgba(0,0,0,.25); } - &::-webkit-scrollbar-track:active { background: rgba(0,0,0,.25); } - &::-webkit-scrollbar-corner { background: 0 0; } - - @media only screen and (max-width: 40em) { - max-width: 95vw; - } - - @media only screen and (min-width: 40.063em) { - max-width: 80vw; - } -`; - -const ModalOverlay = ` - z-index: 900; - display: flex; - align-items: center; - justify-content: center; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(6, 23, 42, 0.75); -`; - -const ModalContainer = styled.div` - width: 100%; - align-items: flex-end; - display: flex; - flex-direction: column; -`; - -const ButtonClose = styled.button` - font-size: 40px; - background: none; - color: inherit; - font: inherit; - cursor: pointer; - outline: inherit; - border: none; - - &:hover { - background-color: #EEE; - } -`; - -const ProgressBarContainer = styled.div` - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 4px; - background-color: ${colorGrayLightest}; - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - overflow: hidden; -`; - -const ProgressBarFill = styled.div<{ progress: number }>` - height: 100%; - background-color: ${colorPrimary}; - width: ${({ progress }) => `${progress}%`}; - transition: width 0.03s linear; -`; - -export default { - PluginModal, - ModalOverlay, - ModalContainer, - ButtonClose, - ProgressBarContainer, - ProgressBarFill, -}; diff --git a/src/components/modal/styles.tsx b/src/components/modal/styles.tsx new file mode 100644 index 0000000..c9c683e --- /dev/null +++ b/src/components/modal/styles.tsx @@ -0,0 +1,153 @@ +import ReactModal from 'react-modal'; +import styled, { css, keyframes } from 'styled-components'; + +const s = (val: number, unit = 'rem') => `calc(${val}${unit} * var(--pru-sm, 1))`; + +const PluginModal = styled(ReactModal)<{ $modalUiScale?: number }>` + --pru-sm: ${({ $modalUiScale }) => $modalUiScale ?? 1}; + position: relative; + z-index: 1000 !important; + outline: transparent; + outline-width: 2px; + outline-style: solid; + display: flex; + flex-direction: column; + background-color: #fff !important; + width: ${s(32)}; + max-width: 95vw; + max-height: 90vh; + border-radius: 0.5rem; + box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4); + overflow: hidden; + font-family: 'Source Sans Pro', Arial, sans-serif; + + &::-webkit-scrollbar { + width: 5px; + height: 5px; + } + &::-webkit-scrollbar-button { + width: 0; + height: 0; + } + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.25); + border: none; + border-radius: 50px; + } + &::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.5); + } + &::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border: none; + border-radius: 50px; + } + &::-webkit-scrollbar-corner { + background: transparent; + } +`; + +const ModalHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: ${s(1.25)} ${s(1.5)} ${s(1.125)}; + border-bottom: 1px solid #E8EDF2; + flex-shrink: 0; +`; + +const ModalTitle = styled.span` + font-weight: 600; + font-size: ${s(1.55)}; + color: #1C2B3A; +`; + +const CloseButton = styled.button` + font-size: ${s(1.3)}; + background: none; + color: #8B9AAF; + border: none; + cursor: pointer; + padding: ${s(0.375)}; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.25rem; + line-height: 1; + + &:hover { + background-color: #EEF2F8; + color: #1C2B3A; + } +`; + +const toastSlideIn = keyframes` + from { opacity: 0; transform: translateX(-50%) translateY(8px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } +`; + +const toastSlideOut = keyframes` + from { opacity: 1; transform: translateX(-50%) translateY(0); } + to { opacity: 0; transform: translateX(-50%) translateY(8px); } +`; + +/* position:relative so the absolutely-positioned toast anchors to this wrapper, + not to the overlay. The wrapper's own height equals only the modal — no shift. */ +const ModalWithToastWrapper = styled.div` + position: relative; +`; + +const FloatingToast = styled.div<{ $exiting: boolean }>` + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + margin-top: ${s(10, 'px')}; + width: fit-content; + white-space: nowrap; + padding: ${s(12, 'px')} ${s(20, 'px')}; + border-radius: 10px; + background-color: #fff; + border: 0.5px solid #E8EDF2; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + display: flex; + align-items: center; + gap: ${s(10, 'px')}; + font-family: 'Source Sans Pro', Arial, sans-serif; + font-size: ${s(17, 'px')}; + color: #6c757d; + pointer-events: none; + ${({ $exiting }) => css` + animation: ${$exiting ? toastSlideOut : toastSlideIn} 0.35s ease forwards; + `} +`; + +/* Auto-close progress bar shown to bot users, which cannot dismiss the modal + themselves. Kept from the v0.1.x branch (BigBlueButton 4.0 only). */ +const ProgressBarContainer = styled.div` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 4px; + background-color: #E8EDF2; + overflow: hidden; +`; + +const ProgressBarFill = styled.div<{ $progress: number }>` + height: 100%; + background-color: #4A6CF7; + width: ${({ $progress }) => `${$progress}%`}; + transition: width 0.03s linear; +`; + +export { + PluginModal, + ModalHeader, + ModalTitle, + CloseButton, + ModalWithToastWrapper, + FloatingToast, + ProgressBarContainer, + ProgressBarFill, +}; diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index 2ed4937..f919f8a 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -1,14 +1,40 @@ +import { + CurrentUserData, + GraphqlResponseWrapper, +} from 'bigbluebutton-html-plugin-sdk'; import { IntlShape } from 'react-intl'; -import { CurrentUserData } from 'bigbluebutton-html-plugin-sdk'; -import { ReplaceEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; -import { PickedUser, PickedUserWithEntryId } from '../pick-random-user/types'; +import { DataChannelEntryResponseType, PushEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; +import { PickedUserWithEntryId, PickedUserSeenEntryDataChannel } from '../pick-random-user/types'; +import { PickRandomUserSettings } from '../../commons/types'; export interface PickUserModalProps { - intl: IntlShape; + pickRandomUserSettings: PickRandomUserSettings + uuid: string, + intl: IntlShape showModal: boolean; - updatePickedRandomUser: ReplaceEntryFunction; handleCloseModal: () => void; - pickedUserWithEntryId: PickedUserWithEntryId; + currentPickedUser: PickedUserWithEntryId | null; currentUser: CurrentUserData; + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>; + pushPickedUserSeen: PushEntryFunction; isBot: boolean; } + +export interface FilterOptionsType { + includeModerators: boolean; + includePresenter: boolean; + includePickedUsers: boolean; +} + +export interface WindowClientSettings extends Window { + meetingClientSettings?: { + public: { + app: { + cdn: string; + basename: string; + } + + } + } +} diff --git a/src/components/modal/user-avatar/component.tsx b/src/components/modal/user-avatar/component.tsx new file mode 100644 index 0000000..ad69142 --- /dev/null +++ b/src/components/modal/user-avatar/component.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import * as Styled from './styles'; + +const FALLBACK_AVATAR_COLORS = ['#4E7FF8', '#2BA084', '#E07A3A', '#7B61D9', '#D4733B']; + +function getAvatarColorFallback(name: string): string { + const hash = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); + return FALLBACK_AVATAR_COLORS[hash % FALLBACK_AVATAR_COLORS.length]; +} + +interface AvatarUser { + name: string; + avatar: string; + color: string; + role: string; +} + +interface UserAvatarProps { + user: AvatarUser; + size: 'small' | 'large'; +} + +export function UserAvatar({ user, size }: UserAvatarProps) { + const color = user.color || getAvatarColorFallback(user.name); + const initials = user.name.slice(0, 2); + const isModerator = user.role === 'MODERATOR'; + + if (user.avatar) { + return ( + + ); + } + return ( + + {initials} + + ); +} diff --git a/src/components/modal/user-avatar/styles.tsx b/src/components/modal/user-avatar/styles.tsx new file mode 100644 index 0000000..c72e879 --- /dev/null +++ b/src/components/modal/user-avatar/styles.tsx @@ -0,0 +1,32 @@ +import styled from 'styled-components'; + +const SIZES = { + small: { dimension: '1.625rem', fontSize: '0.625rem', fontWeight: '600' }, + large: { dimension: '6rem', fontSize: '2.75rem', fontWeight: '400' }, +}; + +const AvatarInitials = styled.div<{ $size: 'small' | 'large'; $color: string; $isModerator: boolean }>` + width: ${({ $size }) => SIZES[$size].dimension}; + height: ${({ $size }) => SIZES[$size].dimension}; + border-radius: ${({ $isModerator }) => ($isModerator ? '20%' : '50%')}; + flex-shrink: 0; + background: ${({ $color }) => $color}; + display: flex; + align-items: center; + justify-content: center; + font-size: ${({ $size }) => SIZES[$size].fontSize}; + font-weight: ${({ $size }) => SIZES[$size].fontWeight}; + color: #fff; + text-transform: capitalize; +`; + +const AvatarImage = styled.img<{ $size: 'small' | 'large'; $isModerator: boolean; $color: string }>` + width: ${({ $size }) => SIZES[$size].dimension}; + height: ${({ $size }) => SIZES[$size].dimension}; + border-radius: ${({ $isModerator }) => ($isModerator ? '20%' : '50%')}; + flex-shrink: 0; + object-fit: cover; + border: 2px solid ${({ $color }) => $color}; +`; + +export { AvatarInitials, AvatarImage }; diff --git a/src/components/modal/utils.ts b/src/components/modal/utils.ts new file mode 100644 index 0000000..7c7e93a --- /dev/null +++ b/src/components/modal/utils.ts @@ -0,0 +1,24 @@ +import { pluginLogger } from 'bigbluebutton-html-plugin-sdk'; + +import { TIMEOUT_CLOSE_NOTIFICATION } from '../../commons/constants'; +import { WindowClientSettings } from './types'; + +declare const window: WindowClientSettings; + +export function notifyRandomlyPickedUser(message: string) { + if (!('Notification' in window)) { + pluginLogger.warn('This browser does not support notifications'); + } else if (Notification.permission === 'granted') { + const notification = new Notification(message); + setTimeout(() => { + notification.close(); + }, TIMEOUT_CLOSE_NOTIFICATION); + } else if (Notification.permission !== 'denied') { + pluginLogger.warn('Browser notification permission has been denied'); + } +} + +export function pingSoundForRandomlyPickedUser(pingSoundUrl: string) { + const audio = new Audio(pingSoundUrl); + audio.play(); +} diff --git a/src/components/pick-random-user-panel/component.tsx b/src/components/pick-random-user-panel/component.tsx index 80183fc..df1eb0e 100644 --- a/src/components/pick-random-user-panel/component.tsx +++ b/src/components/pick-random-user-panel/component.tsx @@ -1,321 +1,71 @@ import * as React from 'react'; -import { useState, useEffect } from 'react'; -import { DataChannelTypes, RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; -import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; -import { USERS_MORE_INFORMATION } from './queries'; -import { PickedUser, UsersMoreInformationGraphqlResponse } from '../pick-random-user/types'; -import { PanelInformationFromPresenter, PickRandomUserPanelComponentProps } from './types'; -import { Role } from '../pick-random-user/enums'; -import { intlMessages } from '../../intlMessages'; -import Styled from './styles'; +import * as Styled from './styles'; +import { PickRandomUserPanelContentProps, PickRandomUserPanelProps } from './types'; +import { PresenterViewComponent } from '../modal/presenter-view/component'; +import { useGetPossibleUsersToBePicked } from '../modal/presenter-view/hooks'; +import { useGetFilterOptions } from '../modal/hooks'; +import { useGetCurrentPickedUser } from '../pick-random-user/hooks'; +import { PickedUser } from '../pick-random-user/types'; -const MAX_NAMES_TO_SHOW = 3; -const DATA_CHANNEL_LOADING_TOLERANCE_TIMEOUT = 3000; +/** + * Gathers everything `PresenterViewComponent` needs. On v0.0.x those props come from the + * modal, which does not host the presenter view on this branch. This renders in its own + * React root (created by the sidekick area's contentFunction), which is why the data + * hooks are called here rather than passed in. + * + * It is kept separate from `PickRandomUserPanel` so that the `useUsersBasicInfo` + * subscription is only ever opened for the presenter, and stays alive across picks. + */ +function PickRandomUserPanelContent(props: PickRandomUserPanelContentProps) { + const { pluginApi, intl } = props; -const makeHorizontalListOfNames = (list?: PickedUser[]) => { - const length = list?.length; - const formattedList = list?.filter((_, index) => { - if (length > MAX_NAMES_TO_SHOW) return index < MAX_NAMES_TO_SHOW; - return true; - }).reduce((accumulator, currentValue) => ((accumulator !== '') ? `${accumulator}, ${currentValue.name}` : currentValue.name), ''); - if (length > MAX_NAMES_TO_SHOW) return `${formattedList}...`; - return formattedList; -}; + const [filterOptions, setFilterOptions] = useGetFilterOptions(pluginApi, true); -const makeVerticalListOfNames = ( - list?: DataChannelEntryResponseType[], -) => list?.filter((u) => !!u.payloadJson).map((u) => { - const time = new Date(u.createdAt); - return ( -
  • - {u.payloadJson.name} - {' '} - ( - {time.getHours().toString().padStart(2, '0')} - : - {time.getMinutes().toString().padStart(2, '0')} - ) -
  • + const { users: usersToBePicked, isLoading } = useGetPossibleUsersToBePicked( + pluginApi, + filterOptions, ); -}); - -const hasChanged = (targetValue: boolean, baseValue: boolean) => (targetValue !== baseValue); -export function PickRandomUserPanelComponent(props: PickRandomUserPanelComponentProps) { - const { - pluginApi, - intl, - setShowModal, - currentUser, - } = props; - const allUsersInfo = pluginApi - .useCustomSubscription(USERS_MORE_INFORMATION); - const [includeModerators, setincludeModerators] = useState(false); - const [includePresenter, setincludePresenter] = useState(false); - const [includePickedUsers, setIncludePickedUsers] = useState(true); - const [ - initialPanelInformationAlreadySynced, - setInitialPanelInformationAlreadySynced, - ] = useState(false); - const { data: allUsers } = allUsersInfo; const { data: pickedUserFromDataChannelResponse, - pushEntry: pushPickedUser, deleteEntry: deletePickedUser, } = pluginApi.useDataChannel('pickRandomUser'); - const pickedUserFromDataChannel = { - data: pickedUserFromDataChannelResponse?.data, - loading: false, - }; - const { - data: panelInformationFromPresenter, - pushEntry: dispatchPanelInformationFromPresenter, - } = pluginApi.useDataChannel('panelInformationFromPresenter', DataChannelTypes.LATEST_ITEM); + const dataChannelPickedUsers = pickedUserFromDataChannelResponse?.data; - useEffect(() => { - const panelInformationList = panelInformationFromPresenter - .data; - if (!panelInformationList || panelInformationFromPresenter.loading) { - if (!initialPanelInformationAlreadySynced) { - // This timeout is used to handle a special case when the panel is being opened for the - // first time. In this scenario, there is no prior panel state in the data channel, causing - // the data to be undefined while the loading state is still true. This creates ambiguity, - // as it’s difficult to distinguish this situation from when the data channel is still - // actively fetching data. To resolve this, we use a timeout of 3 seconds to update the - // state and trigger a forced dispatch to send the current state to the data channel. - setTimeout( - () => setInitialPanelInformationAlreadySynced(true), - DATA_CHANNEL_LOADING_TOLERANCE_TIMEOUT, - ); - } - return; - } - const panelInformation = panelInformationList - ? panelInformationList[0]?.payloadJson : null; - const { - includeModerators: dChannelIncludeModerators = null, - includePresenter: dChannelIncludePresenter = null, - includePickedUsers: dChannelIncludePickedUsers = null, - } = panelInformation || {}; + const pickedUserWithEntryId = useGetCurrentPickedUser(dataChannelPickedUsers); - if (dChannelIncludeModerators !== null - && hasChanged(dChannelIncludeModerators, includeModerators)) { - setincludeModerators(dChannelIncludeModerators); - } - if (dChannelIncludePresenter !== null - && hasChanged(dChannelIncludePresenter, includePresenter)) { - setincludePresenter(dChannelIncludePresenter); - } - if (dChannelIncludePickedUsers !== null - && hasChanged(dChannelIncludePickedUsers, includePickedUsers)) { - setIncludePickedUsers(dChannelIncludePickedUsers); - } - if (!initialPanelInformationAlreadySynced) setInitialPanelInformationAlreadySynced(true); - }, [panelInformationFromPresenter]); - - useEffect(() => { - const panelInformationList = panelInformationFromPresenter - .data; - const panelInformation = panelInformationList - ? panelInformationList[0]?.payloadJson : null; - const { - includeModerators: dChannelIncludeModerators = null, - includePresenter: dChannelIncludePresenter = null, - includePickedUsers: dChannelIncludePickedUsers = null, - } = panelInformation || {}; - - if (currentUser?.presenter - && dispatchPanelInformationFromPresenter - && initialPanelInformationAlreadySynced) { - if (hasChanged(dChannelIncludeModerators, includeModerators) - || hasChanged(dChannelIncludePresenter, includePresenter) - || hasChanged(dChannelIncludePickedUsers, includePickedUsers)) { - dispatchPanelInformationFromPresenter({ - includeModerators, - includePresenter, - includePickedUsers, - }); - } - } - }, [ - dispatchPanelInformationFromPresenter, - includeModerators, - includePresenter, - includePickedUsers, - ]); - - const usersToBePicked: PickedUser[] = (allUsers?.user ?? []).filter((user) => !user.bot) - .filter((user) => { - let roleFilter = true; - if (!includeModerators) roleFilter = user.role === Role.VIEWER; - if (!includePickedUsers && pickedUserFromDataChannel.data) { - return roleFilter && pickedUserFromDataChannel - .data.findIndex( - (u) => u?.payloadJson?.userId === user?.userId, - ) === -1; - } - return roleFilter; - }).filter((user) => { - if (!includePresenter) return !user.presenter; - return true; - }); + return ( + + ); +} - const handlePickRandomUser = () => { - if (usersToBePicked && usersToBePicked.length > 0 && currentUser?.presenter) { - const randomIndex = Math.floor(Math.random() * usersToBePicked.length); - const randomlyPickedUser = usersToBePicked[randomIndex]; - pushPickedUser(randomlyPickedUser); - } - setShowModal(true); - }; +/** + * Content of the sidekick area registered by + * `extensible-areas/generic-content-sidekick-area/component`. + */ +export function PickRandomUserPanel(props: PickRandomUserPanelProps) { + const { pluginApi, intl, modalUiScale } = props; - const renderButton = (userRole: string) => { - if (currentUser?.presenter) { - if (usersToBePicked?.length > 0) { - return ( - - { - handlePickRandomUser(); - }} - > - { - (pickedUserFromDataChannel?.data?.length > 0) - ? intl.formatMessage(intlMessages.pickRandomUserButtonLabelAgain) - : intl.formatMessage(intlMessages.pickRandomUserButtonLabelRole, { 0: userRole }) - } - - - ); - } - return ( - -

    - {intl.formatMessage(intlMessages.noUsersAvailableWarning, { 0: userRole })} -

    -
    - ); - } - return null; - }; + const { data: currentUser } = pluginApi.useCurrentUser(); - let userRole: string; - if (!includeModerators) { - userRole = (usersToBePicked?.length !== 1) - ? intl.formatMessage(intlMessages.viewers) - : intl.formatMessage(intlMessages.viewer); - } else { - userRole = (usersToBePicked?.length !== 1) - ? intl.formatMessage(intlMessages.users) - : intl.formatMessage(intlMessages.user); - } - const hasPermissionToSeePanel = currentUser?.presenter || currentUser?.role === Role.MODERATOR; - if (!hasPermissionToSeePanel) return null; + if (!currentUser?.presenter) return null; return ( - - - {intl.formatMessage(intlMessages.options)} - - - { - setincludeModerators((value) => !value); - }} - name="options" - value="includeModerators" - inputProps={{ - 'aria-label': intl.formatMessage(intlMessages.includeModerators), - }} - /> - - {intl.formatMessage(intlMessages.includeModerators)} - - - - { - setincludePresenter((value) => !value); - }} - name="options" - value="includePresenter" - inputProps={{ - 'aria-label': intl.formatMessage(intlMessages.includePresenter), - }} - /> - - {intl.formatMessage(intlMessages.includePresenter)} - - - - { - setIncludePickedUsers((value) => !value); - }} - name="options" - value="includePickedUsers" - inputProps={{ - 'aria-label': intl.formatMessage(intlMessages.includePickedUsers), - }} - /> - - {intl.formatMessage(intlMessages.includePickedUsers)} - - - - - - - {intl.formatMessage(intlMessages.availableForSelection)} - - - {usersToBePicked?.length} - {' '} - {userRole} - : - {' '} - {makeHorizontalListOfNames(usersToBePicked)} - - - - - - {intl.formatMessage(intlMessages.previouslyPicked)} - - {currentUser?.presenter && pickedUserFromDataChannel?.data?.length > 0 && ( - { - deletePickedUser([RESET_DATA_CHANNEL]); - }} - > - {intl.formatMessage(intlMessages.clearAll)} - - )} - - -
      - { - makeVerticalListOfNames(pickedUserFromDataChannel?.data) - } -
    -
    -
    - {renderButton(userRole)} -
    + + + ); } diff --git a/src/components/pick-random-user-panel/queries.ts b/src/components/pick-random-user-panel/queries.ts deleted file mode 100644 index 1d74b56..0000000 --- a/src/components/pick-random-user-panel/queries.ts +++ /dev/null @@ -1,13 +0,0 @@ -// TODO: Replace this query with the useUsersBasicInfo once it's merged in -export const USERS_MORE_INFORMATION = ` -subscription usersMoreInformation { - user(where: { bot: { _eq: false } }) { - color - name - userId - role - presenter - bot - } -} -`; diff --git a/src/components/pick-random-user-panel/styles.ts b/src/components/pick-random-user-panel/styles.ts deleted file mode 100644 index 3199a7c..0000000 --- a/src/components/pick-random-user-panel/styles.ts +++ /dev/null @@ -1,209 +0,0 @@ -import styled from 'styled-components'; -import { Switch } from '@mui/material'; -import { styled as materialStyled } from '@mui/material/styles'; -import { - colorPrimary, - colorGray, - colorWhite, - colorHeading, -} from '../../styles/pallete'; -import { - defaultPanelPadding, - defaultPanelSubtitleFontSize, - defaultPanelTextFontSize, -} from '../../styles/general'; - -const PanelContainer = styled.div` - width: 100%; - height: 100%; - max-height: 100%; - align-items: flex-start; - display: flex; - flex-direction: column; - padding: 0 ${defaultPanelPadding} ${defaultPanelPadding}; - background-color: ${colorWhite}; - color: ${colorGray}; - gap: 1.5rem; -`; - -const PanelSubTitle = styled.p` - color: ${colorHeading}; - font-size: ${defaultPanelSubtitleFontSize}; - font-weight: 600; - margin: 0.7rem 0; -`; - -const PanelSectionWrapperGrow = styled.div` - width: 100%; - flex-grow: 1; -`; - -const PanelSectionWrapperGrowScrollable = styled.div` - width: 100%; - flex-grow: 1; - display: flex; - flex-direction: column; - min-height: 0; -`; - -const PanelSectionWrapperNotGrow = styled.div` - width: 100%; - flex-grow: 0; -`; - -const OptionsWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 0.7rem; -`; - -const OptionContainer = styled.label` - display: flex; - align-items: center; - gap: 0.5rem; -`; - -const OptionLabel = styled.label` - font-size: ${defaultPanelTextFontSize}; -`; - -const AlreadyPickedWarpper = styled.div` - display: flex; - flex-direction: row; - gap: 0.7rem; - align-items: center; -`; - -const CleanAllButton = styled.button` - padding: 1px 10px; - margin-left: 8px; - font-size: 15px; - background: #efefef; - border: none; - color: inherit; - border-radius: 8px; - cursor: pointer; - - &:hover { - background-color: #ddd; - } -`; - -const MaterialSwitch = materialStyled(Switch)(({ theme }) => ({ - width: '2.3rem', - height: '1.2rem', - padding: 0, - display: 'flex', - '&:active': { - '& .MuiSwitch-switchBase.Mui-checked': { - transform: 'translateX(1.2rem)', - }, - }, - '& .MuiSwitch-switchBase': { - padding: '0.2rem', - '&.Mui-checked': { - transform: 'translateX(1.2rem)', - color: '#fff', - '& + .MuiSwitch-track': { - opacity: 1, - backgroundColor: colorPrimary, - ...theme.applyStyles('dark', { - backgroundColor: colorPrimary, - }), - }, - }, - }, - '& .MuiSwitch-thumb': { - boxShadow: '0 2px 4px 0 rgb(0 35 11 / 20%)', - width: '0.6rem', - height: '0.6rem', - borderRadius: '0.5rem', - transition: theme.transitions.create(['width'], { - duration: 200, - }), - transform: 'translateY(1px)', - }, - '& .MuiSwitch-track': { - borderRadius: '0.6rem', - opacity: 1, - backgroundColor: 'rgba(0,0,0,.25)', - boxSizing: 'border-box', - ...theme.applyStyles('dark', { - backgroundColor: 'rgba(255,255,255,.35)', - }), - }, -})); - -const PickRandomUserButtonWrapper = styled.div` - width: 100%; - display: flex; - justify-content: center; -`; - -const PickRandomUserButton = styled.button` - background-color: ${colorPrimary}; - border: none; - border-radius: 1rem; - cursor: pointer; - font-size: 0.8rem; - color: ${colorWhite}; - padding: 1rem 1.5rem; - border-radius: 1rem; - font-weight: 600; -`; - -const ScrollboxVertical = styled.div` - overflow-y: auto; - background: linear-gradient(white 30%, rgba(255,255,255,0)), - linear-gradient(rgba(255,255,255,0), white 70%) 0 100%, - /* Shadows */ - radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.2), rgba(0,0,0,0)), - radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.2), rgba(0,0,0,0)) 0 100%; - - background-repeat: no-repeat; - background-color: transparent; - background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; - background-attachment: local, local, scroll, scroll; - - // Fancy scroll - &::-webkit-scrollbar { - width: 5px; - height: 5px; - } - &::-webkit-scrollbar-button { - width: 0; - height: 0; - } - &::-webkit-scrollbar-thumb { - background: rgba(0,0,0,.25); - border: none; - border-radius: 50px; - } - &::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,.5); } - &::-webkit-scrollbar-thumb:active { background: rgba(0,0,0,.25); } - &::-webkit-scrollbar-track { - background: rgba(0,0,0,.25); - border: none; - border-radius: 50px; - } - &::-webkit-scrollbar-track:hover { background: rgba(0,0,0,.25); } - &::-webkit-scrollbar-track:active { background: rgba(0,0,0,.25); } - &::-webkit-scrollbar-corner { background: 0 0; } -`; - -export default { - PanelContainer, - PanelSubTitle, - PanelSectionWrapperGrow, - PanelSectionWrapperGrowScrollable, - PanelSectionWrapperNotGrow, - OptionsWrapper, - OptionContainer, - OptionLabel, - AlreadyPickedWarpper, - CleanAllButton, - MaterialSwitch, - PickRandomUserButtonWrapper, - PickRandomUserButton, - ScrollboxVertical, -}; diff --git a/src/components/pick-random-user-panel/styles.tsx b/src/components/pick-random-user-panel/styles.tsx new file mode 100644 index 0000000..aefa83a --- /dev/null +++ b/src/components/pick-random-user-panel/styles.tsx @@ -0,0 +1,19 @@ +import styled from 'styled-components'; + +/** + * Root of the sidekick panel. It defines `--pru-sm`, the scale custom property that + * every `s()` helper in the shared styles reads, so the `modalUiScale` setting applies + * to the panel exactly like it does to the modal. + */ +const PanelWrapper = styled.div<{ $modalUiScale?: number }>` + --pru-sm: ${({ $modalUiScale }) => $modalUiScale ?? 1}; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 0; + color: #1C2B3A; + font-family: 'Source Sans Pro', Arial, sans-serif; +`; + +export { PanelWrapper }; diff --git a/src/components/pick-random-user-panel/types.ts b/src/components/pick-random-user-panel/types.ts index 0973d6f..a174c8f 100644 --- a/src/components/pick-random-user-panel/types.ts +++ b/src/components/pick-random-user-panel/types.ts @@ -1,15 +1,11 @@ +import { PluginApi } from 'bigbluebutton-html-plugin-sdk'; import { IntlShape } from 'react-intl'; -import { CurrentUserData, PluginApi } from 'bigbluebutton-html-plugin-sdk'; -export interface PickRandomUserPanelComponentProps { +export interface PickRandomUserPanelContentProps { pluginApi: PluginApi; intl: IntlShape; - setShowModal: (value: boolean) => void; - currentUser: CurrentUserData; } -export interface PanelInformationFromPresenter { - includeModerators: boolean; - includePresenter: boolean; - includePickedUsers: boolean; +export interface PickRandomUserPanelProps extends PickRandomUserPanelContentProps { + modalUiScale: number; } diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index f3c4cb1..12ad76b 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -1,125 +1,102 @@ import * as React from 'react'; -import { useState, useEffect, useRef } from 'react'; -import * as ReactDOM from 'react-dom/client'; +import { useState } from 'react'; -import { GenericContentSidekickArea } from 'bigbluebutton-html-plugin-sdk'; +import { BbbPluginSdk, PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import { + useControlModalState, + useGetAllSettings, + useGetCurrentPickedUser, + useRequestPermissionForNotification, +} from './hooks'; import { BotDataWrapper, PickRandomUserPluginProps, + PickedUserSeenEntryDataChannel, PickedUser, - PickedUserWithEntryId, } from './types'; -import { PickUserModal } from '../modal/component'; -import { Role } from './enums'; import { BOT_SUBSCRIPTION } from './queries'; -import { PickRandomUserPanelComponent } from '../pick-random-user-panel/component'; -import { intlMessages } from '../../intlMessages'; +import { PickUserModal } from '../modal/component'; +import GenericContentSidekickAreaManager from '../extensible-areas/generic-content-sidekick-area/component'; +import { useGetInternationalization } from '../../commons/hooks'; -const NAVIGATION_SIDEBAR_BUTTON_ICON = 'random'; +function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { + BbbPluginSdk.initialize(uuid); + const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); -function PickRandomUserPlugin({ pluginApi, intl }: PickRandomUserPluginProps) { const [showModal, setShowModal] = useState(false); - const [ - pickedUserWithEntryId, - setPickedUserWithEntryId] = useState(); - const shouldPluginUnmount = pluginApi.useShouldUnmountPlugin(); + + const settingsResponseData = pluginApi.usePluginSettings(); + + const pickRandomUserSettings = useGetAllSettings(settingsResponseData); + const { pickedUserTimeWindow, browserNotificationEnabled } = pickRandomUserSettings; + + useRequestPermissionForNotification(browserNotificationEnabled); + const currentUserInfo = pluginApi.useCurrentUser(); + const shouldUnmountPlugin = pluginApi.useShouldUnmountPlugin(); const { data: currentUser } = currentUserInfo; + const { data: botData } = pluginApi .useCustomSubscription!(BOT_SUBSCRIPTION) || {}; const isBot = botData?.user_current?.[0]?.bot || false; + const { + intl, + localeMessagesLoading, + } = useGetInternationalization(pluginApi); + const { data: pickedUserFromDataChannelResponse, - replaceEntry: updatePickedRandomUser, } = pluginApi.useDataChannel('pickRandomUser'); + const pickedUserFromDataChannel = pickedUserFromDataChannelResponse?.data; - const pickedUserFromDataChannel = { - data: pickedUserFromDataChannelResponse?.data, - loading: false, - }; - const genericContentId = useRef(''); - - useEffect(() => { - if (pickedUserFromDataChannel.data - && pickedUserFromDataChannel.data?.length > 0) { - const pickedUserToUpdate = pickedUserFromDataChannel - .data[0]; - if (pickedUserToUpdate?.entryId !== pickedUserWithEntryId?.entryId) { - setPickedUserWithEntryId({ - pickedUser: pickedUserToUpdate?.payloadJson, - entryId: pickedUserToUpdate.entryId, - }); - } - if (pickedUserToUpdate?.payloadJson && pickedUserToUpdate?.payloadJson.isPresenterViewing) { - setShowModal(true); - } - } else if (pickedUserFromDataChannel.data - && pickedUserFromDataChannel.data?.length === 0) { - setPickedUserWithEntryId(null); - if (currentUser && !currentUser.presenter) setShowModal(false); - } - }, [pickedUserFromDataChannelResponse]); - - useEffect(() => { - if (!pickedUserWithEntryId && !currentUser?.presenter) setShowModal(false); - }, [pickedUserWithEntryId]); - - const genericContentSidekickArea = { - contentFunction: (element: HTMLElement) => { - const root = ReactDOM.createRoot(element); - root.render( - - - , - ); - return root; - }, - name: intl.formatMessage(intlMessages.title), - section: '', - open: false, - buttonIcon: NAVIGATION_SIDEBAR_BUTTON_ICON, - ...(genericContentId.current && { id: genericContentId.current }), + const currentPickedUser = useGetCurrentPickedUser(pickedUserFromDataChannel); + + const { + data: pickedUserSeenEntries, + pushEntry: pushPickedUserSeen, + } = pluginApi.useDataChannel('pickedUserSeenEntry'); + + const handleCloseModal = (): void => { + setShowModal(false); }; - useEffect(() => { - if (currentUser?.role === Role.MODERATOR) { - const generatedIds = pluginApi.setGenericContentItems([ - new GenericContentSidekickArea(genericContentSidekickArea), - ]); - genericContentId.current = generatedIds.pop(); - return; - } - if (genericContentId.current) { - pluginApi.setGenericContentItems([]); - genericContentId.current = ''; - } - }, [ - intl, - genericContentSidekickArea.name, - genericContentSidekickArea.section, - genericContentSidekickArea.buttonIcon, - setShowModal, + useControlModalState( + pickedUserFromDataChannel, + pickedUserSeenEntries, currentUser, - ]); - if (!pickedUserWithEntryId) return null; - return !shouldPluginUnmount && ( - { setShowModal(false); }, - pickedUserWithEntryId, - currentUser, - updatePickedRandomUser, - }} - /> + pickedUserTimeWindow, + currentPickedUser, + setShowModal, + ); + + if (!intl || localeMessagesLoading) return null; + + return !shouldUnmountPlugin && ( + <> + + + ); } diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts new file mode 100644 index 0000000..ca90f86 --- /dev/null +++ b/src/components/pick-random-user/hooks.ts @@ -0,0 +1,211 @@ +import { + CurrentUserData, + DataChannelEntryResponseType, + GraphqlResponseWrapper, +} from 'bigbluebutton-html-plugin-sdk'; +import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; +import { useEffect, useState } from 'react'; +import { + DEFAULT_PING_SOUND_URL, + DEFAULT_PREVENT_CLOSE_DELAY_SECONDS, + DEFAULT_MODAL_UI_SCALE, + PICKED_USER_TIME_WINDOW, +} from '../../commons/constants'; +import { hasCurrentUserSeenPickedUser, isNumber } from '../../commons/utils'; +import { PickRandomUserSettings } from '../../commons/types'; +import { WindowClientSettings } from '../modal/types'; +import { + PickedUser, + PickedUserSeenEntryDataChannel, + PickedUserWithEntryId, +} from './types'; + +declare const window: WindowClientSettings; + +// Settings hooks and utilities + +const useSettingsLoaded = ( + callback: (settings: PluginSettingsData) => void, + settingsData: GraphqlResponseWrapper, +) => { + const { data: pluginSettings, loading: isPluginSettingsLoading } = settingsData; + useEffect(() => { + if (!isPluginSettingsLoading && pluginSettings) { + callback(pluginSettings); + } + }, [isPluginSettingsLoading, pluginSettings]); +}; + +export const useRequestPermissionForNotification = ( + browserNotificationEnabled: boolean, +) => { + useEffect(() => { + if (browserNotificationEnabled) { + Notification.requestPermission(); + } + }, [browserNotificationEnabled]); +}; + +export const getPingSoundEnabled = ( + settings: PluginSettingsData, + previousState: boolean, +): boolean => { + if (settings.pingSoundEnabled === undefined || settings.pingSoundEnabled === null) { + return previousState; + } return !!settings.pingSoundEnabled; +}; + +export const getBrowserNotificationEnabled = ( + settings: PluginSettingsData, + previousState: boolean, +): boolean => { + if (settings.browserNotificationEnabled === undefined + || settings.browserNotificationEnabled === null) { + return previousState; + } return !!settings.browserNotificationEnabled; +}; + +export const getPickedUserTimeWindowFromSettings = (settings: PluginSettingsData) => { + const settingTimeWindow = settings.pickedUserTimeWindow as unknown; + if (isNumber(settingTimeWindow)) { + const timeWindow: number = settingTimeWindow as number; + return timeWindow; + } return PICKED_USER_TIME_WINDOW; +}; + +export const getPingSoundUrl = (settings: PluginSettingsData): string => { + const { cdn, basename } = window.meetingClientSettings.public.app; + const host = cdn + basename; + const pingSoundUrl: string = settings.pingSoundUrl + ? String(settings.pingSoundUrl) + : `${host}/${DEFAULT_PING_SOUND_URL}`; + return pingSoundUrl; +}; + +export const getPreventCloseDelayFromSettings = (settings: PluginSettingsData) => { + const settingPreventCloseDelay = settings.preventCloseDelaySeconds as unknown; + if (isNumber(settingPreventCloseDelay)) { + const delay: number = settingPreventCloseDelay as number; + return delay; + } return DEFAULT_PREVENT_CLOSE_DELAY_SECONDS; +}; + +const getSizeMultiplierFromSettings = (settings: PluginSettingsData) => { + const settingSizeMultiplier = settings.modalUiScale as unknown; + if (isNumber(settingSizeMultiplier)) { + const multiplier: number = settingSizeMultiplier as number; + return multiplier; + } return DEFAULT_MODAL_UI_SCALE; +}; + +export const useGetAllSettings = ( + settingsData: GraphqlResponseWrapper, +): PickRandomUserSettings => { + const [pingSoundEnabled, setPingSoundEnabled] = useState(true); + const [browserNotificationEnabled, setBrowserNotificationEnabled] = useState(false); + const [pingSoundUrl, setPingSoundUrl] = useState(DEFAULT_PING_SOUND_URL); + const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState( + PICKED_USER_TIME_WINDOW, + ); + const [preventCloseDelaySeconds, setPreventCloseDelaySeconds] = useState( + DEFAULT_PREVENT_CLOSE_DELAY_SECONDS, + ); + const [modalUiScale, setModalUiScale] = useState(DEFAULT_MODAL_UI_SCALE); + useSettingsLoaded((settings) => { + setBrowserNotificationEnabled( + (previousState) => getBrowserNotificationEnabled(settings, previousState), + ); + setPingSoundEnabled( + (previousState) => getPingSoundEnabled(settings, previousState), + ); + setPickedUserTimeWindow(getPickedUserTimeWindowFromSettings(settings)); + setPingSoundUrl(getPingSoundUrl(settings)); + setPreventCloseDelaySeconds(getPreventCloseDelayFromSettings(settings)); + setModalUiScale(getSizeMultiplierFromSettings(settings)); + }, settingsData); + return { + pingSoundEnabled, + pingSoundUrl, + browserNotificationEnabled, + pickedUserTimeWindow, + preventCloseDelaySeconds, + modalUiScale, + }; +}; + +// --- + +export const useGetCurrentPickedUser = ( + pickedUserFromDataChannel: DataChannelEntryResponseType[] | undefined, +): PickedUserWithEntryId | null => { + const [ + pickedUserWithEntryId, + setPickedUserWithEntryId] = useState(null); + + useEffect(() => { + if (pickedUserFromDataChannel + && pickedUserFromDataChannel?.length > 0) { + const pickedUserToUpdate = pickedUserFromDataChannel[0]; + if (pickedUserToUpdate?.entryId !== pickedUserWithEntryId?.entryId) { + setPickedUserWithEntryId({ + pickedUser: pickedUserToUpdate?.payloadJson, + entryId: pickedUserToUpdate.entryId, + }); + } + } else if (pickedUserFromDataChannel + && pickedUserFromDataChannel?.length === 0) { + setPickedUserWithEntryId(null); + } + }, [pickedUserFromDataChannel]); + return pickedUserWithEntryId; +}; + +export const useControlModalState = ( + pickedUserFromDataChannel: DataChannelEntryResponseType[], + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentUser: CurrentUserData, + pickedUserTimeWindow: number, + currentPickedUser: PickedUserWithEntryId | null, + setShowModal: React.Dispatch>, +) => { + const hasValidPickInTimeWindow = (pickedUser?: DataChannelEntryResponseType) => { + if (!pickedUser) return false; + const secondsSincePick = (Date.now() - new Date(pickedUser.createdAt).getTime()) / 1000; + return secondsSincePick <= pickedUserTimeWindow; + }; + + const hasUserSeenPickedUser = (pickedUser?: DataChannelEntryResponseType) => { + if (!pickedUser) return false; + return hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUser?.userId, + pickedUser.payloadJson.userId, + ); + }; + + const shouldShowModal = (pickedUser?: DataChannelEntryResponseType) => { + if (!pickedUser) return false; + return ( + !hasUserSeenPickedUser(pickedUser) + && !pickedUserSeenEntries?.loading + && hasValidPickInTimeWindow(pickedUser) + ); + }; + + useEffect(() => { + const firstPick = pickedUserFromDataChannel?.[0]; + + if (firstPick && shouldShowModal(firstPick)) { + setShowModal(true); + return; + } + + const shouldCloseModal = (!firstPick && !currentUser?.presenter) + || (!currentPickedUser && !currentUser?.presenter); + + if (shouldCloseModal) { + setShowModal(false); + } + }, [currentPickedUser, pickedUserFromDataChannel, pickedUserSeenEntries, pickedUserTimeWindow]); +}; diff --git a/src/components/pick-random-user/types.ts b/src/components/pick-random-user/types.ts index d7bfa67..5165773 100644 --- a/src/components/pick-random-user/types.ts +++ b/src/components/pick-random-user/types.ts @@ -1,12 +1,9 @@ -import { PluginApi } from 'bigbluebutton-html-plugin-sdk'; -import { IntlShape } from 'react-intl'; - export interface PickedUser { - isPresenterViewing: boolean; presenter: boolean; userId: string; name: string; role: string; + avatar: string; color: string; bot: boolean; } @@ -17,30 +14,13 @@ export interface PickedUserWithEntryId { } export interface PickRandomUserPluginProps { - pluginApi: PluginApi, - intl: IntlShape, -} - -export interface UsersMoreInformationGraphqlResponse { - user: PickedUser[]; -} - -export interface DataChannelArrayMessages { - createdAt: string; - dataChannel: string; - fromUserId: string; - messageId: string; - payloadJson: T; - pluginName: string; - toRoles: string[]; -} - -export interface DataChannelPickedUserResponse { - pluginDataChannelMessage: DataChannelArrayMessages[]; + pluginName: string, + pluginUuid: string, } -export interface DataChannelLastResetTimeResponse { - pluginDataChannelMessage: DataChannelArrayMessages[]; +export interface PickedUserSeenEntryDataChannel { + pickedUserId: string; + seenByUserId: string; } export interface BotData { diff --git a/src/hooks/injectIntl.tsx b/src/hooks/injectIntl.tsx deleted file mode 100644 index 2b4aafc..0000000 --- a/src/hooks/injectIntl.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { PluginApi, IntlLocaleUiDataNames } from 'bigbluebutton-html-plugin-sdk'; -import { createIntl } from 'react-intl'; - -const localesContext = require.context('@locales', true, /\.json$/); - -export const useInjectIntl =

    (WrappedComponent: React.ComponentType

    , pluginApi: PluginApi) => function injectIntl(props: Omit) { - const currentLocale = pluginApi.useUiData(IntlLocaleUiDataNames.CURRENT_LOCALE, { - locale: 'en', - fallbackLocale: 'en', - }); - - let messages = []; - try { - messages = localesContext(`./${currentLocale.locale}.json`); - } catch { - messages = localesContext(`./${currentLocale.fallbackLocale}.json`); - } - - const intl = createIntl({ - locale: currentLocale.locale, - messages, - fallbackOnEmptyString: true, - }); - - return ; -}; diff --git a/src/index.tsx b/src/index.tsx index b5ad359..8fb60c1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,22 +1,16 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { BbbPluginSdk, PluginApi } from 'bigbluebutton-html-plugin-sdk'; import PickRandomUserPlugin from './components/pick-random-user/component'; -import { useInjectIntl } from './hooks/injectIntl'; const uuid = document.currentScript?.getAttribute('uuid') || 'root'; -function PluginInitializer({ pluginUuid }: { pluginUuid: string }): React.ReactNode { - BbbPluginSdk.initialize(pluginUuid); - const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid); - const IntlInjectedPickRandomUserPlugin = useInjectIntl(PickRandomUserPlugin, pluginApi); - - return (); -} +const pluginName = document.currentScript?.getAttribute('pluginName') || 'plugin'; const root = ReactDOM.createRoot(document.getElementById(uuid)); root.render( - - - , + , ); diff --git a/src/intlMessages.js b/src/intlMessages.js deleted file mode 100644 index e5a6d6c..0000000 --- a/src/intlMessages.js +++ /dev/null @@ -1,76 +0,0 @@ -import { defineMessages } from 'react-intl'; - -export const intlMessages = defineMessages({ - title: { - id: 'app.pickRandomUser.title', - description: 'Title for pick random user panel', - }, - options: { - id: 'app.pickRandomUser.options', - description: 'Subtitle for panel options', - }, - includeModerators: { - id: 'app.pickRandomUser.includeModerators', - description: 'Option to include moderators in the random user pick', - }, - includePresenter: { - id: 'app.pickRandomUser.includePresenter', - description: 'Option to include the presenter in the random user pick', - }, - includePickedUsers: { - id: 'app.pickRandomUser.includePickedUsers', - description: 'Option to include previously picked users in the selection', - }, - availableForSelection: { - id: 'app.pickRandomUser.availableForSelection', - description: 'Label indicating how many users are available for selection', - }, - previouslyPicked: { - id: 'app.pickRandomUser.previouslyPicked', - description: 'Label for previously picked users', - }, - clearAll: { - id: 'app.pickRandomUser.clearAll', - description: 'Button label to clear all selected users', - }, - noUsersAvailableWarning: { - id: 'app.pickRandomUser.noUsersAvailableWarning', - description: 'Warning message when no users are available for selection', - }, - pickRandomUserButtonLabelAgain: { - id: 'app.pickRandomUser.pickRandomUserButtonLabelAgain', - description: 'Button label to pick a random user again', - }, - pickRandomUserButtonLabelRole: { - id: 'app.pickRandomUser.pickRandomUserButtonLabelRole', - description: 'Button label to pick a random user by their role', - }, - viewer: { - id: 'app.pickRandomUser.viewer', - description: 'Singular term for a viewer in the panel', - }, - viewers: { - id: 'app.pickRandomUser.viewers', - description: 'Plural term for viewers in the panel', - }, - user: { - id: 'app.pickRandomUser.user', - description: 'Singular term for a user in the panel', - }, - users: { - id: 'app.pickRandomUser.users', - description: 'Plural term for users in the panel', - }, - youWerePicked: { - id: 'app.pickRandomUser.youWerePicked', - description: 'Message indicating you have been randomly picked', - }, - pickedUser: { - id: 'app.pickRandomUser.pickedUser', - description: 'Message indicating the user who has been randomly picked', - }, - closeButton: { - id: 'app.pickRandomUser.modal.closeButton', - description: 'Label for the colse button in the picked user modal', - }, -}); diff --git a/src/styles/general.ts b/src/styles/general.ts deleted file mode 100644 index e0765e6..0000000 --- a/src/styles/general.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const defaultPanelPadding = '1.5rem'; -export const defaultPanelSubtitleFontSize = '0.9rem'; -export const defaultPanelTextFontSize = '0.875rem'; diff --git a/src/styles/pallete.ts b/src/styles/pallete.ts deleted file mode 100644 index 00750e1..0000000 --- a/src/styles/pallete.ts +++ /dev/null @@ -1,27 +0,0 @@ -const colorWhite = 'var(--color-white, #FFF)'; -const colorPrimary = 'var(--color-primary, #0F70D7)'; -const colorGrayIcons = 'var(--color-gray-icons, #909CAF)'; -const colorGray = 'var(--color-gray, #4E5A66)'; -const colorGrayLightest = 'var(--color-gray-lightest, #D4D9DF)'; -const colorGrayDark = 'var(--color-gray-dark, #06172A)'; -const colorDanger = 'var(--color-danger, #DF2721)'; -const colorText = `var(--color-text, ${colorGray})`; -const colorHeading = `var(--color-heading, ${colorGrayDark})`; -const colorUserModerator = 'var(--color-user-moderator, #7B209F)'; -const colorUserYou = 'var(--color-user-you, #19237C)'; -const colorUserViewer = 'var(--color-user-viewer, #2296C9)'; - -export { - colorWhite, - colorPrimary, - colorGrayIcons, - colorGray, - colorGrayLightest, - colorGrayDark, - colorDanger, - colorText, - colorHeading, - colorUserModerator, - colorUserYou, - colorUserViewer, -}; diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..a9ea9be --- /dev/null +++ b/tests/README.md @@ -0,0 +1,138 @@ +# Pick Random User Plugin – Automated Tests + +End-to-end tests for the **Pick Random User Plugin** written with [Playwright](https://playwright.dev/). +The test infrastructure lives entirely inside `tests/core/`. + +--- + +## How the plugin is reached (BigBlueButton 4.0) + +This branch targets BigBlueButton 4.0, which has no actions-button dropdown. The presenter +view is the plugin's **sidekick panel**, opened from the **apps gallery** in the navigation +sidebar; the modal only ever shows the picked user and opens by itself after a pick, for +presenter and viewers alike. + +Consequences for the tests: + +- `openPickRandomUserPanel()` (in `tests/behavioral/helpers.ts`) replaces the old + "open the modal from the actions dropdown" helper. It is idempotent — the panel is a + sidebar and stays open across tests in a suite. +- `closePickedUserModal()` replaces `goBackToPresenterView()`. "Back" dismisses the modal; + the panel behind it was never unmounted. +- The apps gallery entry is found through `apps_gallery_item_pickRandomUser`. The client + builds that attribute from the `dataTest` passed to `GenericContentSidekickArea` in + `src/components/extensible-areas/generic-content-sidekick-area/component.tsx`, so the + suite never has to match on the translated panel name. + +--- + +## Test scenarios + +### Structural (`tests/structural/test.spec.ts`) + +Verify that the plugin renders the correct elements with the correct labels and +initial state. + +### Behavioural – single user (`tests/behavioral/single-user.spec.ts`) + +Verify the pick-and-track workflow using only the moderator/presenter. + +### Behavioural – multi-user (`tests/behavioral/multi-user.spec.ts`) + +Verify real-time data-channel synchronization between the presenter and an +attendee in the same meeting. + +--- + +## How to run the tests + +### 1 – Install dependencies + +From the **plugin root** (`plugin-pick-random-user/`): + +```bash +npm install +npx playwright install --with-deps chromium +``` + +### 2 – Configure environment variables + +```bash +cp .env.template .env +# edit .env and set BBB_URL and BBB_SECRET +``` + +| Variable | Required | Description | +|----------|----------|-------------| +| `BBB_URL` | **yes** | Full API URL, e.g. `https://bbb.example.com/bigbluebutton/` | +| `BBB_SECRET` | **yes** | Shared secret of the BBB server | +| `PICK_RANDOM_USER_PLUGIN_URL` | no | Direct URL to `manifest.json`; auto-detected from server otherwise | +| `LOCAL_CONTAINER_NAME` | no | Docker container name for the local deployment script | +| `TIMEOUT_MULTIPLIER` | no | Multiply all timeouts (default 1 locally, 2 in CI) | +| `CI` | no | `"true"` enables CI reporter and single-worker mode | +| `TEST_MEETINGS` | no | Set to `"isolated"` to give each test its own meeting (see [Meeting isolation](#meeting-isolation)) | + +### 3 – Build and deploy the plugin + +To build the plugin use the command: + +```bash +npm run build-bundle +``` + +Then you need to deploy the plugin serve it so that the target BBB server can access it. You can deploy it to a S3-like environment or to your own server that you are targeting the test to. For the second option, we have a command to do that already: + +```bash +npm run publish-plugin:dev +``` + +--- + +## Meeting isolation + +By default every test suite (`test.describe` block) shares **one BBB meeting** for all its tests — the meeting is created once in `beforeAll` and torn down in `afterAll`. This is fast (~3 meetings total) but tests within a suite depend on the cleanup performed between them. + +Setting `TEST_MEETINGS=isolated` switches every suite to **one meeting per test**: the meeting is created in `beforeEach` and destroyed in `afterEach`. Tests become fully independent and can run in parallel, at the cost of more meeting setups (~15 meetings total). + +| Mode | Meetings created | Tests run | When to use | +|------|-----------------|-----------|-------------| +| default (`npm test`) | ~3 (one per suite) | serially within each suite | Normal development | +| isolated (`npm run test:isolated`) | ~1 per test | can run in parallel | Debugging flaky state, CI full isolation | + +--- + +## Running the tests + +```bash +# All suites – shared meetings (default) +npm test + +# All suites – one meeting per test +npm run test:isolated + +# Only structural tests +npm test -- tests/structural + +# Only behavioural tests +npm test -- tests/behavioral + +# Only multi-user behavioural tests +npm test -- tests/behavioral/multi-user + +# A single test by name +npm test -- -g "should show the same picked user name" + +# View the HTML report after a run +npx playwright show-report +``` + +--- + +## Test output + +| Artifact | Location | +|----------|----------| +| HTML report | `playwright-report/index.html` | +| Traces | Attached to every test in the HTML report | +| Screenshots | Captured for every test | +| Video | Every test locally; failure-only in CI | diff --git a/tests/behavioral/fixtures.ts b/tests/behavioral/fixtures.ts new file mode 100644 index 0000000..d646a30 --- /dev/null +++ b/tests/behavioral/fixtures.ts @@ -0,0 +1,92 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { test as base } from '@playwright/test'; +import { Plugin } from '../core/plugin'; +import { SessionPage } from '../core/sessionPage'; +import { + encodeCustomParams, + getJoinURL, + generateSettingsData, +} from '../core/helpers'; +import { ELEMENT_WAIT_EXTRA_LONG_TIME } from '../core/constants'; + +export interface MultiUserTestFixtures { + multiUserTest: { + modPage: SessionPage; + attendeePage: SessionPage; + }; +} + +export type MultiUserTestConfig = { + envVarName: string; + getPluginUrl?: () => string | undefined; +}; + +/** + * Creates a Playwright test instance whose `multiUserTest` fixture spins up + * two independent browser contexts: one moderator (also presenter) and one + * attendee, both joined into the same BBB meeting with the plugin active. + */ +export function createMultiUserTest(config: MultiUserTestConfig) { + let pluginUrl: string | undefined = process.env[config.envVarName]; + + const test = base.extend({ + multiUserTest: async ({ browser }, use) => { + const customUrl = config.getPluginUrl?.() || pluginUrl; + if (!customUrl) { + throw new Error( + `Plugin URL is not set. Set ${config.envVarName} or ensure beforeAll ran successfully.`, + ); + } + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: customUrl }])}`, + ); + + // ── Moderator / presenter ──────────────────────────────────────────── + const modContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const modRawPage = await modContext.newPage(); + const sample = new Plugin({ browser, context: modContext }); + await sample.initModPage(modRawPage, { createParameter }); + const { modPage } = sample; + + // ── Attendee (viewer) ──────────────────────────────────────────────── + // Joins the same meeting already created by the moderator. + const attendeeContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const attendeeRawPage = await attendeeContext.newPage(); + const attendeePage = new SessionPage({ browser, page: attendeeRawPage }); + + const joinUrl = getJoinURL({ + meetingID: modPage.meetingId, + isModerator: false, + skipSessionDetailsModal: true, + }); + await attendeeRawPage.goto(joinUrl); + await attendeeRawPage.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + attendeePage.settings = await generateSettingsData(attendeeRawPage); + if (attendeePage.settings?.autoJoinAudioModal) { + await attendeePage.closeAudioModal(); + } + await attendeeRawPage.addStyleTag({ + content: "body { font-family: 'Liberation Sans', Arial, sans-serif; }", + }); + attendeePage.meetingId = modPage.meetingId; + + await use({ modPage, attendeePage }); + + await modContext.close(); + await attendeeContext.close(); + }, + }); + + return { + test, + setPluginUrl: (url: string) => { pluginUrl = url; }, + getPluginUrl: () => pluginUrl, + }; +} diff --git a/tests/behavioral/helpers.ts b/tests/behavioral/helpers.ts new file mode 100644 index 0000000..f97aa70 --- /dev/null +++ b/tests/behavioral/helpers.ts @@ -0,0 +1,132 @@ +import { test } from '@playwright/test'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as Page } from '../core/sessionPage'; + +/** + * Click a chip and verify the underlying checkbox reaches checked state, retrying once. + * @param clickSelector - the visible chip label to click + * @param checkSelector - the hidden input whose checked state to assert (defaults to clickSelector) + */ +export async function clickToggleOnWithRetry( + page: Page, + clickSelector: string, + description: string, + checkSelector: string = clickSelector, +): Promise { + const checkLocator = page.page.locator(checkSelector); + await page.page.click(clickSelector); + try { + await test.expect(checkLocator, `${description} toggle should be on`).toBeChecked({ timeout: ELEMENT_WAIT_LONGER_TIME }); + } catch { + test.info().annotations.push({ + type: 'toggle-retry', + description: `"${description}" toggle didn't register on first click and was retried`, + }); + await page.page.click(clickSelector); + await test.expect(checkLocator, `${description} toggle should be on (retry)`).toBeChecked({ timeout: ELEMENT_WAIT_LONGER_TIME }); + } +} + +/** + * Open the presenter view. + * + * On BigBlueButton 4.0 the presenter view is the plugin's sidekick panel, reached through + * the apps gallery: there is no actions-button dropdown to open a modal from. The panel is + * a sidebar and stays open between tests, so this is idempotent. + */ +export async function openPickRandomUserPanel(modPage: Page): Promise { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + + if (await modPage.page.locator(e.pickRandomUserPanel).isVisible()) return; + + // The sidebar button toggles the gallery, so only click it when the gallery is not + // already showing — a previous test may have left it open. + if (!await modPage.page.locator(e.pickRandomUserAppsGalleryItem).isVisible()) { + await modPage.page.click(e.appsGallerySidebarButton); + } + await modPage.hasElement( + e.pickRandomUserAppsGalleryItem, + 'plugin should be listed in the apps gallery', + ELEMENT_WAIT_LONGER_TIME, + ); + await modPage.page.click(e.pickRandomUserAppsGalleryItem); + await modPage.hasElement( + e.pickRandomUserPanel, + 'sidekick panel should render after opening the plugin from the apps gallery', + ELEMENT_WAIT_LONGER_TIME, + ); +} + +/** + * Dismiss the picked-user modal via its "back" button. + * + * On v0.0.x "back" returns to the presenter view inside the modal. Here the presenter view + * is the panel behind the modal, so "back" just closes the modal and the panel is revealed + * again — unchanged, since it was never unmounted. + */ +export async function closePickedUserModal(modPage: Page): Promise { + await modPage.hasElement(e.pickRandomUserBackButton, 'back button should be visible'); + await modPage.page.click(e.pickRandomUserBackButton); + await modPage.wasRemoved( + e.pickRandomUserPickedUserViewTitle, + 'picked-user modal should close after clicking back', + ELEMENT_WAIT_TIME, + ); + await modPage.hasElement( + e.includeModeratorsChip, + 'presenter panel should still be shown once the modal is closed', + ELEMENT_WAIT_TIME, + ); +} + +export async function moderatorCleanupAfterTest(modPage: Page): Promise { + // Close the picked-user modal if a pick left it open. + const modCloseBtn = modPage.page.locator(e.pickRandomUserModalCloseButton); + if (await modCloseBtn.isVisible()) { + await modCloseBtn.click(); + await modPage.wasRemoved( + e.pickRandomUserPickedUserViewTitle, + 'picked-user modal should close during cleanup', + ELEMENT_WAIT_TIME, + ); + } + + // The panel stays mounted between tests, so reset whatever the test changed in it. + if (!await modPage.page.locator(e.pickRandomUserPanel).isVisible()) return; + + // Clear the previously-picked history first, and wait for the data channel round-trip + // to land. Without the wait the next test can re-enable its filters against a pool that + // still excludes the already-picked user, and find no pick button. + const clearBtn = modPage.page.locator(e.pickRandomUserClearAllButton); + if (await clearBtn.isVisible()) { + await clearBtn.click(); + // Counted rather than asserted through wasRemoved(): toBeHidden() raises a strict-mode + // violation as soon as the list holds more than one entry. + await test.expect + .poll( + async () => modPage.page.locator(`${e.pickRandomUserPreviouslyPickedList} li`).count(), + { timeout: ELEMENT_WAIT_LONGER_TIME, message: 'previously-picked list should be empty after cleanup' }, + ) + .toBe(0); + } + + const includePickedUsers = modPage.page.locator(e.includePickedUsersCheckbox); + if (await includePickedUsers.isChecked()) await modPage.page.click(e.includePickedUsersChip); + const includeModerators = modPage.page.locator(e.includeModeratorsCheckbox); + if (await includeModerators.isChecked()) await modPage.page.click(e.includeModeratorsChip); + const includePresenter = modPage.page.locator(e.includePresenterCheckbox); + if (await includePresenter.isChecked()) await modPage.page.click(e.includePresenterChip); +} + +export async function attendeeCleanupAfterTest(attendeePage: Page): Promise { + const attendeeCloseBtn = attendeePage.page.locator(e.pickRandomUserModalCloseButton); + if (await attendeeCloseBtn.isVisible()) { + await attendeeCloseBtn.click(); + await attendeePage.wasRemoved( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should close during cleanup', + ELEMENT_WAIT_TIME, + ); + } +} diff --git a/tests/behavioral/multi-user.spec.ts b/tests/behavioral/multi-user.spec.ts new file mode 100644 index 0000000..e09be96 --- /dev/null +++ b/tests/behavioral/multi-user.spec.ts @@ -0,0 +1,423 @@ +/** + * Behavioural tests – multi-user (moderator/presenter + attendee/viewer). + */ +import { + test, BrowserContext, Browser, APIRequestContext, TestInfo, +} from '@playwright/test'; +import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME, ELEMENT_WAIT_EXTRA_LONG_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as Page } from '../core/sessionPage'; +import { Plugin } from '../core/plugin'; +import { + encodeCustomParams, + getJoinURL, + generateSettingsData, +} from '../core/helpers'; +import { + attendeeCleanupAfterTest, + closePickedUserModal, + moderatorCleanupAfterTest, + openPickRandomUserPanel, +} from './helpers'; + +const PLUGIN_NAME = 'pick-random-user-plugin'; +const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; + +let pluginUrl: string | undefined = process.env[ENV_VAR_NAME]; +const setPluginUrl = (url: string) => { pluginUrl = url; }; +const getPluginUrl = () => pluginUrl; + +/** + * Wait for the attendee's page to finish loading the whiteboard. + * This ensures the attendee is fully in the meeting before the presenter picks. + */ +async function waitForAttendeeMeeting(attendeePage: Page): Promise { + await attendeePage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); +} + +/** + * Reset state after each test for both pages: + */ +async function cleanupAfterTest(modPage: Page, attendeePage: Page): Promise { + await attendeeCleanupAfterTest(attendeePage); + await moderatorCleanupAfterTest(modPage); +} + +const ISOLATED = process.env.TEST_MEETINGS === 'isolated'; + +// ── Tests ───────────────────────────────────────────────────────────────────── +test.describe('Pick Random User Plugin - Behavioural (multi-user)', () => { + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); + + let modPage: Page; + let attendeePage: Page; + let modContext: BrowserContext; + let attendeeContext: BrowserContext; + + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, + ); + + // ── Moderator / presenter ────────────────────────────────────────────── + modContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const modRawPage = await modContext.newPage(); + const sample = new Plugin({ browser, context: modContext }); + await sample.initModPage(modRawPage, { createParameter }); + modPage = sample.modPage; + + // ── Attendee (viewer) ────────────────────────────────────────────────── + attendeeContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const attendeeRawPage = await attendeeContext.newPage(); + attendeePage = new Page({ browser, page: attendeeRawPage }); + + const joinUrl = getJoinURL({ + meetingID: modPage.meetingId, + isModerator: false, + skipSessionDetailsModal: true, + }); + await attendeeRawPage.goto(joinUrl); + await attendeeRawPage.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + attendeePage.settings = await generateSettingsData(attendeeRawPage); + if (attendeePage.settings?.autoJoinAudioModal) { + await attendeePage.closeAudioModal(); + } + await attendeeRawPage.addStyleTag({ + content: "body { font-family: 'Liberation Sans', Arial, sans-serif; }", + }); + attendeePage.meetingId = modPage.meetingId; + } + + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + test.setTimeout(120000); + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + // Two users joining a fresh meeting does not reliably fit in the default budget. + test.setTimeout(120000); + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); + test.afterEach(async () => { + if (modPage && attendeePage) await cleanupAfterTest(modPage, attendeePage); + }); + } + + test('should show the same picked user name on both the presenter page and the attendee page', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openPickRandomUserPanel(modPage); + + // With default filters the attendee is the only eligible viewer. + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should be visible: there is 1 eligible viewer (the attendee)', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Pick the user. + await modPage.page.click(e.pickRandomUserPickButton); + + // Presenter page: the picked-user modal opens on top of the panel. + await modPage.hasElement( + e.pickRandomUserPickedUserName, + 'presenter page should show the picked user name', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Attendee page: modal opens automatically. + await attendeePage.hasElement( + e.pickRandomUserPickedUserName, + 'attendee page should show the same picked user name (data channel sync)', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Both pages must display the exact same name text. + const pickedNamePresenter = await modPage.getLocator( + e.pickRandomUserPickedUserName, + ).textContent(); + const pickedNameAttendee = await attendeePage + .getLocator(e.pickRandomUserPickedUserName).textContent(); + + test.expect(pickedNamePresenter, 'picked user name on presenter page should not be empty').toBeTruthy(); + test.expect( + pickedNameAttendee, + 'picked user name shown to attendee must match the name shown to the presenter', + ).toBe(pickedNamePresenter); + }); + + test('should open the attendee modal automatically without any action on the attendee side', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openPickRandomUserPanel(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + + // Pick – the attendee has not interacted with their page at all. + await modPage.page.click(e.pickRandomUserPickButton); + + // The attendee's modal should open driven purely by the data-channel push. + await attendeePage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should open automatically via data channel after the presenter picks', + ELEMENT_WAIT_LONGER_TIME, + ); + }); + + test('should show the "Result" section label to both the picked attendee and the presenter', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openPickRandomUserPanel(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + await modPage.page.click(e.pickRandomUserPickButton); + + await attendeePage.hasElement(e.pickRandomUserPickedUserViewTitle, 'attendee modal should open', ELEMENT_WAIT_LONGER_TIME); + await attendeePage.hasText( + e.pickRandomUserPickedUserViewTitle, + 'Result', + 'picked attendee should see the "Result" section label', + ); + + await modPage.hasElement(e.pickRandomUserPickedUserViewTitle, 'presenter should see the picked-user modal', ELEMENT_WAIT_LONGER_TIME); + await modPage.hasText( + e.pickRandomUserPickedUserViewTitle, + 'Result', + 'presenter should see the "Result" section label', + ); + }); + + test('should keep the previously-picked viewer in the available pool and re-pick the same user when "include already picked users" is enabled', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openPickRandomUserPanel(modPage); + + // Enable "Include already picked users" so the viewer stays eligible after being picked. + await modPage.page.click(e.includePickedUsersChip); + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should be visible once the attendee is an eligible viewer', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Pick the attendee (the only eligible viewer with default filters). + await modPage.page.click(e.pickRandomUserPickButton); + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'presenter should see the picked-user modal', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Capture the name of the first picked user for later comparison. + const firstPickedName = await modPage.getLocator(e.pickRandomUserPickedUserName).textContent(); + test.expect(firstPickedName, 'first picked user name should not be empty').toBeTruthy(); + + // Dismiss the picked-user modal; the presenter panel is behind it, untouched. + await closePickedUserModal(modPage); + + // ── Assertion 1: attendee appears in the "Previously picked" list ──────── + const pickedList = modPage.getLocator(`${e.pickRandomUserPreviouslyPickedList} li`); + await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + const pickedListCount = await pickedList.count(); + test.expect( + pickedListCount, + 'previously-picked list must contain at least one entry – the attendee was picked', + ).toBeGreaterThanOrEqual(1); + + // ── Assertion 2: picked user is still in the available-for-selection pool ─ + // With includePickedUsers=true the attendee is not removed from the eligible pool. + await modPage.hasText( + e.pickRandomUserAvailableContent, + '1 viewer', + 'available section should still list the attendee as eligible (includePickedUsers is ON)', + ); + await modPage.hasText( + e.pickRandomUserPickButton, + 'Pick next random user', + 'pick button should read "Pick next random user" because a user has already been picked and includePickedUsers is ON', + ); + + // ── Assertion 3: re-picking selects the same (and only) eligible user ───── + await modPage.page.click(e.pickRandomUserPickButton); + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'presenter should see the picked-user modal after re-picking', + ELEMENT_WAIT_LONGER_TIME, + ); + const secondPickedName = await modPage.getLocator(e.pickRandomUserPickedUserName).textContent(); + test.expect( + secondPickedName, + 'the same user must be picked again - they are the only eligible viewer in the pool', + ).toBe(firstPickedName); + }); +}); + +// ── Countdown / close-prevention tests ──────────────────────────────────────── +// These tests require preventCloseDelaySeconds to exceed the source-code threshold +// (MIN_PREVENT_CLOSE_DELAY_FOR_TOAST_SECONDS = 2 s) so the countdown toast appears, +// and to be long enough that the countdown is still active after the data-channel +// sync delay. The setting is injected at meeting-creation time via a +// clientSettingsOverride JSON hosted at a publicly reachable URL (set via env var). +// +// NOTE: the BBB server must be able to fetch PREVENT_CLOSE_DELAY_SETTINGS_URL. +// A URL pointing to a private/Docker IP will be rejected by BBB's URL validator. +// Use a public gist (or any public HTTPS host) and set PREVENT_CLOSE_DELAY_SETTINGS_URL +// in .env. Until then these tests are marked fixme and skipped. +test.describe('Pick Random User Plugin - Behavioural (countdown and close-prevention, multi-user)', () => { + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); + + const SETTINGS_OVERRIDE_URL = process.env.PREVENT_CLOSE_DELAY_SETTINGS_URL ?? ''; + + let modPage: Page; + let attendeePage: Page; + let modContext: BrowserContext; + let attendeeContext: BrowserContext; + + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const pluginManifestsParam = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, + ); + const createParameter = SETTINGS_OVERRIDE_URL + ? `${pluginManifestsParam}&clientSettingsOverrideJsonUrl=${encodeURIComponent(SETTINGS_OVERRIDE_URL)}` + : pluginManifestsParam; + + modContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const modRawPage = await modContext.newPage(); + const sample = new Plugin({ browser, context: modContext }); + await sample.initModPage(modRawPage, { createParameter }); + modPage = sample.modPage; + + attendeeContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const attendeeRawPage = await attendeeContext.newPage(); + attendeePage = new Page({ browser, page: attendeeRawPage }); + + const joinUrl = getJoinURL({ + meetingID: modPage.meetingId, + isModerator: false, + skipSessionDetailsModal: true, + }); + await attendeeRawPage.goto(joinUrl); + await attendeeRawPage.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + attendeePage.settings = await generateSettingsData(attendeeRawPage); + if (attendeePage.settings?.autoJoinAudioModal) { + await attendeePage.closeAudioModal(); + } + await attendeeRawPage.addStyleTag({ + content: "body { font-family: 'Liberation Sans', Arial, sans-serif; }", + }); + attendeePage.meetingId = modPage.meetingId; + } + + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + test.setTimeout(120000); + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + // Two users joining a fresh meeting does not reliably fit in the default budget. + test.setTimeout(120000); + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); + test.afterEach(async () => { + if (modPage && attendeePage) await cleanupAfterTest(modPage, attendeePage); + }); + } + + test('should show a countdown message to both users during the prevent-close delay', async (): Promise => { + test.skip(!SETTINGS_OVERRIDE_URL, 'Set PREVENT_CLOSE_DELAY_SETTINGS_URL in .env to enable this test'); + await waitForAttendeeMeeting(attendeePage); + await openPickRandomUserPanel(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + await modPage.page.click(e.pickRandomUserPickButton); + + await modPage.hasElement(e.pickRandomUserPickedUserViewTitle, 'presenter modal should open', ELEMENT_WAIT_LONGER_TIME); + await attendeePage.hasElement(e.pickRandomUserPickedUserViewTitle, 'attendee view should open', ELEMENT_WAIT_LONGER_TIME); + + const attendeeCountdown = attendeePage.getLocator(e.pickRandomUserCountDownMessage); + await test.expect( + attendeeCountdown, + 'attendee should see the countdown message during the prevent-close delay', + ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); + + const presenterCountdown = modPage.getLocator(e.pickRandomUserCountDownMessage); + await test.expect( + presenterCountdown, + 'presenter should see the countdown message during the prevent-close delay', + ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); + }); + + test('should not close the picked attendee modal when clicking the overlay during the countdown lock period', async (): Promise => { + test.skip(!SETTINGS_OVERRIDE_URL, 'Set PREVENT_CLOSE_DELAY_SETTINGS_URL in .env to enable this test'); + await waitForAttendeeMeeting(attendeePage); + await openPickRandomUserPanel(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + + // Pick the attendee – the countdown lock starts on the attendee side immediately. + await modPage.page.click(e.pickRandomUserPickButton); + + // Wait for the attendee's modal to open via data-channel sync. + await attendeePage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should open after being picked', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Click the modal overlay at the top-left corner (well outside the centred modal + // content) while the countdown is still active (shouldCloseOnOverlayClick === false). + await attendeePage.page.locator(e.pickRandomUserModalOverlay).click({ + position: { x: 5, y: 5 }, + force: true, + }); + + // The modal must still be open – the overlay click should have been swallowed. + await test.expect( + attendeePage.getLocator(e.pickRandomUserPickedUserViewTitle), + 'modal should remain open after clicking the overlay during the countdown lock period', + ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); + }); +}); diff --git a/tests/behavioral/single-user.spec.ts b/tests/behavioral/single-user.spec.ts new file mode 100644 index 0000000..b9e1263 --- /dev/null +++ b/tests/behavioral/single-user.spec.ts @@ -0,0 +1,212 @@ +/** + * Behavioural tests – single user (moderator / presenter only). + */ +import { + test, BrowserContext, Browser, APIRequestContext, TestInfo, +} from '@playwright/test'; +import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as ModPage } from '../core/sessionPage'; +import { Plugin } from '../core/plugin'; +import { encodeCustomParams } from '../core/helpers'; +import { + closePickedUserModal, openPickRandomUserPanel, moderatorCleanupAfterTest, clickToggleOnWithRetry, +} from './helpers'; + +const PLUGIN_NAME = 'pick-random-user-plugin'; +const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; + +let pluginUrl: string | undefined = process.env[ENV_VAR_NAME]; +const setPluginUrl = (url: string) => { pluginUrl = url; }; +const getPluginUrl = () => pluginUrl; + +/** Enable both inclusion filters so the single moderator/presenter is eligible. */ +async function enableInclusionFilters(modPage: ModPage): Promise { + await clickToggleOnWithRetry(modPage, e.includeModeratorsChip, 'includeModerators', e.includeModeratorsCheckbox); + await clickToggleOnWithRetry(modPage, e.includePresenterChip, 'includePresenter', e.includePresenterCheckbox); +} + +/** Enable all three filters (include picked users too, so "Pick again" is reachable). */ +async function enableAllFilters(modPage: ModPage): Promise { + await clickToggleOnWithRetry(modPage, e.includeModeratorsChip, 'includeModerators', e.includeModeratorsCheckbox); + await clickToggleOnWithRetry(modPage, e.includePresenterChip, 'includePresenter', e.includePresenterCheckbox); + await clickToggleOnWithRetry(modPage, e.includePickedUsersChip, 'includePickedUsers', e.includePickedUsersCheckbox); +} + +/** Pick a user and wait for the picked-user modal to open on top of the panel. */ +async function pickUser(modPage: ModPage): Promise { + await modPage.page.click(e.pickRandomUserPickButton); + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'picked-user modal should open after clicking pick', + ELEMENT_WAIT_LONGER_TIME, + ); +} + +async function cleanupAfterTest(modPage: ModPage) { + await moderatorCleanupAfterTest(modPage); +} + +const ISOLATED = process.env.TEST_MEETINGS === 'isolated'; + +// ── Tests ───────────────────────────────────────────────────────────────────── +test.describe('Pick Random User Plugin - Behavioural (single user)', () => { + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); + + let modPage: ModPage; + let sharedContext: BrowserContext; + + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, + ); + sharedContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const page = await sharedContext.newPage(); + const plugin = new Plugin({ browser, context: sharedContext }); + await plugin.initModPage(page, { createParameter }); + modPage = plugin.modPage; + } + + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await sharedContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await sharedContext?.close(); + }); + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + } + + test('should show "Pick next random user" button (not "Pick random user") after closing the picked-user modal when includePickedUsers is enabled', async (): Promise => { + // With "Include already picked users" ON the presenter stays in the pool + // after being picked, so the pick button in the panel keeps its place. + await openPickRandomUserPanel(modPage); + await enableAllFilters(modPage); + await pickUser(modPage); + await closePickedUserModal(modPage); + + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should still be visible (includePickedUsers is enabled)', + ); + await modPage.hasText( + e.pickRandomUserPickButton, + 'Pick next random user', + 'button label should read "Pick next random user" after a user has already been picked (includePickedUsers is enabled)', + ); + }); + + test('should list the picked user in the "Previously picked" section after picking', async (): Promise => { + await openPickRandomUserPanel(modPage); + await enableAllFilters(modPage); + await pickUser(modPage); + await closePickedUserModal(modPage); + + // The "Previously picked"

      should contain at least one
    • entry. + const pickedList = modPage.getLocator(`${e.pickRandomUserPreviouslyPickedList} li`); + await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + const count = await pickedList.count(); + test.expect(count, 'previously-picked list should have at least one entry after picking').toBeGreaterThanOrEqual(1); + }); + + test('should empty the "Previously picked" list when "Clear All" is clicked', async (): Promise => { + await openPickRandomUserPanel(modPage); + await enableAllFilters(modPage); + await pickUser(modPage); + await closePickedUserModal(modPage); + + // Confirm there is at least one entry before clearing. + const pickedList = modPage.getLocator(`${e.pickRandomUserPreviouslyPickedList} li`); + await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + + // Click Clear All. + await modPage.page.click(e.pickRandomUserClearAllButton); + + // The list should now be empty. + await modPage.wasRemoved( + `${e.pickRandomUserPreviouslyPickedList} li`, + '"Previously picked" list should be empty after clicking "Clear All"', + ELEMENT_WAIT_LONGER_TIME, + ); + }); + + test('should drop available count to 0 and show "no users" warning after picking with "Include already picked users" unchecked', async (): Promise => { + // With includePickedUsers=false (default), a picked user is immediately + // removed from the eligible pool after being selected. + await openPickRandomUserPanel(modPage); + await enableInclusionFilters(modPage); // does NOT enable includePickedUsers + + // Verify 1 user is available before picking. + await modPage.hasText( + e.pickRandomUserAvailableContent, + '1 user', + '"1 user" should be displayed before picking', + ); + + await pickUser(modPage); + await closePickedUserModal(modPage); + + // After picking, the presenter is now in the picked list and excluded. + await modPage.hasElement( + e.pickRandomUserNoUsersWarning, + 'no-users warning should appear: the only user was already picked', + ); + await modPage.wasRemoved( + e.pickRandomUserPickButton, + 'pick button should be hidden when there are 0 eligible users', + ELEMENT_WAIT_TIME, + ); + }); + + test('should restore the pick button after "Clear All" resets the picked-user history', async (): Promise => { + // Scenario: pick once (user excluded) → warning shown → Clear All → user eligible again. + await openPickRandomUserPanel(modPage); + await enableInclusionFilters(modPage); // includePickedUsers stays OFF + await pickUser(modPage); + await closePickedUserModal(modPage); + + // No-users warning is shown (picked user excluded). + await modPage.hasElement( + e.pickRandomUserNoUsersWarning, + 'no-users warning should be visible before Clear All', + ); + + // Clear the pick history – the user is no longer in the "picked" list. + await modPage.page.click(e.pickRandomUserClearAllButton); + + // The user is now eligible again, so the pick button should reappear. + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should reappear after clearing the pick history', + ELEMENT_WAIT_LONGER_TIME, + ); + await modPage.wasRemoved( + e.pickRandomUserNoUsersWarning, + 'no-users warning should disappear once the pick button is available again', + ELEMENT_WAIT_TIME, + ); + }); +}); diff --git a/tests/core/constants.ts b/tests/core/constants.ts new file mode 100644 index 0000000..5df6056 --- /dev/null +++ b/tests/core/constants.ts @@ -0,0 +1,14 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ path: path.resolve(__dirname, '../../.env') }); + +export const CI = process.env.CI === 'true'; +const TIMEOUT_MULTIPLIER = Number(process.env.TIMEOUT_MULTIPLIER); +const MULTIPLIER = CI ? TIMEOUT_MULTIPLIER || 2 : TIMEOUT_MULTIPLIER || 1; + +// GLOBAL TESTS VARS +export const ELEMENT_WAIT_TIME: number = 5000 * MULTIPLIER; +export const ELEMENT_WAIT_LONGER_TIME: number = 10000 * MULTIPLIER; +export const ELEMENT_WAIT_EXTRA_LONG_TIME: number = 15000 * MULTIPLIER; +export const LOOP_INTERVAL: number = 1200; diff --git a/tests/core/coreElements.ts b/tests/core/coreElements.ts new file mode 100644 index 0000000..93129ec --- /dev/null +++ b/tests/core/coreElements.ts @@ -0,0 +1,11 @@ +export const coreElements = { + audioModal: 'div[data-test="audioModal"]', + closeModal: 'button[data-test="closeModal"]', + errorMessageLabel: 'span[id="error-message"]', + whiteboard: 'div[data-testid="canvas"]', + // BigBlueButton 4.0 has no actions-button dropdown. Plugins that contribute a sidekick + // area are reached through the apps gallery in the navigation sidebar instead. + appsGallerySidebarButton: 'div[data-test="appsGallerySidebarButton"]', + appsGalleryTitle: 'header[data-test="appsGalleryTitle"]', + hideAppsGallery: 'button[data-test="hideAppsGallery"]', +}; diff --git a/tests/core/fixtures/README.md b/tests/core/fixtures/README.md new file mode 100644 index 0000000..759b7c0 --- /dev/null +++ b/tests/core/fixtures/README.md @@ -0,0 +1,47 @@ +# Sample Test Utilities + +This directory contains shared utilities for sample plugin tests. + +## Usage + +### 1. Using the shared fixture (`sampleFixture.ts`) + +The `createSampleTest` function creates a Playwright test instance with a `sampleTest` fixture that automatically sets up the plugin for testing. + +```typescript +import { expect } from '@playwright/test'; +import { createSampleTest } from '../../../tests/core/sampleFixture'; +import { checkPluginAvailability } from '../../../tests/core/sampleBeforeAll'; + +const { test, setPluginUrl, getPluginUrl } = createSampleTest({ + envVarName: 'YOUR_PLUGIN_URL_ENV_VAR', // e.g., 'ACTIONS_BAR_URL' + getPluginUrl: () => process.env.YOUR_PLUGIN_URL_ENV_VAR, +}); +``` + +### 2. Using the shared beforeAll hook (`sampleBeforeAll.ts`) + +The `checkPluginAvailability` function creates a `beforeAll` hook that automatically checks the plugin availability, fetches the plugin manifest and sets up the plugin URL. + +```typescript +test.describe.parallel('Your Plugin Tests', () => { + test.beforeAll(checkPluginAvailability({ + pluginName: 'sample-your-plugin-name', // Must match the folder name in /plugins/ + envVarName: 'YOUR_PLUGIN_URL_ENV_VAR', + setPluginUrl, + getPluginUrl, + })); + + test('your test', async ({ sampleTest }) => { + // Use sampleTest fixture here + await sampleTest.modPage.hasElement('your-element'); + }); +}); +``` + +## Environment Variable Handling + +The utilities support two ways to provide plugin URLs: + +1. **Custom URL via environment variable**: Set `YOUR_PLUGIN_URL_ENV_VAR` to a custom plugin URL +2. **Auto-generated URL**: If no custom URL is provided, the `beforeAll` hook will construct the URL using the server configuration and plugin name diff --git a/tests/core/fixtures/pluginBeforeAll.ts b/tests/core/fixtures/pluginBeforeAll.ts new file mode 100644 index 0000000..7bb929f --- /dev/null +++ b/tests/core/fixtures/pluginBeforeAll.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-console */ +/* eslint-disable import/no-extraneous-dependencies */ +import { TestInfo, APIRequestContext } from '@playwright/test'; +import { secret, server } from '../parameters'; + +export type SampleBeforeAllConfig = { + pluginName: string; + envVarName: string; + setPluginUrl: (url: string) => void; + getPluginUrl?: () => string | undefined; +}; + +export function checkPluginAvailability(config: SampleBeforeAllConfig) { + return async ({ request }: { request: APIRequestContext }, testInfo: TestInfo): Promise => { + // Check if custom URL is already provided via environment variable + const customUrl = config.getPluginUrl?.(); + if (customUrl) { + config.setPluginUrl(customUrl); + return; + } + + const BBB_URL_PATTERN = /^https:\/\/[^/]+\/bigbluebutton\/$/; + if (!secret) throw new Error('BBB_SECRET environment variable is not set'); + if (!server) throw new Error('BBB_URL environment variable is not set'); + if (!BBB_URL_PATTERN.test(server)) throw new Error('BBB_URL must follow the pattern "https://DOMAIN_NAME/bigbluebutton/"'); + + const serverDomain = new URL(server).origin; + const manifestUrlPath = `/plugins/${config.pluginName}/dist/manifest.json`; + const pluginUrl = `${serverDomain}${manifestUrlPath}`; + + const response = await request.get(pluginUrl); + if (!response.ok()) { + const msg = `Failed to fetch plugin manifest for ${pluginUrl} plugin. returned status ${response.status()}`; + console.error(msg); + testInfo.skip( + true, + msg, + ); + return; + } + + try { + await response.json(); + config.setPluginUrl(pluginUrl); + } catch (error) { + const msg = `Invalid JSON response from plugin manifest for ${testInfo.title} plugin`; + console.error(msg, error); + testInfo.skip(true, msg); + } + }; +} diff --git a/tests/core/helpers.ts b/tests/core/helpers.ts new file mode 100644 index 0000000..71ec5aa --- /dev/null +++ b/tests/core/helpers.ts @@ -0,0 +1,195 @@ +import { expect, Page } from '@playwright/test'; +import sha, { Algorithm } from 'sha.js'; +import xml2js from 'xml2js'; +import axios from 'axios'; +import { + attendeePW, fullName, moderatorPW, secret, server, +} from './parameters'; +import { InitParameters } from './sessionPage'; + +declare global { + interface Window { + meetingClientSettings: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public: any; + }; + } +} + +interface getJoinURLParams { + meetingID: string; + isModerator: boolean; + joinParameter?: string; + skipSessionDetailsModal?: boolean; +} + +export interface SessionSettings { + reactionsButton: boolean; + sharedNotesEnabled: boolean; + directLeaveButton: boolean; + // Audio + autoJoinAudioModal: boolean; + listenOnlyMode: boolean; + forceListenOnly: boolean; + skipEchoTest: boolean; + skipEchoTestOnJoin: boolean; + skipEchoTestIfPreviousDevice: boolean; + speechRecognitionEnabled: boolean; + // Chat + chatEnabled: boolean; + publicChatOptionsEnabled: boolean; + maxMessageLength: number; + emojiPickerEnabled: boolean; + autoConvertEmojiEnabled: boolean; + // Polling + pollEnabled: boolean; + pollChatMessage: boolean; + // Presentation + originalPresentationDownloadable: boolean; + presentationWithAnnotationsDownloadable: boolean; + externalVideoPlayer: boolean; + hidePresentationOnJoin: boolean; + // Screensharing + screensharingEnabled: boolean; + // Timeouts + listenOnlyCallTimeout: number; + videoPreviewTimeout: number; + // Webcam + webcamSharingEnabled: boolean; + skipVideoPreview: boolean; + skipVideoPreviewOnFirstJoin: boolean; + skipVideoPreviewIfPreviousDevice: boolean; + // Emoji + emojiRain: boolean; +} + +function getChecksum(text: string) { + let algorithm: Algorithm = (process.env.CHECKSUM || '').toLowerCase() as Algorithm; + if (!['sha1', 'sha256', 'sha512'].includes(algorithm)) { + switch (secret?.length) { + case 128: + algorithm = 'sha512'; + break; + case 64: + algorithm = 'sha256'; + break; + case 40: + default: + algorithm = 'sha1'; + } + } + return sha(algorithm).update(text).digest('hex'); +} + +function getRandomInt(min: number, max: number) { + const adjustedMin = Math.ceil(min); + const adjustedMax = Math.floor(max); + return Math.floor(Math.random() * (adjustedMax - adjustedMin)) + adjustedMin; +} + +function createMeetingUrl(params: InitParameters, createParameter: string | undefined) { + const meetingID = `random-${getRandomInt(1000000, 10000000).toString()}`; + const mp = params.moderatorPW; + const ap = params.attendeePW; + const baseQuery = `name=${meetingID}&meetingID=${meetingID}&attendeePW=${ap}&moderatorPW=${mp}` + + `&allowStartStopRecording=true&autoStartRecording=false&welcome=${params.welcome}`; + const query = createParameter !== undefined ? `${baseQuery}&${createParameter}` : baseQuery; + const apiCall = `create${query}${params.secret}`; + const checksum = getChecksum(apiCall); + const url = `${params.server}/api/create?${query}&checksum=${checksum}`; + return url; +} + +function createMeetingPromise(params: InitParameters, createParameter: string | undefined) { + const url = createMeetingUrl(params, createParameter); + return axios.get(url, { adapter: 'http' }); +} + +export async function createMeeting( + params: InitParameters, + createParameter: string | undefined, +): Promise { + const promise = createMeetingPromise(params, createParameter); + const response = await promise; + expect(response.status).toEqual(200); + const xmlResponse = await xml2js.parseStringPromise(response.data); + return xmlResponse.response.meetingID[0]; +} + +export function getJoinURL({ + meetingID, isModerator, joinParameter, skipSessionDetailsModal, +}: getJoinURLParams) { + const pw = isModerator ? moderatorPW : attendeePW; + const shouldSkipSessionDetailsModal = skipSessionDetailsModal ? '&userdata-bbb_show_session_details_on_join=false' : ''; // default value in settings.yml is true + const baseQuery = `fullName=${fullName}&meetingID=${meetingID}&password=${pw}${shouldSkipSessionDetailsModal}`; + const query = joinParameter ? `${baseQuery}&${joinParameter}` : baseQuery; + const apiCall = `join${query}${secret}`; + const checksum = getChecksum(apiCall); + return `${server}/api/join?${query}&checksum=${checksum}`; +} + +export function encodeCustomParams(param: string): string { + try { + const splitted = param.split('='); + if (splitted.length > 2) { + const aux = splitted.shift(); + splitted[1] = splitted.join('='); + splitted[0] = aux || ''; + } + splitted[1] = encodeURIComponent(splitted[1]).replace(/%20/g, '+'); + return splitted.join('='); + } catch (err) { + throw new Error(`Error encoding custom params: ${err}`); + } +} + +export async function generateSettingsData(page: Page): Promise { + try { + const settingsData = await page.evaluate(() => window.meetingClientSettings.public); + + const settings = { + reactionsButton: settingsData.app?.reactionsButton?.enabled, + sharedNotesEnabled: settingsData.notes?.enabled, + directLeaveButton: settingsData.app?.defaultSettings?.application?.directLeaveButton, + // Audio + autoJoinAudioModal: settingsData.app?.autoJoin, + listenOnlyMode: settingsData.app?.listenOnlyMode, + forceListenOnly: settingsData.app?.forceListenOnly, + skipEchoTest: settingsData.app?.skipCheck, + skipEchoTestOnJoin: settingsData.app?.skipCheckOnJoin, + skipEchoTestIfPreviousDevice: settingsData.app?.skipEchoTestIfPreviousDevice, + speechRecognitionEnabled: settingsData.app?.audioCaptions?.enabled, + // Chat + chatEnabled: settingsData.chat?.enabled, + publicChatOptionsEnabled: settingsData.chat?.enableSaveAndCopyPublicChat, + maxMessageLength: settingsData.chat?.max_message_length, + emojiPickerEnabled: settingsData.chat?.emojiPicker?.enable, + autoConvertEmojiEnabled: settingsData.chat?.autoConvertEmoji, + // Polling + pollEnabled: settingsData.poll?.enabled, + pollChatMessage: settingsData.poll?.chatMessage, + // Presentation + originalPresentationDownloadable: settingsData.presentation?.allowDownloadOriginal, + presentationWithAnnotationsDownloadable: + settingsData.presentation?.allowDownloadWithAnnotations, + externalVideoPlayer: settingsData.externalVideoPlayer?.enabled, + hidePresentationOnJoin: settingsData.layout?.hidePresentationOnJoin, + // Screensharing + screensharingEnabled: settingsData.kurento?.enableScreensharing, + // Timeouts + listenOnlyCallTimeout: parseInt(settingsData.media?.listenOnlyCallTimeout, 10), + videoPreviewTimeout: parseInt(settingsData.kurento?.gUMTimeout, 10), + // Webcam + webcamSharingEnabled: settingsData.kurento?.enableVideo, + skipVideoPreview: settingsData.kurento?.skipVideoPreview, + skipVideoPreviewOnFirstJoin: settingsData.kurento?.skipVideoPreviewOnFirstJoin, + skipVideoPreviewIfPreviousDevice: settingsData.kurento?.skipVideoPreviewIfPreviousDevice, + // Emoji + emojiRain: settingsData.app?.emojiRain?.enabled, + }; + + return settings; + } catch (err) { + throw new Error(`Error generating settings data: ${err}`); + } +} diff --git a/tests/core/parameters.ts b/tests/core/parameters.ts new file mode 100644 index 0000000..7029805 --- /dev/null +++ b/tests/core/parameters.ts @@ -0,0 +1,6 @@ +export const server: string | undefined = process.env.BBB_URL; +export const secret: string | undefined = process.env.BBB_SECRET; +export const welcome: string = '%3Cbr%3EWelcome+to+%3Cb%3E%25%25CONFNAME%25%25%3C%2Fb%3E%21'; +export const fullName: string = 'User1'; +export const moderatorPW: string = 'mp'; +export const attendeePW: string = 'ap'; diff --git a/tests/core/plugin.ts b/tests/core/plugin.ts new file mode 100644 index 0000000..6685b06 --- /dev/null +++ b/tests/core/plugin.ts @@ -0,0 +1,30 @@ +import { Page, Browser, BrowserContext } from '@playwright/test'; +import { InitOptions, SessionPage } from './sessionPage'; + +interface PluginProps { + browser: Browser; + context: BrowserContext; +} + +export class Plugin { + readonly browser: Browser; + + readonly context: BrowserContext; + + modPage!: SessionPage; + + constructor({ browser, context }: PluginProps) { + this.browser = browser; + this.context = context; + } + + async initModPage(page: Page, { + fullName = 'Moderator', + shouldCloseAudioModal = true, + ...restOptions + }: InitOptions = {}) { + const options = { fullName, ...restOptions }; + this.modPage = new SessionPage({ browser: this.browser, page }); + await this.modPage.init({ isModerator: true, shouldCloseAudioModal, initOptions: options }); + } +} diff --git a/tests/core/sessionPage.ts b/tests/core/sessionPage.ts new file mode 100644 index 0000000..555e127 --- /dev/null +++ b/tests/core/sessionPage.ts @@ -0,0 +1,135 @@ +import { + expect, Page, Browser, Locator, +} from '@playwright/test'; +import { ELEMENT_WAIT_EXTRA_LONG_TIME, ELEMENT_WAIT_TIME } from './constants'; +import * as parameters from './parameters'; +import { + createMeeting, generateSettingsData, getJoinURL, SessionSettings, +} from './helpers'; +import { coreElements as e } from './coreElements'; + +interface PageProps { + browser: Browser; + page: Page; +} + +export interface InitOptions { + fullName?: string; + createParameter?: string; + joinParameter?: string; + shouldCloseAudioModal?: boolean; + skipSessionDetailsModal?: boolean; + shouldCheckAllInitialSteps?: boolean; +} + +interface InitFunctionParameters { + isModerator: boolean; + shouldCloseAudioModal: boolean; + initOptions: InitOptions; +} + +export interface InitParameters { + server: string | undefined; + secret: string | undefined; + welcome: string; + fullName: string; + moderatorPW: string; + attendeePW: string; +} + +export class SessionPage { + readonly page: Page; + + readonly browser: Browser; + + settings: SessionSettings | undefined; + + initParameters: InitParameters; + + username: string; + + meetingId: string; + + constructor({ browser, page }: PageProps) { + this.browser = browser; + this.page = page; + this.username = ''; + this.meetingId = ''; + this.initParameters = { ...parameters }; + } + + async init({ isModerator, shouldCloseAudioModal, initOptions = {} }: InitFunctionParameters) { + const { + fullName, + createParameter, + joinParameter, + skipSessionDetailsModal = true, + shouldCheckAllInitialSteps = true, + } = initOptions; + + if (!isModerator) this.initParameters.moderatorPW = ''; + this.username = fullName || this.initParameters.fullName; + + this.meetingId = await createMeeting(this.initParameters, createParameter); + const joinUrl = getJoinURL({ + meetingID: this.meetingId, isModerator, joinParameter, skipSessionDetailsModal, + }); + const response = await this.page.goto(joinUrl); + await expect(response?.ok()).toBeTruthy(); + const hasErrorLabel = await this.checkElement(e.errorMessageLabel); + await expect(hasErrorLabel, 'should pass the authentication and the layout element should be displayed').toBeFalsy(); + if (shouldCheckAllInitialSteps) { + await this.page.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + this.settings = await generateSettingsData(this.page); + const autoJoinAudioModal = this.settings?.autoJoinAudioModal; + if (shouldCloseAudioModal && autoJoinAudioModal) await this.closeAudioModal(); + } + // overwrite for font used in CI + await this.page.addStyleTag({ + content: ` + body { + font-family: 'Liberation Sans', Arial, sans-serif; + }`, + }); + } + + async hasElement(selector: string, description: string, timeout = ELEMENT_WAIT_TIME) { + const locator = this.getLocator(selector); + await expect(locator, description).toBeVisible({ timeout }); + } + + async wasRemoved(selector: string, description: string, timeout = ELEMENT_WAIT_TIME) { + const locator = this.getLocator(selector); + await expect(locator, description).toBeHidden({ timeout }); + } + + async checkElement(selector: string, index = 0): Promise { + // eslint-disable-next-line @typescript-eslint/no-shadow + return this.page.evaluate(([selector, index]) => { + if (typeof selector !== 'string') throw new Error('Selector must be a string'); + const element = document.querySelectorAll(selector); + if (element.length > 0) { + return element[index as number] !== undefined; + } + return false; + }, [selector, index]); + } + + getLocator(selector: string): Locator { + return this.page.locator(selector); + } + + async waitForPluginLogger() { + return this.page.waitForEvent('console', (msg) => msg.text().includes('PluginLogger')); + } + + async hasText(selector: string, text: string, description: string, timeout = ELEMENT_WAIT_TIME) { + const locator = this.getLocator(selector).first(); + await expect(locator, description).toContainText(text, { timeout }); + } + + async closeAudioModal() { + await this.hasElement(e.audioModal, 'should display the audio modal', ELEMENT_WAIT_EXTRA_LONG_TIME); + await this.page.click(e.closeModal); + } +} diff --git a/tests/elements.ts b/tests/elements.ts new file mode 100644 index 0000000..b9c2dec --- /dev/null +++ b/tests/elements.ts @@ -0,0 +1,48 @@ +import { coreElements } from './core/coreElements'; + +export const elements = { + ...coreElements, + + // Apps gallery entry contributed by the plugin's sidekick area. The client builds the + // attribute as `apps_gallery_item_`, where comes from the + // GenericContentSidekickArea registration in + // src/components/extensible-areas/generic-content-sidekick-area/component.tsx. + pickRandomUserAppsGalleryItem: 'div[data-test="apps_gallery_item_pickRandomUser"]', + + // Sidekick panel: the client's header, and the plugin's own panel root inside it. + pickRandomUserSidekickHeader: 'header[data-test="sidekick_header_pickRandomUser"]', + pickRandomUserPanel: 'div[data-test="pickRandomUserPanel"]', + + // Modal close button + pickRandomUserModalCloseButton: '[data-test="pickRandomUserModalCloseButton"]', + + // Presenter view – filter chips (label elements, for click and visibility checks) + includeModeratorsChip: '[data-test="includeModeratorsChip"]', + includePresenterChip: '[data-test="includePresenterChip"]', + includePickedUsersChip: '[data-test="includePickedUsersChip"]', + + // Presenter view – filter checkboxes (hidden inputs, for isChecked / not.toBeChecked only) + includeModeratorsCheckbox: '#includeModerators', + includePresenterCheckbox: '#includePresenter', + includePickedUsersCheckbox: '#includePickedUsers', + + // Presenter view – available users section + pickRandomUserAvailableContent: '[data-test="pickRandomUserAvailableContent"]', + + // Presenter view – pick button and "no users" warning + pickRandomUserPickButton: '[data-test="pickRandomUserPickButton"]', + pickRandomUserNoUsersWarning: '[data-test="pickRandomUserNoUsersWarning"]', + + // Presenter view – previously picked section + pickRandomUserClearAllButton: '[data-test="pickRandomUserClearAllButton"]', + pickRandomUserPreviouslyPickedList: '[data-test="pickRandomUserPreviouslyPickedList"]', + + // Picked-user view + pickRandomUserPickedUserViewTitle: '[data-test="pickRandomUserPickedUserViewTitle"]', + pickRandomUserPickedUserName: '[data-test="pickRandomUserPickedUserName"]', + pickRandomUserBackButton: '[data-test="pickRandomUserBackButton"]', + pickRandomUserCountDownMessage: 'div[data-test="countDownMessage"]', + + // Modal overlay (ReactModal renders this as a full-screen backdrop) + pickRandomUserModalOverlay: '.modalOverlay', +}; diff --git a/tests/structural/test.spec.ts b/tests/structural/test.spec.ts new file mode 100644 index 0000000..d0424ad --- /dev/null +++ b/tests/structural/test.spec.ts @@ -0,0 +1,146 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { + test, BrowserContext, Browser, APIRequestContext, TestInfo, +} from '@playwright/test'; +import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as ModPage } from '../core/sessionPage'; +import { Plugin } from '../core/plugin'; +import { encodeCustomParams } from '../core/helpers'; +import { moderatorCleanupAfterTest, openPickRandomUserPanel } from '../behavioral/helpers'; + +const PLUGIN_NAME = 'pick-random-user-plugin'; +const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; + +let pluginUrl: string | undefined = process.env[ENV_VAR_NAME]; +const setPluginUrl = (url: string) => { pluginUrl = url; }; +const getPluginUrl = () => pluginUrl; + +/** Reset state after each test: close the picked-user modal and the presenter panel. */ +async function cleanupAfterTest(modPage: ModPage): Promise { + await moderatorCleanupAfterTest(modPage); +} + +const ISOLATED = process.env.TEST_MEETINGS === 'isolated'; + +// ── Tests ───────────────────────────────────────────────────────────────────── + +test.describe('Pick Random User Plugin - Structural', () => { + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); + + let modPage: ModPage; + let sharedContext: BrowserContext; + + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, + ); + sharedContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const page = await sharedContext.newPage(); + const plugin = new Plugin({ browser, context: sharedContext }); + await plugin.initModPage(page, { createParameter }); + modPage = plugin.modPage; + } + + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await sharedContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await sharedContext?.close(); + }); + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + } + + test('should list "Pick random user" in the apps gallery for a presenter', async (): Promise => { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await modPage.page.click(e.appsGallerySidebarButton); + await modPage.hasElement( + e.pickRandomUserAppsGalleryItem, + 'should display the plugin entry in the apps gallery', + ELEMENT_WAIT_LONGER_TIME, + ); + await modPage.hasText( + e.pickRandomUserAppsGalleryItem, + 'Pick random user', + 'should display the correct label "Pick random user"', + ); + }); + + test('should open the presenter panel when clicking the apps gallery entry', async (): Promise => { + await openPickRandomUserPanel(modPage); + await modPage.hasElement( + e.pickRandomUserSidekickHeader, + 'should render the sidekick panel header', + ); + await modPage.hasElement( + e.includeModeratorsChip, + 'should show the presenter view with the "Include moderators" chip', + ); + await modPage.hasElement( + e.pickRandomUserAvailableContent, + 'should display the "Available for selection" section', + ); + }); + + test('should display all three filter chips in the presenter panel', async (): Promise => { + await openPickRandomUserPanel(modPage); + await modPage.hasElement(e.includeModeratorsChip, 'should display the "Include moderators" chip'); + await modPage.hasElement(e.includePresenterChip, 'should display the "Include presenter" chip'); + await modPage.hasElement(e.includePickedUsersChip, 'should display the "Include already picked" chip'); + }); + + test('should have all three filter checkboxes unchecked by default', async (): Promise => { + await openPickRandomUserPanel(modPage); + await test.expect( + modPage.getLocator(e.includeModeratorsCheckbox), + '"Include moderators" should be unchecked by default', + ).not.toBeChecked(); + await test.expect( + modPage.getLocator(e.includePresenterCheckbox), + '"Include presenter" should be unchecked by default', + ).not.toBeChecked(); + await test.expect( + modPage.getLocator(e.includePickedUsersCheckbox), + '"Include already picked user" should be unchecked by default', + ).not.toBeChecked(); + }); + + test('should show "no users" warning and hide the pick button with default filters (only presenter in meeting)', async (): Promise => { + // Default: includeModerators=false, includePresenter=false → + // the single moderator/presenter user is excluded by both rules → 0 eligible. + await openPickRandomUserPanel(modPage); + await modPage.hasElement( + e.pickRandomUserNoUsersWarning, + 'should show the "No {0} available" warning when 0 users are eligible', + ); + await modPage.wasRemoved( + e.pickRandomUserPickButton, + 'should NOT render the pick button when there are no eligible users', + ELEMENT_WAIT_TIME, + ); + }); +}); diff --git a/tests/unit/commons-utils.test.ts b/tests/unit/commons-utils.test.ts new file mode 100644 index 0000000..393f0e1 --- /dev/null +++ b/tests/unit/commons-utils.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest'; +import { hasCurrentUserSeenPickedUser, isNumber } from '../../src/commons/utils'; + +const wrap = (data: unknown) => ({ data } as never); +const seenEntry = (seenByUserId: string, pickedUserId: string) => ({ + payloadJson: { seenByUserId, pickedUserId }, +}); + +describe('isNumber', () => { + it('returns true for finite numbers, including zero and negatives', () => { + expect(isNumber(0)).toBe(true); + expect(isNumber(10)).toBe(true); + expect(isNumber(-3)).toBe(true); + expect(isNumber(3.14)).toBe(true); + }); + + it('returns false for NaN', () => { + expect(isNumber(NaN)).toBe(false); + }); + + it('returns false for non-number types', () => { + expect(isNumber('5')).toBe(false); + expect(isNumber('abc')).toBe(false); + expect(isNumber(undefined)).toBe(false); + expect(isNumber(null)).toBe(false); + expect(isNumber({})).toBe(false); + expect(isNumber([])).toBe(false); + expect(isNumber(true)).toBe(false); + }); +}); + +describe('hasCurrentUserSeenPickedUser', () => { + it('returns false when there are no entries', () => { + expect(hasCurrentUserSeenPickedUser(wrap([]), 'u1', 'p1')).toBe(false); + expect(hasCurrentUserSeenPickedUser(wrap(undefined), 'u1', 'p1')).toBe(false); + expect(hasCurrentUserSeenPickedUser(undefined as never, 'u1', 'p1')).toBe(false); + }); + + it('returns true when the current user has seen the picked user', () => { + expect(hasCurrentUserSeenPickedUser(wrap([seenEntry('u1', 'p1')]), 'u1', 'p1')).toBe(true); + }); + + it('returns false when seenByUserId belongs to another user', () => { + expect(hasCurrentUserSeenPickedUser(wrap([seenEntry('u2', 'p1')]), 'u1', 'p1')).toBe(false); + }); + + it('returns false when the entry refers to a different picked user', () => { + expect(hasCurrentUserSeenPickedUser(wrap([seenEntry('u1', 'p2')]), 'u1', 'p1')).toBe(false); + }); + + it('finds a matching entry among several', () => { + const entries = [seenEntry('u2', 'p1'), seenEntry('u1', 'p1')]; + expect(hasCurrentUserSeenPickedUser(wrap(entries), 'u1', 'p1')).toBe(true); + }); + + it('ignores entries without payloadJson', () => { + expect(hasCurrentUserSeenPickedUser(wrap([{ payloadJson: null }]), 'u1', 'p1')).toBe(false); + }); +}); diff --git a/tests/unit/filter-options.test.ts b/tests/unit/filter-options.test.ts new file mode 100644 index 0000000..18ce62b --- /dev/null +++ b/tests/unit/filter-options.test.ts @@ -0,0 +1,59 @@ +import { + describe, it, expect, vi, +} from 'vitest'; + +// modal/hooks.ts transitively imports the plugin SDK (pluginLogger, DataChannelTypes) +// as runtime values. Stub them so the pure helpers can be imported in isolation. +vi.mock('bigbluebutton-html-plugin-sdk', () => ({ + pluginLogger: { + warn: vi.fn(), info: vi.fn(), error: vi.fn(), debug: vi.fn(), + }, + DataChannelTypes: { LATEST_ITEM: 'LATEST_ITEM' }, +})); + +// eslint-disable-next-line import/first +import { + hasFilterOptionsChanged, + getLatestFilterOptionsFromDataChannel, +} from '../../src/components/modal/hooks'; +// eslint-disable-next-line import/first +import { FilterOptionsType } from '../../src/components/modal/types'; + +const opts = (over: Partial = {}): FilterOptionsType => ({ + includeModerators: false, + includePresenter: false, + includePickedUsers: false, + ...over, +}); + +describe('hasFilterOptionsChanged', () => { + it('returns false when all flags are equal', () => { + expect(hasFilterOptionsChanged(opts(), opts())).toBe(false); + }); + + it('returns true when any flag differs', () => { + expect(hasFilterOptionsChanged(opts(), opts({ includeModerators: true }))).toBe(true); + expect(hasFilterOptionsChanged(opts(), opts({ includePresenter: true }))).toBe(true); + expect(hasFilterOptionsChanged(opts(), opts({ includePickedUsers: true }))).toBe(true); + }); + + it('returns true when the data-channel value is missing', () => { + expect(hasFilterOptionsChanged(opts(), undefined)).toBe(true); + }); +}); + +describe('getLatestFilterOptionsFromDataChannel', () => { + it('returns null when there is no data', () => { + expect(getLatestFilterOptionsFromDataChannel({ data: null } as never)).toBeNull(); + }); + + it('returns undefined when the data list is empty', () => { + expect(getLatestFilterOptionsFromDataChannel({ data: [] } as never)).toBeUndefined(); + }); + + it('returns the payloadJson of the first entry', () => { + const fo = opts({ includeModerators: true }); + expect(getLatestFilterOptionsFromDataChannel({ data: [{ payloadJson: fo }] } as never)) + .toEqual(fo); + }); +}); diff --git a/tests/unit/filter-possible-users.test.ts b/tests/unit/filter-possible-users.test.ts new file mode 100644 index 0000000..9da4bbb --- /dev/null +++ b/tests/unit/filter-possible-users.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect } from 'vitest'; +import { filterPossibleUsersToBePicked } from '../../src/components/modal/presenter-view/utils'; +import { FilterOptionsType } from '../../src/components/modal/types'; + +interface TestUser { + userId: string; + name: string; + role: string; + presenter: boolean; + bot: boolean; +} + +const user = (over: Partial): TestUser => ({ + userId: 'x', + name: 'X', + role: 'VIEWER', + presenter: false, + bot: false, + ...over, +}); + +const filters = (over: Partial = {}): FilterOptionsType => ({ + includeModerators: false, + includePresenter: false, + includePickedUsers: false, + ...over, +}); + +const viewer = user({ userId: 'v1', name: 'Viewer 1' }); +const moderator = user({ userId: 'm1', name: 'Mod 1', role: 'MODERATOR' }); +const viewerPresenter = user({ userId: 'p1', name: 'Viewer Presenter', presenter: true }); +const botUser = user({ userId: 'b1', name: 'Bot', bot: true }); +const alreadyPicked = user({ userId: 'v2', name: 'Viewer 2' }); + +const allUsers = { + user: [viewer, moderator, viewerPresenter, botUser, alreadyPicked], +} as never; + +const pickedFromDataChannel = [{ payloadJson: { userId: 'v2' } }] as never; + +const idsOf = (result: { user: TestUser[] }) => result.user.map((u) => u.userId).sort(); + +describe('filterPossibleUsersToBePicked', () => { + it('with all filters off keeps only non-bot, non-presenter, not-yet-picked viewers', () => { + const result = filterPossibleUsersToBePicked(allUsers, pickedFromDataChannel, filters()); + expect(idsOf(result as never)).toEqual(['v1']); + }); + + it('always excludes bots, regardless of filters', () => { + const result = filterPossibleUsersToBePicked( + allUsers, + pickedFromDataChannel, + filters({ includeModerators: true, includePresenter: true, includePickedUsers: true }), + ); + expect((result as { user: TestUser[] }).user.every((u) => !u.bot)).toBe(true); + }); + + it('includes moderators when includeModerators is on', () => { + const result = filterPossibleUsersToBePicked( + allUsers, + pickedFromDataChannel, + filters({ includeModerators: true }), + ); + expect(idsOf(result as never)).toEqual(['m1', 'v1']); + }); + + it('includes the presenter when includePresenter is on', () => { + const result = filterPossibleUsersToBePicked( + allUsers, + pickedFromDataChannel, + filters({ includePresenter: true }), + ); + expect(idsOf(result as never)).toEqual(['p1', 'v1']); + }); + + it('includes already-picked users when includePickedUsers is on', () => { + const result = filterPossibleUsersToBePicked( + allUsers, + pickedFromDataChannel, + filters({ includePickedUsers: true }), + ); + expect(idsOf(result as never)).toEqual(['v1', 'v2']); + }); + + it('with every filter on keeps all non-bot users', () => { + const result = filterPossibleUsersToBePicked( + allUsers, + pickedFromDataChannel, + filters({ includeModerators: true, includePresenter: true, includePickedUsers: true }), + ); + expect(idsOf(result as never)).toEqual(['m1', 'p1', 'v1', 'v2']); + }); + + it('keeps every viewer when the picked list is empty', () => { + const result = filterPossibleUsersToBePicked(allUsers, [] as never, filters()); + expect(idsOf(result as never)).toEqual(['v1', 'v2']); + }); + + it('returns an empty list when there are no users', () => { + const result = filterPossibleUsersToBePicked(undefined, pickedFromDataChannel, filters()); + expect((result as { user: TestUser[] }).user).toEqual([]); + }); +}); diff --git a/tests/unit/format-picked-time.test.ts b/tests/unit/format-picked-time.test.ts new file mode 100644 index 0000000..480ffd7 --- /dev/null +++ b/tests/unit/format-picked-time.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { formatPickedTime } from '../../src/components/modal/presenter-view/utils'; + +// Dates are built from local components so the assertions are timezone-independent. +describe('formatPickedTime', () => { + it('zero-pads single-digit hours and minutes', () => { + expect(formatPickedTime(new Date(2024, 0, 1, 9, 5))).toBe('09:05'); + }); + + it('formats afternoon times in 24h notation', () => { + expect(formatPickedTime(new Date(2024, 0, 1, 14, 30))).toBe('14:30'); + }); + + it('handles midnight', () => { + expect(formatPickedTime(new Date(2024, 0, 1, 0, 0))).toBe('00:00'); + }); + + it('accepts a numeric timestamp', () => { + const ts = new Date(2024, 0, 1, 7, 8).getTime(); + expect(formatPickedTime(ts)).toBe('07:08'); + }); +}); diff --git a/tests/unit/presenter-subscription-lifecycle.test.tsx b/tests/unit/presenter-subscription-lifecycle.test.tsx new file mode 100644 index 0000000..54ba460 --- /dev/null +++ b/tests/unit/presenter-subscription-lifecycle.test.tsx @@ -0,0 +1,174 @@ +import * as React from 'react'; +import { + describe, it, expect, vi, +} from 'vitest'; +import { render } from '@testing-library/react'; +import { createIntl } from 'react-intl'; +import { PickRandomUserPanel } from '../../src/components/pick-random-user-panel/component'; +import { PickedUser } from '../../src/components/pick-random-user/types'; + +// On this branch (BigBlueButton 4.0) the presenter view lives in the sidekick panel, not +// in the modal: the actions-button dropdown that opens the modal on v0.0.x does not exist +// in the 0.1.x SDK. The invariant under test is unchanged though — the useUsersBasicInfo +// subscription must be opened only for the presenter and must survive a pick, so the +// "available for selection" list does not flash back into a loading state on every pick. + +// The component tree under test pulls in RESET_DATA_CHANNEL and pluginLogger (values) +// from the SDK. Stub them so the debug instrumentation does not reach the real logging +// pipeline; every other SDK import used here is type-only and is erased at build time. +vi.mock('bigbluebutton-html-plugin-sdk', () => ({ + RESET_DATA_CHANNEL: 'RESET_DATA_CHANNEL', + DataChannelTypes: { LATEST_ITEM: 'Hooks::DataChannel::LatestItem' }, + pluginLogger: { + debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), + }, +})); + +const intl = createIntl({ locale: 'en', messages: {} }); + +const graphqlWrapper = (data: T) => ({ loading: false, data, error: undefined }); + +const pickedUser = (userId: string): PickedUser => ({ + userId, name: userId, role: 'MODERATOR', presenter: false, bot: false, avatar: '', color: '#000', +}); + +const pickedUserEntry = (userId: string, entryId: string) => ({ + entryId, + createdAt: '2026-01-01T10:00:00.000Z', + payloadJson: pickedUser(userId), +}); + +// A pluginApi whose useUsersBasicInfo reports every subscribe/unsubscribe through the +// given spies, mirroring how a real GraphQL subscription hook ties its lifecycle to the +// mount/unmount of whichever component calls it. `pickedEntries` stands in for the +// pickRandomUser data channel, so a pick can be simulated between renders. +function makePluginApi( + onSubscribe: () => void, + onUnsubscribe: () => void, + presenter = true, + pickedEntries: ReturnType[] = [], +) { + return { + useUsersBasicInfo: () => { + React.useEffect(() => { + onSubscribe(); + return () => onUnsubscribe(); + }, []); + return graphqlWrapper({ user: [] }); + }, + useDataChannel: (channelName: string) => ({ + data: graphqlWrapper(channelName === 'pickRandomUser' ? pickedEntries : []), + pushEntry: vi.fn(), + deleteEntry: vi.fn(), + }), + useCurrentUser: () => ({ data: { userId: 'presenter-1', presenter } }), + } as never; +} + +describe('presenter-view data subscription lifecycle (unstable connection follow-up)', () => { + it('never subscribes when the current user is not the presenter', () => { + const onSubscribe = vi.fn(); + const onUnsubscribe = vi.fn(); + + render( + , + ); + + expect(onSubscribe).not.toHaveBeenCalled(); + }); + + it('subscribes exactly once for the presenter, and does not re-subscribe across a pick', () => { + const onSubscribe = vi.fn(); + const onUnsubscribe = vi.fn(); + + const { rerender } = render( + , + ); + + expect(onSubscribe).toHaveBeenCalledTimes(1); + expect(onUnsubscribe).not.toHaveBeenCalled(); + + // The presenter picks someone: the pickRandomUser data channel gains an entry and the + // picked-user modal opens on top. Neither may disturb the panel's subscription. + rerender( + , + ); + + expect(onSubscribe).toHaveBeenCalledTimes(1); + expect(onUnsubscribe).not.toHaveBeenCalled(); + + // ...and again, for a second pick. + rerender( + , + ); + + expect(onSubscribe).toHaveBeenCalledTimes(1); + expect(onUnsubscribe).not.toHaveBeenCalled(); + }); + + it('tears the subscription down once the panel is closed', () => { + const onSubscribe = vi.fn(); + const onUnsubscribe = vi.fn(); + + const { unmount } = render( + , + ); + + expect(onSubscribe).toHaveBeenCalledTimes(1); + expect(onUnsubscribe).not.toHaveBeenCalled(); + + unmount(); + + expect(onUnsubscribe).toHaveBeenCalledTimes(1); + }); + + it('tears the subscription down when the current user loses the presenter role', () => { + const onSubscribe = vi.fn(); + const onUnsubscribe = vi.fn(); + + const { rerender } = render( + , + ); + + expect(onSubscribe).toHaveBeenCalledTimes(1); + + rerender( + , + ); + + expect(onUnsubscribe).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/unit/settings.test.ts b/tests/unit/settings.test.ts new file mode 100644 index 0000000..df50baf --- /dev/null +++ b/tests/unit/settings.test.ts @@ -0,0 +1,93 @@ +import { + describe, it, expect, beforeEach, afterEach, +} from 'vitest'; +import { + getPingSoundEnabled, + getBrowserNotificationEnabled, + getPickedUserTimeWindowFromSettings, + getPreventCloseDelayFromSettings, + getPingSoundUrl, +} from '../../src/components/pick-random-user/hooks'; +import { + PICKED_USER_TIME_WINDOW, + DEFAULT_PREVENT_CLOSE_DELAY_SECONDS, + DEFAULT_PING_SOUND_URL, +} from '../../src/commons/constants'; + +const settings = (overrides: Record = {}) => overrides as never; + +describe('getPingSoundEnabled', () => { + it('keeps the previous state when the setting is absent', () => { + expect(getPingSoundEnabled(settings({}), true)).toBe(true); + expect(getPingSoundEnabled(settings({ pingSoundEnabled: undefined }), false)).toBe(false); + expect(getPingSoundEnabled(settings({ pingSoundEnabled: null }), true)).toBe(true); + }); + + it('coerces the configured value to a boolean', () => { + expect(getPingSoundEnabled(settings({ pingSoundEnabled: true }), false)).toBe(true); + expect(getPingSoundEnabled(settings({ pingSoundEnabled: false }), true)).toBe(false); + }); +}); + +describe('getBrowserNotificationEnabled', () => { + it('keeps the previous state when the setting is absent', () => { + expect(getBrowserNotificationEnabled(settings({}), true)).toBe(true); + expect(getBrowserNotificationEnabled(settings({ browserNotificationEnabled: null }), false)).toBe(false); + }); + + it('coerces the configured value to a boolean', () => { + expect(getBrowserNotificationEnabled(settings({ browserNotificationEnabled: true }), false)).toBe(true); + expect(getBrowserNotificationEnabled(settings({ browserNotificationEnabled: false }), true)).toBe(false); + }); +}); + +describe('getPickedUserTimeWindowFromSettings', () => { + it('returns the numeric setting when valid', () => { + expect(getPickedUserTimeWindowFromSettings(settings({ pickedUserTimeWindow: 25 }))).toBe(25); + }); + + it('treats zero as a valid value (regression: it used to fall back to the default)', () => { + expect(getPickedUserTimeWindowFromSettings(settings({ pickedUserTimeWindow: 0 }))).toBe(0); + }); + + it('falls back to the default for non-numeric values', () => { + expect(getPickedUserTimeWindowFromSettings(settings({ pickedUserTimeWindow: '25' }))) + .toBe(PICKED_USER_TIME_WINDOW); + expect(getPickedUserTimeWindowFromSettings(settings({}))).toBe(PICKED_USER_TIME_WINDOW); + }); +}); + +describe('getPreventCloseDelayFromSettings', () => { + it('returns the numeric setting when valid, including zero', () => { + expect(getPreventCloseDelayFromSettings(settings({ preventCloseDelaySeconds: 5 }))).toBe(5); + expect(getPreventCloseDelayFromSettings(settings({ preventCloseDelaySeconds: 0 }))).toBe(0); + }); + + it('falls back to the default for non-numeric values', () => { + expect(getPreventCloseDelayFromSettings(settings({ preventCloseDelaySeconds: 'x' }))) + .toBe(DEFAULT_PREVENT_CLOSE_DELAY_SECONDS); + expect(getPreventCloseDelayFromSettings(settings({}))).toBe(DEFAULT_PREVENT_CLOSE_DELAY_SECONDS); + }); +}); + +describe('getPingSoundUrl', () => { + beforeEach(() => { + (window as unknown as Record).meetingClientSettings = { + public: { app: { cdn: 'https://cdn.example.com', basename: '/html5client' } }, + }; + }); + + afterEach(() => { + delete (window as unknown as Record).meetingClientSettings; + }); + + it('returns the configured url as a string', () => { + expect(getPingSoundUrl(settings({ pingSoundUrl: 'resources/sounds/alarm.mp3' }))) + .toBe('resources/sounds/alarm.mp3'); + }); + + it('builds the default url from cdn + basename when not configured', () => { + expect(getPingSoundUrl(settings({}))) + .toBe(`https://cdn.example.com/html5client/${DEFAULT_PING_SOUND_URL}`); + }); +}); diff --git a/tests/unit/setup.ts b/tests/unit/setup.ts new file mode 100644 index 0000000..d8ecf83 --- /dev/null +++ b/tests/unit/setup.ts @@ -0,0 +1,9 @@ +import '@testing-library/jest-dom/vitest'; +import { cleanup } from '@testing-library/react'; +import { afterEach } from 'vitest'; + +// React Testing Library does not auto-clean with Vitest's default config, +// so unmount any rendered tree between tests to avoid cross-test leakage. +afterEach(() => { + cleanup(); +}); diff --git a/tests/unit/sidekick-area-registration.test.tsx b/tests/unit/sidekick-area-registration.test.tsx new file mode 100644 index 0000000..19806b8 --- /dev/null +++ b/tests/unit/sidekick-area-registration.test.tsx @@ -0,0 +1,183 @@ +import * as React from 'react'; +import { + describe, it, expect, vi, +} from 'vitest'; +import { render, renderHook } from '@testing-library/react'; +import { createIntl } from 'react-intl'; +import GenericContentSidekickAreaManager from '../../src/components/extensible-areas/generic-content-sidekick-area/component'; +import { useGetInternationalization } from '../../src/commons/hooks'; + +// Registering an extensible-area item makes the client re-render, which renders the +// plugin again. If the registering effect depends on anything rebuilt per render, that +// loops forever: the client's layout engine reports "Maximum update depth exceeded" and +// the sidekick panel goes blank because its React root is recreated on every pass. + +vi.mock('bigbluebutton-html-plugin-sdk', () => ({ + GenericContentSidekickArea: class { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(itemProps: any) { Object.assign(this, itemProps); } + }, + RESET_DATA_CHANNEL: 'RESET_DATA_CHANNEL', + DataChannelTypes: { LATEST_ITEM: 'Hooks::DataChannel::LatestItem' }, + pluginLogger: { + debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), + }, +})); + +const intl = createIntl({ locale: 'en', messages: {} }); + +// Fresh object on every call, mirroring what useGetAllSettings/useCurrentUser hand over. +const settings = () => ({ + pingSoundEnabled: false, + pingSoundUrl: '', + browserNotificationEnabled: false, + pickedUserTimeWindow: 30, + preventCloseDelaySeconds: 0, + modalUiScale: 1, +}); + +const currentUser = (presenter: boolean) => ({ userId: 'user-1', presenter } as never); + +function makePluginApi(setGenericContentItems: ReturnType) { + return { setGenericContentItems } as never; +} + +describe('sidekick area registration', () => { + it('registers once and does not re-register when per-render objects are rebuilt', () => { + const setGenericContentItems = vi.fn(() => ['generated-id-1']); + const pluginApi = makePluginApi(setGenericContentItems); + + const { rerender } = render( + , + ); + + expect(setGenericContentItems).toHaveBeenCalledTimes(1); + + // New currentUser and settings objects with identical values, exactly what the parent + // produces on every data-channel or settings update. + rerender( + , + ); + rerender( + , + ); + + expect(setGenericContentItems).toHaveBeenCalledTimes(1); + }); + + it('reuses the same generated id when it does re-register', () => { + const setGenericContentItems = vi.fn(() => ['generated-id-1']); + const pluginApi = makePluginApi(setGenericContentItems); + + const { rerender } = render( + , + ); + + rerender( + , + ); + + expect(setGenericContentItems).toHaveBeenCalledTimes(2); + expect(setGenericContentItems.mock.calls[1][0][0]).toMatchObject({ id: 'generated-id-1' }); + }); + + it('removes the item when the current user stops being the presenter', () => { + const setGenericContentItems = vi.fn(() => ['generated-id-1']); + const pluginApi = makePluginApi(setGenericContentItems); + + const { rerender } = render( + , + ); + + rerender( + , + ); + + expect(setGenericContentItems).toHaveBeenCalledTimes(2); + expect(setGenericContentItems.mock.calls[1][0]).toEqual([]); + }); + + it('never registers for a user who is not the presenter', () => { + const setGenericContentItems = vi.fn(() => ['generated-id-1']); + + render( + , + ); + + expect(setGenericContentItems).not.toHaveBeenCalled(); + }); +}); + +describe('useGetInternationalization', () => { + it('keeps the same intl instance across renders', () => { + const messages = { 'pickRandomUserPlugin.modal.title': 'Pick random user' }; + const pluginApi = { + useLocaleMessages: () => ({ messages, currentLocale: 'en', loading: false }), + } as never; + + const { result, rerender } = renderHook(() => useGetInternationalization(pluginApi)); + + const first = result.current.intl; + rerender(); + rerender(); + + expect(result.current.intl).toBe(first); + }); + + it('builds a new intl instance when the locale changes', () => { + const messages = { 'pickRandomUserPlugin.modal.title': 'Pick random user' }; + let currentLocale = 'en'; + const pluginApi = { + useLocaleMessages: () => ({ messages, currentLocale, loading: false }), + } as never; + + const { result, rerender } = renderHook(() => useGetInternationalization(pluginApi)); + + const first = result.current.intl; + currentLocale = 'pt-BR'; + rerender(); + + expect(result.current.intl).not.toBe(first); + expect(result.current.intl?.locale).toBe('pt-BR'); + }); +}); diff --git a/tests/unit/use-get-all-settings.test.ts b/tests/unit/use-get-all-settings.test.ts new file mode 100644 index 0000000..1a4b2e6 --- /dev/null +++ b/tests/unit/use-get-all-settings.test.ts @@ -0,0 +1,68 @@ +import { + describe, it, expect, beforeEach, afterEach, +} from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { useGetAllSettings } from '../../src/components/pick-random-user/hooks'; +import { + PICKED_USER_TIME_WINDOW, + DEFAULT_PREVENT_CLOSE_DELAY_SECONDS, + DEFAULT_PING_SOUND_URL, + DEFAULT_MODAL_UI_SCALE, +} from '../../src/commons/constants'; + +const wrapper = (data: unknown, loading: boolean) => ({ data, loading } as never); + +describe('useGetAllSettings', () => { + beforeEach(() => { + (window as unknown as Record).meetingClientSettings = { + public: { app: { cdn: 'https://cdn.example.com', basename: '/html5client' } }, + }; + }); + + afterEach(() => { + delete (window as unknown as Record).meetingClientSettings; + }); + + it('returns the defaults while settings are still loading', () => { + const { result } = renderHook(() => useGetAllSettings(wrapper(undefined, true))); + + expect(result.current).toEqual({ + pingSoundEnabled: true, + pingSoundUrl: DEFAULT_PING_SOUND_URL, + browserNotificationEnabled: false, + pickedUserTimeWindow: PICKED_USER_TIME_WINDOW, + preventCloseDelaySeconds: DEFAULT_PREVENT_CLOSE_DELAY_SECONDS, + modalUiScale: DEFAULT_MODAL_UI_SCALE, + }); + }); + + it('maps loaded plugin settings into the resolved settings object', () => { + const settings = { + pingSoundEnabled: false, + browserNotificationEnabled: true, + pingSoundUrl: 'resources/sounds/alarm.mp3', + pickedUserTimeWindow: 20, + preventCloseDelaySeconds: 5, + modalUiScale: 1.5, + }; + + const { result } = renderHook(() => useGetAllSettings(wrapper(settings, false))); + + expect(result.current).toEqual({ + pingSoundEnabled: false, + browserNotificationEnabled: true, + pingSoundUrl: 'resources/sounds/alarm.mp3', + pickedUserTimeWindow: 20, + preventCloseDelaySeconds: 5, + modalUiScale: 1.5, + }); + }); + + it('falls back to the default UI scale when modalUiScale is not configured', () => { + const settings = { pingSoundEnabled: true }; + + const { result } = renderHook(() => useGetAllSettings(wrapper(settings, false))); + + expect(result.current.modalUiScale).toBe(DEFAULT_MODAL_UI_SCALE); + }); +}); diff --git a/tests/unit/use-get-current-picked-user.test.ts b/tests/unit/use-get-current-picked-user.test.ts new file mode 100644 index 0000000..c8e5b47 --- /dev/null +++ b/tests/unit/use-get-current-picked-user.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { useGetCurrentPickedUser } from '../../src/components/pick-random-user/hooks'; + +const entry = (entryId: string, userId: string) => ({ + entryId, + payloadJson: { userId, name: userId }, +}); + +describe('useGetCurrentPickedUser', () => { + it('returns null when there is no data-channel data', () => { + const { result } = renderHook( + ({ dc }) => useGetCurrentPickedUser(dc), + { initialProps: { dc: undefined as never } }, + ); + expect(result.current).toBeNull(); + }); + + it('returns the first entry as the current picked user', () => { + const { result } = renderHook( + ({ dc }) => useGetCurrentPickedUser(dc), + { initialProps: { dc: [entry('e1', 'p1')] as never } }, + ); + expect(result.current).toEqual({ + entryId: 'e1', + pickedUser: { userId: 'p1', name: 'p1' }, + }); + }); + + it('updates when the entryId changes and clears when the list empties', () => { + const { result, rerender } = renderHook( + ({ dc }) => useGetCurrentPickedUser(dc), + { initialProps: { dc: [entry('e1', 'p1')] as never } }, + ); + expect(result.current?.entryId).toBe('e1'); + + rerender({ dc: [entry('e2', 'p2')] as never }); + expect(result.current?.entryId).toBe('e2'); + expect(result.current?.pickedUser.userId).toBe('p2'); + + rerender({ dc: [] as never }); + expect(result.current).toBeNull(); + }); +}); diff --git a/tests/unit/use-get-possible-users-to-be-picked.test.ts b/tests/unit/use-get-possible-users-to-be-picked.test.ts new file mode 100644 index 0000000..f343415 --- /dev/null +++ b/tests/unit/use-get-possible-users-to-be-picked.test.ts @@ -0,0 +1,98 @@ +import { + describe, it, expect, vi, +} from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { useGetPossibleUsersToBePicked } from '../../src/components/modal/presenter-view/hooks'; +import { FilterOptionsType } from '../../src/components/modal/types'; + +// The hook imports RESET_DATA_CHANNEL and pluginLogger (a value) from the SDK. +// Stub them so the debug instrumentation does not reach the real logging pipeline. +vi.mock('bigbluebutton-html-plugin-sdk', () => ({ + RESET_DATA_CHANNEL: 'RESET_DATA_CHANNEL', + pluginLogger: { + debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), + }, +})); + +const mod = (userId: string) => ({ + userId, name: userId, role: 'MODERATOR', presenter: false, bot: false, +}); + +const filters = (over: Partial = {}): FilterOptionsType => ({ + includeModerators: true, + includePresenter: true, + includePickedUsers: true, + ...over, +}); + +// A minimal PluginApi whose useUsersBasicInfo returns the given wrapper and whose +// pickRandomUser data-channel is empty. +const makePluginApi = (usersInfo: unknown) => ({ + useUsersBasicInfo: () => usersInfo, + useDataChannel: () => ({ data: undefined }), +}) as never; + +const loadingWrapper = { loading: true, data: undefined, error: undefined }; +const loadedWrapper = (users: ReturnType[]) => ({ + loading: false, data: { user: users }, error: undefined, +}); + +describe('useGetPossibleUsersToBePicked (unstable connection)', () => { + it('reports loading — not an empty room — while the first snapshot has not arrived', () => { + const { result } = renderHook( + ({ api }) => useGetPossibleUsersToBePicked(api, filters()), + { initialProps: { api: makePluginApi(loadingWrapper) } }, + ); + expect(result.current.isLoading).toBe(true); + expect(result.current.users).toEqual([]); + }); + + it('exposes the users once an authoritative snapshot arrives', () => { + const { result } = renderHook( + ({ api }) => useGetPossibleUsersToBePicked(api, filters()), + { initialProps: { api: makePluginApi(loadedWrapper([mod('m1'), mod('m2')])) } }, + ); + expect(result.current.isLoading).toBe(false); + expect(result.current.users.map((u) => u.userId)).toEqual(['m1', 'm2']); + }); + + it('reports loading — never a stale list — during a mid-session reconnect', () => { + // This plugin must not offer a picked user who may have already left, so it does + // NOT retain the previous snapshot. A reconnect surfaces as loading, not stale data. + const { result, rerender } = renderHook( + ({ api }) => useGetPossibleUsersToBePicked(api, filters()), + { initialProps: { api: makePluginApi(loadedWrapper([mod('m1'), mod('m2')])) } }, + ); + expect(result.current.users).toHaveLength(2); + + // Connection drops: the subscription re-fires with loading / undefined data. + rerender({ api: makePluginApi(loadingWrapper) }); + expect(result.current.isLoading).toBe(true); + expect(result.current.users).toEqual([]); + }); + + it('reports loading when the SDK is refetching even though data is still present', () => { + // loading: true with a populated snapshot must still gate the Pick button, since we + // cannot be sure the snapshot is current. + const { result } = renderHook( + ({ api }) => useGetPossibleUsersToBePicked(api, filters()), + { + initialProps: { + api: makePluginApi({ + loading: true, data: { user: [mod('m1'), mod('m2')] }, error: undefined, + }), + }, + }, + ); + expect(result.current.isLoading).toBe(true); + }); + + it('treats a genuinely empty room (authoritative empty snapshot) as not-loading', () => { + const { result } = renderHook( + ({ api }) => useGetPossibleUsersToBePicked(api, filters()), + { initialProps: { api: makePluginApi(loadedWrapper([])) } }, + ); + expect(result.current.isLoading).toBe(false); + expect(result.current.users).toEqual([]); + }); +}); diff --git a/tests/unit/use-prevent-close-countdown.test.ts b/tests/unit/use-prevent-close-countdown.test.ts new file mode 100644 index 0000000..bb42dc8 --- /dev/null +++ b/tests/unit/use-prevent-close-countdown.test.ts @@ -0,0 +1,62 @@ +import { + describe, it, expect, vi, beforeEach, afterEach, +} from 'vitest'; +import { act, renderHook } from '@testing-library/react'; + +vi.mock('bigbluebutton-html-plugin-sdk', () => ({ + pluginLogger: { + warn: vi.fn(), info: vi.fn(), error: vi.fn(), debug: vi.fn(), + }, + DataChannelTypes: { LATEST_ITEM: 'LATEST_ITEM' }, +})); + +// eslint-disable-next-line import/first +import { usePreventCloseModalCountdown } from '../../src/components/modal/hooks'; + +const currentUser = { userId: 'u1' } as never; +const notSeen = { data: [], loading: false } as never; +const pickedUser = { entryId: 'e1', pickedUser: { userId: 'p1' } } as never; +const settings = (preventCloseDelaySeconds: number) => ({ preventCloseDelaySeconds } as never); + +describe('usePreventCloseModalCountdown', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('blocks closing while the countdown runs and allows it once elapsed', () => { + const { result } = renderHook( + () => usePreventCloseModalCountdown(currentUser, notSeen, pickedUser, settings(3)), + ); + + expect(result.current.canClose).toBe(false); + expect(result.current.remainingSeconds).toBe(3); + + act(() => { + vi.advanceTimersByTime(3500); + }); + + expect(result.current.canClose).toBe(true); + expect(result.current.remainingSeconds).toBe(0); + }); + + it('does not block closing when the delay is zero', () => { + const { result } = renderHook( + () => usePreventCloseModalCountdown(currentUser, notSeen, pickedUser, settings(0)), + ); + + expect(result.current.canClose).toBe(true); + expect(result.current.remainingSeconds).toBe(0); + }); + + it('allows closing when there is no picked user', () => { + const { result } = renderHook( + () => usePreventCloseModalCountdown(currentUser, notSeen, null, settings(3)), + ); + + expect(result.current.canClose).toBe(true); + }); +}); diff --git a/tests/unit/use-previous-value.test.ts b/tests/unit/use-previous-value.test.ts new file mode 100644 index 0000000..74f69a7 --- /dev/null +++ b/tests/unit/use-previous-value.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { renderHook } from '@testing-library/react'; +import { usePreviousValue } from '../../src/commons/hooks'; + +describe('usePreviousValue', () => { + it('returns undefined on the first render', () => { + const { result } = renderHook(() => usePreviousValue('first')); + expect(result.current).toBeUndefined(); + }); + + it('returns the value from the previous render after an update', () => { + const { result, rerender } = renderHook(({ value }) => usePreviousValue(value), { + initialProps: { value: 'a' }, + }); + + rerender({ value: 'b' }); + expect(result.current).toBe('a'); + + rerender({ value: 'c' }); + expect(result.current).toBe('b'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 3ec0e69..e7a8e11 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,18 +2,17 @@ "compilerOptions": { "outDir": "./dist-tsc/", "noImplicitAny": true, + "skipLibCheck": true, "module": "ES2022", "target": "ES2022", + "lib": ["ES2022", "DOM"], "jsx": "react", "allowJs": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, "esModuleInterop": true }, - "include": ["src/*"], - "paths": { - "*": ["node_modules"] - }, + "include": ["src/*", "public/locales/*"], "exclude": [ "node_modules" ] diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..05e711f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + include: ['tests/unit/**/*.{test,spec}.{ts,tsx}'], + setupFiles: ['tests/unit/setup.ts'], + clearMocks: true, + restoreMocks: true, + css: false, + coverage: { + provider: 'v8', + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/index.tsx'], + reporter: ['text', 'lcov'], + }, + }, +}); diff --git a/webpack.config.js b/webpack.config.js index 56a75cf..0282138 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,8 +5,8 @@ const path = require('path'); module.exports = { entry: './src/index.tsx', output: { - filename: 'PickRandomUserPlugin.js', - library: 'PickRandomUserPlugin', + filename: 'BbbPluginPickRandomUser.js', + library: 'BbbPluginPickRandomUser', libraryTarget: 'umd', publicPath: '/', globalObject: 'this', @@ -17,8 +17,18 @@ module.exports = { host: '0.0.0.0', hot: false, liveReload: false, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', + 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization, ngrok-skip-browser-warning', + }, client: { overlay: false, + webSocketURL: { + hostname: 'localhost', + port: 4701, + protocol: 'ws', + }, }, onBeforeSetupMiddleware: (devServer) => { if (!devServer) { @@ -53,14 +63,12 @@ module.exports = { }, resolve: { extensions: ['.js', '.jsx', '.tsx', '.ts'], - alias: { - '@locales': path.resolve(__dirname, './locales'), - }, }, plugins: [ new CopyWebpackPlugin({ patterns: [ { from: 'manifest.json', to: './' }, // Copy manifest.json to static/ in the output folder + { from: 'public/locales/', to: './locales/' }, // Copy all locales to the output folder ], }), ],