diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ae2cf6294d..ad1804bfc5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,6 +16,7 @@ /src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers /playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers /playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers +/packages/shared-components/src/crypto/ @element-hq/element-crypto-web-reviewers /src/models/Call.ts @element-hq/element-call-reviewers @@ -25,7 +26,10 @@ # Ignore translations as those will be updated by GHA for Localazy download /src/i18n/strings -/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers +/packages/shared-components/src/i18n/strings +/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers +/packages/shared-components/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers + # Ignore the synapse & mas plugins as this is updated by GHA for docker image updating /playwright/testcontainers/synapse.ts /playwright/testcontainers/mas.ts diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 759266d4e0..86ab82944a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,7 @@ ## Checklist -- [ ] I have read through [review guidelines](../docs/review.md) and [CONTRIBUTING.md](../CONTRIBUTING.md). +- [ ] I have read through [review guidelines](https://github.com/element-hq/element-web/blob/develop/docs/review.md) and [CONTRIBUTING.md](https://github.com/element-hq/element-web/blob/develop/CONTRIBUTING.md). - [ ] Tests written for new code (and old code if feasible). - [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation. - [ ] Linter and other CI checks pass. diff --git a/.github/labels.yml b/.github/labels.yml index 649e1a7407..8cf5613f92 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -173,6 +173,12 @@ color: "bfd4f2" - name: "A-Welcome-Page" color: "bfd4f2" +- name: "A-Install" + color: "72A447" +- name: "A-Seshat" + color: "8262BE" +- name: "A-Update" + color: "17BE67" - name: "backport staging" description: "Label to automatically backport PR to staging branch" color: "B60205" @@ -282,3 +288,23 @@ - name: "Z-Skip-Coverage" description: "Skip SonarQube coverage for this PR" color: "ededed" +- name: "Z-Arch" + color: "D601BE" +- name: "Z-ARM" + color: "5DEC5B" +- name: "Z-Flatpak" + color: "0CA856" +- name: "Z-Linux" + color: "7B4A9C" +- name: "Z-macOS" + color: "500605" +- name: "Z-Official" + color: "1D2B20" +- name: "Z-Snap" + color: "29CD95" +- name: "Z-Suse" + color: "79D07B" +- name: "Z-Wayland" + color: "94C519" +- name: "Z-Windows" + color: "0632DE" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74425f25b4..5d863a6a32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: # Disable cache on Windows as it is slower than not caching # https://github.com/actions/setup-node/issues/975 @@ -66,7 +66,7 @@ jobs: run: VERSION=$(scripts/get-version-from-git.sh) yarn build - name: Upload Artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: webapp-${{ matrix.image }} path: webapp diff --git a/.github/workflows/build_debian.yaml b/.github/workflows/build_debian.yaml index 48c21c9dc5..6ad62f85aa 100644 --- a/.github/workflows/build_debian.yaml +++ b/.github/workflows/build_debian.yaml @@ -62,7 +62,7 @@ jobs: dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: element-web.deb path: element-web.deb diff --git a/.github/workflows/build_develop.yml b/.github/workflows/build_develop.yml index a923e1db1d..72ebd40d25 100644 --- a/.github/workflows/build_develop.yml +++ b/.github/workflows/build_develop.yml @@ -28,7 +28,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version: "lts/*" @@ -53,7 +53,7 @@ jobs: - run: mv dist/element-*.tar.gz dist/develop.tar.gz - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: webapp path: dist/develop.tar.gz @@ -104,7 +104,7 @@ jobs: running-workflow-name: "Build & Deploy develop.element.io" repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 10 - check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$ + check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages|Upload).)*$ # We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier # as the expires after 24h and requires auth to download. diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 5c2e989e28..a11da471f9 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -32,25 +32,10 @@ jobs: uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 with: install: true - - name: Login to Docker Hub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 - if: github.event_name != 'pull_request' - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 - if: github.event_name != 'pull_request' - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and load id: test-build uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 @@ -102,12 +87,64 @@ jobs: images: | vectorim/element-web ghcr.io/element-hq/element-web + oci-push.vpn.infra.element.io/element-web tags: | type=ref,event=branch type=ref,event=tag flavor: | latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }} + - name: Login to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + if: github.event_name != 'pull_request' + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + if: github.event_name != 'pull_request' + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Connect to Tailscale + uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4 + if: github.event_name != 'pull_request' + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + audience: ${{ secrets.TS_AUDIENCE }} + tags: tag:github-actions + + - name: Compute vault jwt role name + id: vault-jwt-role + if: github.event_name != 'pull_request' + run: | + echo "role_name=github_service_management_$( echo "${{ github.repository }}" | sed -r 's|[/-]|_|g')" | tee -a "$GITHUB_OUTPUT" + + - name: Get team registry token + id: import-secrets + uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3 + if: github.event_name != 'pull_request' + with: + url: https://vault.infra.ci.i.element.dev + role: ${{ steps.vault-jwt-role.outputs.role_name }} + path: service-management/github-actions + jwtGithubAudience: https://vault.infra.ci.i.element.dev + method: jwt + secrets: | + services/web-repositories/secret/data/oci.element.io username | OCI_USERNAME ; + services/web-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; + + - name: Login to oci.element.io Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + if: github.event_name != 'pull_request' + with: + registry: oci-push.vpn.infra.element.io + username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} + password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }} + - name: Build and push id: build-and-push uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 @@ -139,16 +176,3 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} repository: vectorim/element-web - - - name: Repository Dispatch - uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 - if: github.event_name != 'pull_request' - with: - repository: element-hq/element-web-pro - token: ${{ secrets.ELEMENT_BOT_TOKEN }} - event-type: image-built - # Stable way to determine the :version - client-payload: |- - { - "base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}" - } diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9020f7914b..a3e9e40247 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,7 +33,7 @@ jobs: repository: matrix-org/matrix-js-sdk path: matrix-js-sdk - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" cache-dependency-path: element-web/yarn.lock diff --git a/.github/workflows/end-to-end-tests-netlify.yaml b/.github/workflows/end-to-end-tests-netlify.yaml index 049f4ea343..a08efa220b 100644 --- a/.github/workflows/end-to-end-tests-netlify.yaml +++ b/.github/workflows/end-to-end-tests-netlify.yaml @@ -25,7 +25,7 @@ jobs: actions: read steps: - name: Download HTML report - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 1b366333b6..304e4468e2 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -54,7 +54,7 @@ jobs: with: repository: element-hq/element-web - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version: "lts/*" @@ -74,7 +74,7 @@ jobs: run: VERSION=$(scripts/get-version-from-git.sh) yarn build - name: Upload Artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: webapp path: webapp @@ -128,12 +128,12 @@ jobs: repository: element-hq/element-web - name: 📥 Download artifact - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: webapp path: webapp - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" cache-dependency-path: yarn.lock @@ -147,7 +147,7 @@ jobs: run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT - name: Cache playwright binaries - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 id: playwright-cache with: path: ~/.cache/ms-playwright @@ -172,7 +172,7 @@ jobs: - name: Upload blob report to GitHub Actions Artifacts if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }} path: blob-report @@ -200,7 +200,7 @@ jobs: persist-credentials: false repository: element-hq/element-web - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 if: inputs.skip != true with: cache: "yarn" @@ -212,7 +212,7 @@ jobs: - name: Download blob reports from GitHub Actions Artifacts if: inputs.skip != true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: pattern: all-blob-reports-* path: all-blob-reports @@ -228,7 +228,7 @@ jobs: # Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected - name: Upload HTML report if: always() && inputs.skip != true - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: html-report path: playwright-report diff --git a/.github/workflows/localazy_upload.yaml b/.github/workflows/localazy_upload.yaml index 8cb7743968..ad23f350f7 100644 --- a/.github/workflows/localazy_upload.yaml +++ b/.github/workflows/localazy_upload.yaml @@ -4,6 +4,7 @@ on: branches: [develop] paths: - "src/i18n/strings/en_EN.json" + - "packages/shared-components/src/i18n/strings/en_EN.json" permissions: {} # No permissions needed jobs: upload: diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index ab2433c267..9507567ef6 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -28,7 +28,7 @@ jobs: Exercise caution. Use test accounts. - name: 📥 Download artifact - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/playwright-image-updates.yaml b/.github/workflows/playwright-image-updates.yaml index 1ce3d767a7..d0cd242884 100644 --- a/.github/workflows/playwright-image-updates.yaml +++ b/.github/workflows/playwright-image-updates.yaml @@ -32,7 +32,7 @@ jobs: - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8 with: token: ${{ secrets.ELEMENT_BOT_TOKEN }} branch: actions/playwright-image-updates diff --git a/.github/workflows/shared-component-publish.yaml b/.github/workflows/shared-component-publish.yaml index 6869c17660..a1b6a0c882 100644 --- a/.github/workflows/shared-component-publish.yaml +++ b/.github/workflows/shared-component-publish.yaml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: 🔧 Set up node environment - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version-file: ".node-version" @@ -37,4 +37,4 @@ jobs: - name: 🚀 Publish to npm working-directory: packages/shared-components - run: npm publish --access public --tag test --provenance + run: npm publish --access public --provenance diff --git a/.github/workflows/shared-component-storybook-publish.yaml b/.github/workflows/shared-component-storybook-publish.yaml new file mode 100644 index 0000000000..620f7a48e8 --- /dev/null +++ b/.github/workflows/shared-component-storybook-publish.yaml @@ -0,0 +1,41 @@ +name: Publish shared component storybook +on: + workflow_dispatch: {} + push: + branches: + - "develop" + paths: + - "packages/shared-components/**/*" + +permissions: {} + +jobs: + doc: + name: Publish storybook + runs-on: ubuntu-latest + environment: SharedComponents + steps: + - name: 🧮 Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: 🔧 Yarn cache + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + with: + cache: "yarn" + node-version-file: package.json + + - name: 🔨 Install dependencies + working-directory: packages/shared-components + run: "yarn install --pure-lockfile" + + - name: 📖 Build Storybook + working-directory: packages/shared-components + run: yarn build:storybook + + - name: 🚀 Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3 + with: + apiToken: ${{ secrets.CF_PAGES_TOKEN }} + accountId: ${{ secrets.CF_PAGES_ACCOUNT_ID }} + workingDirectory: "packages/shared-components" + command: pages deploy storybook-static --project-name=shared-components-storybook diff --git a/.github/workflows/shared-component-visual-tests-netlify.yaml b/.github/workflows/shared-component-visual-tests-netlify.yaml index 816d899836..950dd8febf 100644 --- a/.github/workflows/shared-component-visual-tests-netlify.yaml +++ b/.github/workflows/shared-component-visual-tests-netlify.yaml @@ -27,7 +27,7 @@ jobs: run: "sudo apt-get install -y tree" - name: Download Diffs - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/shared-component-visual-tests.yaml b/.github/workflows/shared-component-visual-tests.yaml index 8f9cbdae31..b2015f0a08 100644 --- a/.github/workflows/shared-component-visual-tests.yaml +++ b/.github/workflows/shared-component-visual-tests.yaml @@ -26,14 +26,11 @@ jobs: persist-credentials: false repository: element-hq/element-web - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version: "lts/*" - - name: Install element web dependencies - run: yarn install --frozen-lockfile - - name: Install dependencies working-directory: packages/shared-components run: yarn install --frozen-lockfile @@ -44,7 +41,7 @@ jobs: run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT - name: Cache playwright binaries - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 id: playwright-cache with: path: ~/.cache/ms-playwright @@ -56,11 +53,18 @@ jobs: run: "yarn playwright install --with-deps --only-shell" - name: Run Visual tests - run: "yarn --cwd packages/shared-components test:storybook:ci" + working-directory: packages/shared-components + run: "yarn test:storybook --run" + + # Workaround for vis silently adding new baselines if they didn't exist + # Can be removed once https://github.com/repobuddy/visual-testing/issues/516 is released + - run: | + git add -N . + git diff --exit-code - name: Upload received images & diffs if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: received-images - path: packages/shared-components/playwright/shared-component-received + path: packages/shared-components/__vis__/linux diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 9f90572371..c26ee1b487 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version: "lts/*" @@ -41,8 +41,8 @@ jobs: - name: Typecheck Shared Components run: "yarn --cwd packages/shared-components run lint:types" - i18n_lint: - name: "i18n Check" + i18n_lint_ew: + name: "i18n Check (Element Web)" uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main permissions: pull-requests: read @@ -59,6 +59,15 @@ jobs: devtools|settings|elementCallUrl labs|sliding_sync_description + i18n_lint_shared_components: + name: "i18n Check (Shared Components)" + uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main + permissions: + pull-requests: read + with: + path: "packages/shared-components" + hardcoded-words: "Element" + rethemendex_lint: name: "Rethemendex Check" runs-on: ubuntu-24.04 @@ -75,7 +84,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version: "lts/*" @@ -99,7 +108,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version: "lts/*" @@ -117,7 +126,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version: "lts/*" @@ -135,7 +144,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version: "lts/*" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 95382b180c..4b90ca2cb5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,7 +44,7 @@ jobs: repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }} - name: Yarn cache - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: node-version: "lts/*" cache: "yarn" @@ -55,7 +55,7 @@ jobs: JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }} - name: Jest Cache - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 with: path: /tmp/jest_cache key: ${{ hashFiles('**/yarn.lock') }} @@ -84,7 +84,7 @@ jobs: - name: Upload Artifact if: env.ENABLE_COVERAGE == 'true' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: coverage-${{ matrix.runner }} path: | @@ -93,18 +93,18 @@ jobs: complete: name: jest-tests - needs: jest_ew + needs: [jest_ew, vitest_sc] if: always() runs-on: ubuntu-24.04 permissions: statuses: write steps: - - if: needs.jest_ew.result != 'skipped' && needs.jest_ew.result != 'success' + - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') run: exit 1 - name: Skip SonarCloud in merge queue if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' - uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1 + uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success @@ -113,8 +113,8 @@ jobs: sha: ${{ github.sha }} target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - jest_sc: - name: Jest (Shared Components) + vitest_sc: + name: Vitest (Shared Components) runs-on: ubuntu-24.04 steps: - name: Checkout code @@ -123,43 +123,47 @@ jobs: repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }} - name: Yarn cache - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: node-version: "lts/*" cache: "yarn" - - name: Install EW Deps - run: "yarn install" - - name: Install Shared Component Deps working-directory: "packages/shared-components" run: "yarn install" - - name: Jest Cache - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + - name: Cache storybook & vitest + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 with: - path: /tmp/jest_cache - key: ${{ hashFiles('**/yarn.lock') }} + path: | + packages/shared-components/node_modules/.cache + packages/shared-components/node_modules/.vite/vitest + key: ${{ hashFiles('packages/shared-components/yarn.lock') }} - - name: Get number of CPU cores - id: cpu-cores - uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2 + - name: Get installed Playwright version + working-directory: packages/shared-components + id: playwright + run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT + + - name: Cache playwright binaries + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell + + - name: Install Playwright browsers + working-directory: packages/shared-components + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: "yarn playwright install --with-deps --only-shell" - name: Run tests working-directory: "packages/shared-components" - run: | - yarn test \ - --coverage=${{ env.ENABLE_COVERAGE }} \ - --ci \ - --max-workers ${{ steps.cpu-cores.outputs.count }} \ - --cacheDirectory /tmp/jest_cache - env: - # tell jest to use coloured output - FORCE_COLOR: true + run: yarn test:unit --coverage=${{ env.ENABLE_COVERAGE }} - name: Upload Artifact if: env.ENABLE_COVERAGE == 'true' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: coverage-sharedcomponents path: | diff --git a/.github/workflows/triage-move-review-requests.yml b/.github/workflows/triage-move-review-requests.yml index 4d8745f4bd..6933233c0a 100644 --- a/.github/workflows/triage-move-review-requests.yml +++ b/.github/workflows/triage-move-review-requests.yml @@ -9,7 +9,7 @@ jobs: name: Move PRs asking for design review to the design board runs-on: ubuntu-24.04 steps: - - uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0 + - uses: octokit/graphql-action@ddde8ebb2493e79f390e6449c725c21663a67505 # v3.0.2 id: find_team_members with: headers: '{"GraphQL-Features": "projects_next_graphql"}' @@ -52,7 +52,7 @@ jobs: fi env: TEAM: "design" - - uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0 + - uses: octokit/graphql-action@ddde8ebb2493e79f390e6449c725c21663a67505 # v3.0.2 id: add_to_project if: steps.any_matching_reviewers.outputs.match == 'true' with: @@ -76,7 +76,7 @@ jobs: name: Move PRs asking for design review to the design board runs-on: ubuntu-24.04 steps: - - uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0 + - uses: octokit/graphql-action@ddde8ebb2493e79f390e6449c725c21663a67505 # v3.0.2 id: find_team_members with: headers: '{"GraphQL-Features": "projects_next_graphql"}' @@ -119,7 +119,7 @@ jobs: fi env: TEAM: "product" - - uses: octokit/graphql-action@abaeca7ba4f0325d63b8de7ef943c2418d161b93 # v3.0.0 + - uses: octokit/graphql-action@ddde8ebb2493e79f390e6449c725c21663a67505 # v3.0.2 id: add_to_project if: steps.any_matching_reviewers.outputs.match == 'true' with: diff --git a/.github/workflows/update-jitsi.yml b/.github/workflows/update-jitsi.yml index 67e3fb19c5..b37a2967d4 100644 --- a/.github/workflows/update-jitsi.yml +++ b/.github/workflows/update-jitsi.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: cache: "yarn" node-version: "lts/*" @@ -23,7 +23,7 @@ jobs: run: "yarn update:jitsi" - name: Create Pull Request - uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8 with: token: ${{ secrets.ELEMENT_BOT_TOKEN }} branch: actions/jitsi-update diff --git a/.gitignore b/.gitignore index 3d6d723ac3..870489a7bd 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ electron/pub /index.html # version file and tarball created by `npm pack` / `yarn pack` /git-revision.txt +jest-sonar.xml *storybook.log storybook-static @@ -37,3 +38,6 @@ storybook-static /packages/shared-components/node_modules /packages/shared-components/dist /packages/shared-components/src/i18nKeys.d.ts + +# TSC incremental compilation information +*.tsbuildinfo diff --git a/.stylelintrc.js b/.stylelintrc.js index 3244d122c5..2cbebc0472 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -56,7 +56,6 @@ module.exports = { { from: "res/css/views/rooms/_EditMessageComposer.pcss", type: "css" }, { from: "res/css/views/right_panel/_BaseCard.pcss", type: "css" }, { from: "res/css/views/messages/_MessageTimestamp.pcss", type: "css" }, - { from: "res/css/views/messages/_EventTileBubble.pcss", type: "css" }, { from: "res/css/views/messages/_MessageActionBar.pcss", type: "css" }, { from: "res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss", type: "css" }, { from: "res/css/views/elements/_ToggleSwitch.pcss", type: "css" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 43546c1817..4128a2b75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,84 @@ +Changes in [1.12.9](https://github.com/element-hq/element-web/releases/tag/v1.12.9) (2026-01-27) +================================================================================================ +## ✨ Features + +* Allow local log downloads when a rageshake URL is not configured. ([#31716](https://github.com/element-hq/element-web/pull/31716)). Contributed by @Half-Shot. +* Improve icon rendering accessibility ([#31776](https://github.com/element-hq/element-web/pull/31776)). Contributed by @t3chguy. +* Show "Bob shared this message" on messages shared via MSC4268 ([#31684](https://github.com/element-hq/element-web/pull/31684)). Contributed by @richvdh. +* Update the way we render icons for accessibility ([#31731](https://github.com/element-hq/element-web/pull/31731)). Contributed by @t3chguy. +* Switch from css masks to rendering svg ([#31681](https://github.com/element-hq/element-web/pull/31681)). Contributed by @t3chguy. +* Support for stable MSC4191 account management action parameter ([#31701](https://github.com/element-hq/element-web/pull/31701)). Contributed by @hughns. +* Support for stable m.oauth UIA stage from MSC4312 ([#31704](https://github.com/element-hq/element-web/pull/31704)). Contributed by @hughns. +* Switch to Compound icons to replace old icons ([#31667](https://github.com/element-hq/element-web/pull/31667)). Contributed by @t3chguy. +* Switch from svg masks to svg rendering in more places ([#31652](https://github.com/element-hq/element-web/pull/31652)). Contributed by @t3chguy. +* Switch from svg masks to svg rendering in more places ([#31650](https://github.com/element-hq/element-web/pull/31650)). Contributed by @t3chguy. +* Update notification icons using Compound icons ([#31671](https://github.com/element-hq/element-web/pull/31671)). Contributed by @t3chguy. +* Memoise ListView context ([#31668](https://github.com/element-hq/element-web/pull/31668)). Contributed by @t3chguy. +* Switch emoji picker to use emoji for header icons ([#31645](https://github.com/element-hq/element-web/pull/31645)). Contributed by @t3chguy. +* Replace icons with Compound alternatives ([#31642](https://github.com/element-hq/element-web/pull/31642)). Contributed by @t3chguy. + +## 🐛 Bug Fixes + +* Fix avatar decorations in thread activity centre not being atop avatar ([#31789](https://github.com/element-hq/element-web/pull/31789)). Contributed by @t3chguy. +* Fix room settings roles tab getting confused if power level change fails ([#31768](https://github.com/element-hq/element-web/pull/31768)). Contributed by @t3chguy. +* Custom themes now import highlights in css ([#31758](https://github.com/element-hq/element-web/pull/31758)). Contributed by @Philldomd. +* Use correct translation for url preview settings ([#31740](https://github.com/element-hq/element-web/pull/31740)). Contributed by @florianduros. +* Fix error shown if accepting a 3pid invite ([#31735](https://github.com/element-hq/element-web/pull/31735)). Contributed by @dbkr. +* Ensure correct focus configuration for Element Call before allowing users to call. ([#31490](https://github.com/element-hq/element-web/pull/31490)). Contributed by @Half-Shot. +* Fix emoji font in emoji picker header buttons ([#31679](https://github.com/element-hq/element-web/pull/31679)). Contributed by @t3chguy. +* fix flaky test by waiting for chat panel before counting messages ([#31633](https://github.com/element-hq/element-web/pull/31633)). Contributed by @BillCarsonFr. + + +Changes in [1.12.8](https://github.com/element-hq/element-web/releases/tag/v1.12.8) (2026-01-13) +================================================================================================ +## 🦖 Deprecations + +* Remove `element_call.participant_limit` config and associated code. ([#31638](https://github.com/element-hq/element-web/pull/31638)). Contributed by @Half-Shot. + +## ✨ Features + +* Switch to rendering svg icons rather than masking them ([#31557](https://github.com/element-hq/element-web/pull/31557)). Contributed by @t3chguy. +* Update history visibility UX ([#31635](https://github.com/element-hq/element-web/pull/31635)). Contributed by @langleyd. +* Show correct call icon for joining a call. ([#31489](https://github.com/element-hq/element-web/pull/31489)). Contributed by @Half-Shot. +* Update StopGapWidgetDriver to support sticky events ([#31205](https://github.com/element-hq/element-web/pull/31205)). Contributed by @Half-Shot. +* Remove release announcements for new sounds \& room list ([#31544](https://github.com/element-hq/element-web/pull/31544)). Contributed by @t3chguy. +* Add button to restore from backup into /devtools ([#31581](https://github.com/element-hq/element-web/pull/31581)). Contributed by @mxandreas. +* Switch to non-solid compound icons for room settings \& composer ([#31561](https://github.com/element-hq/element-web/pull/31561)). Contributed by @t3chguy. +* Support encrypted state events MSC4362 ([#31513](https://github.com/element-hq/element-web/pull/31513)). Contributed by @andybalaam. +* Update prop type \& documentation for HistoryVisibleBanner and VM. ([#31545](https://github.com/element-hq/element-web/pull/31545)). Contributed by @kaylendog. +* Switch to Compound icons in more places ([#31560](https://github.com/element-hq/element-web/pull/31560)). Contributed by @t3chguy. +* Switch to rendering svg icons rather than masking them ([#31550](https://github.com/element-hq/element-web/pull/31550)). Contributed by @t3chguy. +* Make AccessibleButton contrast control compatible ([#31308](https://github.com/element-hq/element-web/pull/31308)). Contributed by @t3chguy. +* Switch to compound-design-tokens for platform icons ([#31543](https://github.com/element-hq/element-web/pull/31543)). Contributed by @t3chguy. +* Switch to rendering svg icons rather than masking them ([#31531](https://github.com/element-hq/element-web/pull/31531)). Contributed by @t3chguy. +* Switch to rendering svg icons rather than css masking ([#31517](https://github.com/element-hq/element-web/pull/31517)). Contributed by @t3chguy. +* Auto approve matrix rtc member event (`m.rtc.member`) (sticky events) ([#31452](https://github.com/element-hq/element-web/pull/31452)). Contributed by @toger5. +* Size Autocomplete relative to the RoomView height rather than the viewport height ([#31425](https://github.com/element-hq/element-web/pull/31425)). Contributed by @langleyd. +* Implement UI for history visibility acknowledgement. ([#31156](https://github.com/element-hq/element-web/pull/31156)). Contributed by @kaylendog. +* Export disposing hook from package ([#31498](https://github.com/element-hq/element-web/pull/31498)). Contributed by @MidhunSureshR. +* Change `header-panel-bg-hover` to use `var(--cpd-color-bg-action-secondary-hovered)` for better custom theming ([#31457](https://github.com/element-hq/element-web/pull/31457)). Contributed by @th0mcat. +* Improve icon rendering in iconized context menu ([#31458](https://github.com/element-hq/element-web/pull/31458)). Contributed by @t3chguy. + +## 🐛 Bug Fixes + +* [Backport staging] Fix space settings visibility tab crashing ([#31705](https://github.com/element-hq/element-web/pull/31705)). Contributed by @RiotRobot. +* Fix expand/collapse reply preview not showing in some cases ([#31639](https://github.com/element-hq/element-web/pull/31639)). Contributed by @t3chguy. +* Fix bundled font or custom font not applied after theme switch ([#31591](https://github.com/element-hq/element-web/pull/31591)). Contributed by @florianduros. +* Add ol override CSS for markdown-body ([#31618](https://github.com/element-hq/element-web/pull/31618)). Contributed by @niamu. +* Fix reaction left margin in timeline card ([#31625](https://github.com/element-hq/element-web/pull/31625)). Contributed by @t3chguy. +* Open right panel timeline when jumping to event with maximised widget ([#31626](https://github.com/element-hq/element-web/pull/31626)). Contributed by @t3chguy. +* Fix Compound Link elements not having an underline. ([#31583](https://github.com/element-hq/element-web/pull/31583)). Contributed by @Half-Shot. +* Recalculate mentions metadata of forwarded messages based on message body ([#31193](https://github.com/element-hq/element-web/pull/31193)). Contributed by @twassman. +* Fix Room Preview Card Layout ([#31611](https://github.com/element-hq/element-web/pull/31611)). Contributed by @germain-gg. +* Fix: WidgetMessaging not properly closed causing side effects and bugs ([#31598](https://github.com/element-hq/element-web/pull/31598)). Contributed by @BillCarsonFr. +* Handle cross-signing keys missing locally and/or from secret storage ([#31367](https://github.com/element-hq/element-web/pull/31367)). Contributed by @uhoreg. +* fix: Allow wrapping in `Banner` component. ([#31532](https://github.com/element-hq/element-web/pull/31532)). Contributed by @kaylendog. +* Update algorithm for history visible banner. ([#31577](https://github.com/element-hq/element-web/pull/31577)). Contributed by @kaylendog. +* Fix styling issue when using EW modules ([#31533](https://github.com/element-hq/element-web/pull/31533)). Contributed by @florianduros. +* Prevent history visible banner from displaying in threads. ([#31535](https://github.com/element-hq/element-web/pull/31535)). Contributed by @kaylendog. +* Make the feedback icon be the right color in dark theme ([#31527](https://github.com/element-hq/element-web/pull/31527)). Contributed by @robintown. + + Changes in [1.12.7](https://github.com/element-hq/element-web/releases/tag/v1.12.7) (2025-12-16) ================================================================================================ ## ✨ Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ec59b2eff..e269853d7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -184,6 +184,16 @@ Please ensure your changes match the cosmetic style of the existing project, and **_never_** mix cosmetic and functional changes in the same commit, as it makes it horribly hard to review otherwise. +## Shared Components + +When creating new UI components, consider whether they should be added to the shared components package (`packages/shared-components`) rather than directly in the main `src/` directory. Components should be placed in shared components if they: + +- Are reusable across different parts of the application +- Could potentially be used by other Element projects (Element Desktop, Aurora, Element modules...) +- Follow established patterns and don't have tight coupling to specific application logic + +For more details, see the [shared components README](./packages/shared-components/README.md). + ## Attribution Everyone who contributes anything to Matrix is welcome to be listed in the diff --git a/Dockerfile b/Dockerfile index 5ce43a7c4f..9b418ac7fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -# syntax=docker.io/docker/dockerfile:1.20-labs@sha256:dbcde2ebc4abc8bb5c3c499b9c9a6876842bf5da243951cd2697f921a7aeb6a9 +# syntax=docker.io/docker/dockerfile:1.21-labs@sha256:2e681d22e86e738a057075f930b81b2ab8bc2a34cd16001484a7453cfa7a03fb # Builder -FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:5583cbe5d3347db372d9a9100eba272b548ca1f53246b9b769334bcd0eef2c26 AS builder +FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:8036dbe5b1f465e3acb8b866031cd06e4f84c31b0e83dabbdc59397a40dbe288 AS builder # Support custom branch of the js-sdk. This also helps us build images of element-web develop. ARG USE_CUSTOM_SDKS=false @@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh RUN cp /src/config.sample.json /src/webapp/config.json # App -FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:a6bec37058b9047ece799c01d98dc6d5aa0542b6583cc69f187652f91331a752 +FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:9ac6a908ed07ba7d23cbf6048090453a081abf663c53a7c3f3bf96abc16c0799 # Need root user to install packages & manipulate the usr directory USER root diff --git a/README.md b/README.md index 6f6e3172fa..bf0e10551d 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,17 @@ To add a new translation, head to the [translating doc](docs/translating.md). For a developer guide, see the [translating dev doc](docs/translating-dev.md). +# Extending Element Web with Modules + +Element Web supports a module system that allows you to extend or modify functionality at runtime. Modules are loaded dynamically and provide a safe, predictable API for customization. + +## What are modules? + +Modules are extensions that can add or modify Element Web's functionality. They are: + +- Built using the [`@element-hq/element-web-module-api`](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) +- Loaded in EW via [config.json](docs/config.md#modules) + # Triaging issues Issues are triaged by community members and the Web App Team, following the [triage process](https://github.com/element-hq/element-meta/wiki/Triage-process). diff --git a/code_style.md b/code_style.md index 6e7289d22e..bd1fb4371c 100644 --- a/code_style.md +++ b/code_style.md @@ -272,20 +272,44 @@ Inheriting all the rules of TypeScript, the following additionally apply: 18. Components should serve a single, or near-single, purpose. 19. Prefer to derive information from component properties rather than establish state. 20. Do not use `React.Component::forceUpdate`. +21. Prefer to use [compound typography components](https://compound.element.io/?path=/docs/compound-web_typography--docs) instead of raw HTML elements for text. This ensures consistent font usage and letter spacing across the app. +22. If you can't use 21, don't forget to apply the correct CSS classes for font and letter spacing. +23. Prefer to use `Flex` or `Box` components from shared-components for layout instead of raw HTML elements with CSS flexbox styles. -## Stylesheets (\*.pcss = PostCSS + Plugins) +## Stylesheets -Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not. +1. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers. +2. Components should render only within the bounding box of their outermost DOM element. Page-absolute positioning and negative CSS margins and similar are generally not cool and stop the component from being reused easily in different places. -1. The view's CSS file MUST have the same name as the component (e.g. `view/rooms/_MessageTile.css` for `MessageTile.tsx` component). +### PostCSS (\*.pcss = PostCSS + Plugins) + +> [!NOTE] +> We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not. + +**PostCSS should be use when working in the main Element Web codebase (not shared-components).** + +#### Naming and file structure + +1. The view's CSS file MUST have the same name as the component (e.g. `res/css/components/views/rooms/_RoomTile.pcss` for `RoomTile.tsx` component). 2. Per-view CSS is optional - it could choose to inherit all its styling from the context of the rest of the app, although this is unusual. -3. Class names must be prefixed with "mx\_". +3. Class names must be prefixed with `mx_`. 4. Class names must strictly denote the component which defines them. For example: `mx_MyFoo` for `MyFoo` component. -5. Class names for DOM elements within a view which aren't components are named by appending a lower camel case identifier to the view's class name - e.g. .mx_MyFoo_randomDiv is how you'd name the class of an arbitrary div within the MyFoo view. -6. Use the `$font` variables instead of manual values. -7. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers. -8. Use the whole class name instead of shortcuts: +5. Class names for DOM elements within a view which aren't components are named by appending a lower camel case identifier to the view's class name. + For example: `.mx_MyFoo_randomDiv` is how you'd name the class of an arbitrary div within the MyFoo view. + +#### Variables + +6. Use the `$font-*` variables instead of manual font-size values (e.g., `$font-12px`, `$font-15px`). + - Note: These are deprecated. Prefer Compound typography tokens like `var(--cpd-font-body-md-regular)` for new code. +7. Use theme color variables like `$primary-content`, `$secondary-content`, `$accent`, `$alert` for colors. + - Prefer Compound color tokens like `var(--cpd-color-text-primary)` for new code. +8. Use spacing variables like `$spacing-8`, `$spacing-12`, `$spacing-16` where available. + - Prefer Compound spacing tokens like `var(--cpd-space-2x)` for new code. + +#### Syntax and formatting + +10. Use the whole class name instead of shortcuts: ```scss .mx_MyFoo { @@ -296,7 +320,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b } ``` -9. Break multiple selectors over multiple lines this way: +11. Break multiple selectors over multiple lines this way: ```scss .mx_MyFoo, @@ -306,8 +330,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b } ``` -10. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming. -11. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be +12. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be [documented](#comments) for what the values mean: ```scss @@ -318,9 +341,81 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b } ``` -12. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why. -13. The CSS for a component can override the rules for child components. For instance, .mxRoomList .mx_RoomTile {} would be the selector to override styles of RoomTiles when viewed in the context of a RoomList view. Overrides must be scoped to the View's CSS class - i.e. don't just define .mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override only to the context of RoomList views. N.B. overrides should be relatively rare as in general CSS inheritance should be enough. -14. Components should render only within the bounding box of their outermost DOM element. Page-absolute positioning and negative CSS margins and similar are generally not cool and stop the component from being reused easily in different places. +13. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why. + +#### Component overrides + +14. The CSS for a component can override the rules for child components. For instance, `.mx_RoomList .mx_RoomTile {}` would be the selector to override styles of RoomTiles when viewed in the context of a RoomList view. Overrides must be scoped to the View's CSS class - i.e. don't just define `.mx_RoomTile {}` in RoomList.pcss - only RoomTile.pcss is allowed to define its own CSS. Instead, say `.mx_RoomList .mx_RoomTile {}` to scope the override only to the context of RoomList views. N.B. overrides should be relatively rare as in general CSS inheritance should be enough. + +### CSS module (\*.module.css) + +**CSS modules provide locally-scoped class names and are the preferred approach for new shared components.** + +#### Naming and file structure + +1. The CSS module file MUST have the same name as the component with `.module.css` extension. + For example: `PlayPauseButton.module.css` for `PlayPauseButton.tsx`. +2. Place the CSS module file in the same directory as the component. +3. Class names should be semantic and describe their purpose, NOT prefixed with `mx_`. + For example: `.button`, `.label`, `.content`, `.title`. +4. Use camelCase for multi-word class names: `.playButton`, `.primaryAction`, `.errorMessage`. + +#### Importing and usage + +5. Import the CSS module as `styles`: + + ```tsx + import styles from "./MyComponent.module.css"; + ``` + +6. Apply classes using the styles object: + + ```tsx +
+ {label} + +
+ ``` + +7. Combine multiple classes using `classNames` utility: + + ```tsx + import classNames from "classnames"; + +
+ {label} +
; + ``` + +#### Styling guidelines + +8. Use Compound Design Tokens for all styling values: + - Colors: `var(--cpd-color-bg-subtle-primary)`, `var(--cpd-color-text-primary)` + - Typography: `var(--cpd-font-body-md-regular)`, `var(--cpd-font-heading-sm-semibold)` + - Spacing: `var(--cpd-space-2x)`, `var(--cpd-space-4x)` + - Border radius: `var(--cpd-radius-pill-effect)` or standard px values for design-specific needs + +9. Use CSS custom properties for component-specific themeable values: + + ```css + .flex { + display: var(--mx-flex-display, unset); + flex-direction: var(--mx-flex-direction, unset); + align-items: var(--mx-flex-align, unset); + gap: var(--mx-flex-gap, unset); + } + ``` + +10. Avoid nesting selectors when possible. CSS modules provide scoping, so nesting is rarely needed. +11. If `!important` is necessary (e.g., to override Compound component styles), add a [comment](#comments) explaining why: + + ```css + .button { + background-color: var(--cpd-color-bg-subtle-primary) !important; /* override Compound default */ + } + ``` + +12. CSS modules do not support PostCSS variables (`$variable`). Always use CSS custom properties (`var(--variable)`) or direct values. ## Tests diff --git a/config.sample.json b/config.sample.json index 3bc875cb7e..54656418bd 100644 --- a/config.sample.json +++ b/config.sample.json @@ -43,7 +43,6 @@ }, "element_call": { "url": "https://call.element.io", - "participant_limit": 8, "brand": "Element Call" }, "map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx" diff --git a/docs/MVVM.md b/docs/MVVM.md index 6175e21853..756a974391 100644 --- a/docs/MVVM.md +++ b/docs/MVVM.md @@ -8,7 +8,13 @@ General description of the pattern can be found [here](https://en.wikipedia.org/ If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it. -### Practical guidelines for MVVM in element-web +## Why are we using MVVM? + +1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes. +2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa. +3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md). + +## Practical guidelines for MVVM in element-web A first documentation and implementation of MVVM was done in [MVVM-v1.md](MVVM-v1.md). This v1 version is now deprecated and this document describes the current implementation. @@ -19,12 +25,12 @@ This is anywhere your data or business logic comes from. If your view model is a #### View 1. Located in [`shared-components`](https://github.com/element-hq/element-web/tree/develop/packages/shared-components). Develop it in storybook! -2. Views are simple react components (eg: `FooView`). -3. Views use [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) internally where the view model is the external store. +2. Views are simple react components (eg: `FooView`) with very little state and logic. +3. Views must call `useViewModel` hook with the corresponding view model passed in as argument. This allows the view to re-render when something has changed in the view model. This entire mechanism is powered by [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore). 4. Views should define the interface of the view model they expect: ```tsx - // Snapshot is the return type of your view model + // Snapshot is the data that your view-model provides which is rendered by the view. interface FooViewSnapshot { value: string; } @@ -34,16 +40,16 @@ This is anywhere your data or business logic comes from. If your view model is a doSomething: () => void; } - // ViewModel is a type defining the methods needed for `useSyncExternalStore` + // ViewModel is an object (usually a class) that implements both the interfaces listed above. // https://github.com/element-hq/element-web/blob/develop/packages/shared-components/src/ViewModel.ts type FooViewModel = ViewModel & FooViewActions; interface FooViewProps { + // Ideally the view only depends on the view model i.e you don't expect any other props here. vm: FooViewModel; } function FooView({ vm }: FooViewProps) { - // useViewModel is a helper function that uses useSyncExternalStore under the hood const { value } = useViewModel(vm); return ( - - - -`; diff --git a/packages/shared-components/src/crypto/SasEmoji/SasEmoji.module.css b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.module.css new file mode 100644 index 0000000000..b763ef6664 --- /dev/null +++ b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.module.css @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.container { + display: flex; + flex-wrap: wrap; + gap: var(--cpd-space-2x); + justify-content: space-evenly; +} + +.segment { + display: inline-block; + margin-bottom: var(--cpd-space-4x); + text-align: center; + /* Allow maximum 4 per line, accounting for 8px gap */ + min-width: calc(25% - 8px); +} + +.emoji { + /* Use the Twemoji font for consistency with other clients */ + font-family: Twemoji, var(--cpd-font-family-sans); + font-size: var(--cpd-font-size-heading-xl); +} + +.label { + font-weight: var(--cpd-font-weight-regular); + font-size: var(--cpd-font-size-body-lg); + color: var(--cpd-color-text-secondary); +} diff --git a/packages/shared-components/src/crypto/SasEmoji/SasEmoji.stories.tsx b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.stories.tsx new file mode 100644 index 0000000000..9c0fcc3f40 --- /dev/null +++ b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.stories.tsx @@ -0,0 +1,48 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type Meta, type StoryObj } from "@storybook/react-vite"; + +import { SasEmoji } from "./SasEmoji"; + +const meta = { + title: "Crypto/SasEmoji", + component: SasEmoji, + tags: ["autodocs"], + args: { + emoji: ["🍕", "🌽", "🚀", "🔒", "🔧", "🍓", "⌛"], + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/XLWIAB5n8yObYvU0INKPK1/Verification-by-Emoji?node-id=1-2935&t=NrV9JnuItrAyyh53-4", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WorstCaseAlbanian: Story = { + globals: { + language: "sq", + }, + args: { + emoji: ["🎅", "🎅", "🎅", "🎅", "🎅", "🎅", "🎅"], + }, +}; + +export const WorstCaseGerman: Story = { + globals: { + language: "de", + }, + args: { + emoji: ["🔧", "🔧", "🔧", "🔧", "🔧", "🔧", "🔧"], + }, +}; diff --git a/packages/shared-components/src/crypto/SasEmoji/SasEmoji.test.tsx b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.test.tsx new file mode 100644 index 0000000000..c9946d52a0 --- /dev/null +++ b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.test.tsx @@ -0,0 +1,19 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { describe, it, expect } from "vitest"; +import { render } from "@test-utils"; + +import { SasEmoji } from "./SasEmoji"; + +describe("", () => { + it("should match snapshot", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/packages/shared-components/src/crypto/SasEmoji/SasEmoji.tsx b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.tsx new file mode 100644 index 0000000000..6701cb37c9 --- /dev/null +++ b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import classNames from "classnames"; + +import { type SasEmoji, tEmoji } from "./SasEmojiTranslate.ts"; +import styles from "./SasEmoji.module.css"; +import { useI18n } from "../../utils/i18nContext.ts"; + +export type Props = { + /** + * The emoji to render + */ + emoji: [SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji]; + /** + * Optional className to apply to the container + */ + className?: string; +}; + +/** + * Renders the 7 emoji used for SAS verification. + * The component is responsive so can be rendered in any context, dialog, side panel. + */ +export function SasEmoji({ emoji, className }: Props): JSX.Element { + const { language } = useI18n(); + + const emojiBlocks = emoji.map((emoji, i) => ( +
+
+ {emoji} +
+
{tEmoji(emoji, language)}
+
+ )); + + return
{emojiBlocks}
; +} diff --git a/packages/shared-components/src/crypto/SasEmoji/SasEmojiTranslate.test.ts b/packages/shared-components/src/crypto/SasEmoji/SasEmojiTranslate.test.ts new file mode 100644 index 0000000000..8251e52ea4 --- /dev/null +++ b/packages/shared-components/src/crypto/SasEmoji/SasEmojiTranslate.test.ts @@ -0,0 +1,26 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { describe, it, expect } from "vitest"; + +import { tEmoji, type SasEmoji } from "./SasEmojiTranslate.ts"; + +describe("tEmoji", () => { + it.each([ + ["🐶", "en-GB", "Dog"], + ["🐶", "en", "Dog"], + ["🐶", "de-DE", "Hund"], + ["🐶", "pt", "Cachorro"], + ["🔧", "de-DE", "Schraubenschlüssel"], + ["🎅", "sq", "Babagjyshi i Vitit të Ri"], + ] as [emoji: SasEmoji, locale: string, expectation: string][])( + "should handle locale %s", + (emoji, locale, expectation) => { + expect(tEmoji(emoji, locale)).toEqual(expectation); + }, + ); +}); diff --git a/packages/shared-components/src/crypto/SasEmoji/SasEmojiTranslate.ts b/packages/shared-components/src/crypto/SasEmoji/SasEmojiTranslate.ts new file mode 100644 index 0000000000..da977a98e0 --- /dev/null +++ b/packages/shared-components/src/crypto/SasEmoji/SasEmojiTranslate.ts @@ -0,0 +1,122 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import SasEmojiJson from "@matrix-org/spec/sas-emoji.json"; +import { getNormalizedLanguageKeys } from "matrix-web-i18n"; + +// Type as specified in https://spec.matrix.org/v1.17/client-server-api/#sas-method-emoji +export type SasEmoji = + | "🐶" + | "🐱" + | "🦁" + | "🐎" + | "🦄" + | "🐷" + | "🐘" + | "🐰" + | "🐼" + | "🐓" + | "🐧" + | "🐢" + | "🐟" + | "🐙" + | "🦋" + | "🌷" + | "🌳" + | "🌵" + | "🍄" + | "🌏" + | "🌙" + | "☁" + | "🔥" + | "🍌" + | "🍎" + | "🍓" + | "🌽" + | "🍕" + | "🎂" + | "❤" + | "😀" + | "🤖" + | "🎩" + | "👓" + | "🔧" + | "🎅" + | "👍" + | "☂" + | "⌛" + | "⏰" + | "🎁" + | "💡" + | "📕" + | "✏" + | "📎" + | "✂" + | "🔒" + | "🔑" + | "🔨" + | "☎" + | "🏁" + | "🚂" + | "🚲" + | "✈" + | "🚀" + | "🏆" + | "⚽" + | "🎸" + | "🎺" + | "🔔" + | "⚓" + | "🎧" + | "📁" + | "📌"; + +const SasEmojiMap = new Map< + SasEmoji, + [ + description: string, + translations: { + [normalizedLanguageKey: string]: string; + }, + ] +>( + SasEmojiJson.map(({ emoji, description, translated_descriptions: translations }) => [ + emoji as SasEmoji, + [ + description, + // Normalize the translation keys + Object.keys(translations).reduce>((o, k) => { + for (const key of getNormalizedLanguageKeys(k)) { + o[key] = translations[k as keyof typeof translations]!; + } + return o; + }, {}), + ], + ]), +); + +/** + * Translate given SAS emoji into the target locale + * @param emoji - the SAS emoji to translate + * @param locale - the BCP 47 locale to translate to, will fall back to English as the base locale for Matrix SAS Emoji. + */ +export function tEmoji(emoji: SasEmoji, locale: string): string { + const mapping = SasEmojiMap.get(emoji); + if (!mapping) { + throw new Error(`Emoji mapping not found for emoji ${emoji}`); + } + + const [description, translations] = mapping; + + for (const key of getNormalizedLanguageKeys(locale)) { + if (translations[key]) { + return translations[key]; + } + } + + return description; +} diff --git a/packages/shared-components/src/crypto/SasEmoji/__snapshots__/SasEmoji.test.tsx.snap b/packages/shared-components/src/crypto/SasEmoji/__snapshots__/SasEmoji.test.tsx.snap new file mode 100644 index 0000000000..27f53cb870 --- /dev/null +++ b/packages/shared-components/src/crypto/SasEmoji/__snapshots__/SasEmoji.test.tsx.snap @@ -0,0 +1,115 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > should match snapshot 1`] = ` + +
+
+ +
+ Butterfly +
+
+
+ +
+ Mushroom +
+
+
+ +
+ Ball +
+
+
+ +
+ Globe +
+
+
+ +
+ Unicorn +
+
+
+ +
+ Rocket +
+
+
+ +
+ Spanner +
+
+
+
+`; diff --git a/res/css/views/rooms/RoomListPanel/_RoomList.pcss b/packages/shared-components/src/crypto/SasEmoji/index.ts similarity index 67% rename from res/css/views/rooms/RoomListPanel/_RoomList.pcss rename to packages/shared-components/src/crypto/SasEmoji/index.ts index 54798f1ea9..5c6dc18fb7 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomList.pcss +++ b/packages/shared-components/src/crypto/SasEmoji/index.ts @@ -1,10 +1,8 @@ /* - * Copyright 2025 New Vector Ltd. + * Copyright 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -.mx_RoomList { - height: 100%; -} +export { SasEmoji } from "./SasEmoji.tsx"; diff --git a/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.module.css b/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.module.css new file mode 100644 index 0000000000..40c6156ba8 --- /dev/null +++ b/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.module.css @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.container { + background-color: var(--cpd-color-bg-subtle-secondary); + padding: var(--cpd-space-3x); + border-radius: 8px; + /* Reserve space for external timestamps, but also cap the width */ + /* Legacy variable: --MessageTimestamp-width: 46px; /* 8 + 30 (avatar) + 8 */ + /* max-width: min(calc(100% - 2 * var(--MessageTimestamp-width)), 600px); */ + max-width: min(calc(100% - 2 * 46px), 600px); + box-sizing: border-box; + display: grid; + grid-template-columns: 24px minmax(0, 1fr) min-content min-content; + + svg { + position: relative; + grid-column: 1; + grid-row: 1 / 3; + width: 16px; + height: 16px; + content: ""; + inset: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + margin-top: var(--cpd-space-1x); + } + + .title, + .subtitle { + grid-column: 2; + overflow-wrap: break-word; + min-inline-size: 50px; + } + + .title { + font-weight: var(--cpd-font-weight-semibold); + font-size: var(--cpd-font-size-body-md); + grid-row: 1; + } + + .subtitle { + font-size: var(--cpd-font-size-body-sm); + grid-row: 2; + } +} diff --git a/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.stories.tsx b/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.stories.tsx new file mode 100644 index 0000000000..9ad126da3c --- /dev/null +++ b/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.stories.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { LockSolidIcon, ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; + +import type { Meta, StoryFn } from "@storybook/react-vite"; +import { EventTileBubble } from "./EventTileBubble"; + +export default { + title: "Event/EventTileBubble", + component: EventTileBubble, + tags: ["autodocs"], + args: { + icon: , + title: "Title goes here", + subtitle: "Subtitle goes here", + className: "custom-class", + }, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); + +export const HasLockSolidIcon = Template.bind({}); +HasLockSolidIcon.args = { + className: undefined, + icon: , + children: undefined, +}; + +export const HasChildren = Template.bind({}); +HasChildren.args = { + className: undefined, + children:
children
, +}; + +export const IsCryptoEventBubble = Template.bind({}); +IsCryptoEventBubble.args = { + className: undefined, + icon: , + title: "Encryption enabled", + subtitle: "Messages here are end-to-end encrypted. Verify XYZ in their profile - tap on their profile picture.", +}; diff --git a/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.test.tsx b/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.test.tsx new file mode 100644 index 0000000000..ecac4f6615 --- /dev/null +++ b/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { render } from "@test-utils"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; +import React from "react"; + +import * as stories from "./EventTileBubble.stories.tsx"; + +const { Default, HasLockSolidIcon, HasChildren, IsCryptoEventBubble } = composeStories(stories); + +describe("EventTileBubble", () => { + it("renders the event tile bubble", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the event tile bubble with icon", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the event tile bubble with children", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the event tile bubble as crypto event bubble", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.tsx b/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.tsx new file mode 100644 index 0000000000..7890e62302 --- /dev/null +++ b/packages/shared-components/src/event-tiles/EventTileBubble/EventTileBubble.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, type ReactNode } from "react"; +import classNames from "classnames"; + +import styles from "./EventTileBubble.module.css"; + +export interface EventTileBubbleProps { + /** + * Icon rendered at the start of the bubble. + */ + icon: JSX.Element; + /** + * Main title text for the bubble. + */ + title: string; + /** + * Optional subtitle rendered beneath the title. + */ + subtitle?: ReactNode; + /** + * Optional extra class name for the container. + */ + className?: string; + /** + * Optional children rendered between subtitle and timestamp. + */ + children?: JSX.Element; + /** + * Forwarded ref for the container element. + */ + ref?: React.RefObject; +} + +/** + * EventTileBubble renders a compact event tile with an icon, title, and optional subtitle/content. + * + * @example + * ```tsx + * } title="Room created" /> + * ``` + */ +export function EventTileBubble({ + icon, + title, + subtitle, + className, + children, + ref, +}: EventTileBubbleProps): JSX.Element { + return ( +
+ {icon} +
{title}
+ {subtitle &&
{subtitle}
} + {children} +
+ ); +} diff --git a/packages/shared-components/src/event-tiles/EventTileBubble/__snapshots__/EventTileBubble.test.tsx.snap b/packages/shared-components/src/event-tiles/EventTileBubble/__snapshots__/EventTileBubble.test.tsx.snap new file mode 100644 index 0000000000..ce96c45525 --- /dev/null +++ b/packages/shared-components/src/event-tiles/EventTileBubble/__snapshots__/EventTileBubble.test.tsx.snap @@ -0,0 +1,124 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`EventTileBubble > renders the event tile bubble 1`] = ` +
+
+ + + +
+ Title goes here +
+
+ Subtitle goes here +
+
+
+`; + +exports[`EventTileBubble > renders the event tile bubble as crypto event bubble 1`] = ` +
+
+ + + +
+ Encryption enabled +
+
+ Messages here are end-to-end encrypted. Verify XYZ in their profile - tap on their profile picture. +
+
+
+`; + +exports[`EventTileBubble > renders the event tile bubble with children 1`] = ` +
+
+ + + +
+ Title goes here +
+
+ Subtitle goes here +
+
+ children +
+
+
+`; + +exports[`EventTileBubble > renders the event tile bubble with icon 1`] = ` +
+
+ + + +
+ Title goes here +
+
+ Subtitle goes here +
+
+
+`; diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss b/packages/shared-components/src/event-tiles/EventTileBubble/index.ts similarity index 57% rename from res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss rename to packages/shared-components/src/event-tiles/EventTileBubble/index.ts index cabd9b2d20..e9f58fc469 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss +++ b/packages/shared-components/src/event-tiles/EventTileBubble/index.ts @@ -1,12 +1,8 @@ /* - * Copyright 2025 New Vector Ltd. + * Copyright 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -.mx_RoomListItemMenuView { - svg { - fill: var(--cpd-color-icon-primary); - } -} +export { EventTileBubble, type EventTileBubbleProps } from "./EventTileBubble"; diff --git a/packages/shared-components/src/event-tiles/TextualEventView/TextualEventView.test.tsx b/packages/shared-components/src/event-tiles/TextualEventView/TextualEventView.test.tsx index 5d2dd912ef..3a508df56b 100644 --- a/packages/shared-components/src/event-tiles/TextualEventView/TextualEventView.test.tsx +++ b/packages/shared-components/src/event-tiles/TextualEventView/TextualEventView.test.tsx @@ -6,8 +6,9 @@ Please see LICENSE files in the repository root for full details. */ import { composeStories } from "@storybook/react-vite"; -import { render } from "jest-matrix-react"; +import { render } from "@test-utils"; import React from "react"; +import { describe, it, expect } from "vitest"; import * as stories from "./TextualEventView.stories.tsx"; diff --git a/packages/shared-components/src/event-tiles/TextualEventView/TextualEventView.tsx b/packages/shared-components/src/event-tiles/TextualEventView/TextualEventView.tsx index 3d9e4ac109..d08bfb0adb 100644 --- a/packages/shared-components/src/event-tiles/TextualEventView/TextualEventView.tsx +++ b/packages/shared-components/src/event-tiles/TextualEventView/TextualEventView.tsx @@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details. import React, { type ReactNode, type JSX } from "react"; -import { type ViewModel } from "../../viewmodel/ViewModel"; -import { useViewModel } from "../../useViewModel"; +import { type ViewModel, useViewModel } from "../../viewmodel"; export type TextualEventViewSnapshot = { content: string | ReactNode; diff --git a/packages/shared-components/src/event-tiles/TextualEventView/__snapshots__/TextualEventView.test.tsx.snap b/packages/shared-components/src/event-tiles/TextualEventView/__snapshots__/TextualEventView.test.tsx.snap index 37c53e56bd..7f0a0ff90c 100644 --- a/packages/shared-components/src/event-tiles/TextualEventView/__snapshots__/TextualEventView.test.tsx.snap +++ b/packages/shared-components/src/event-tiles/TextualEventView/__snapshots__/TextualEventView.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`TextualEventView renders a textual event 1`] = ` +exports[`TextualEventView > renders a textual event 1`] = `
{ // Mock event object mockEvent = { - preventDefault: jest.fn(), + preventDefault: vi.fn(), key: "", }; // Mock focus methods mockItems.forEach((item) => { - item.focus = jest.fn(); - item.click = jest.fn(); + item.focus = vi.fn(); + item.click = vi.fn(); }); }); afterEach(() => { document.body.removeChild(mockList); - jest.clearAllMocks(); + vi.clearAllMocks(); }); function render(): { @@ -91,7 +92,7 @@ describe("useListKeyDown", () => { ], )("should handle %s to focus the %inth element", (key, finalPosition, startPosition) => { const result = render(); - mockList.contains = jest.fn().mockReturnValue(true); + mockList.contains = vi.fn().mockReturnValue(true); Object.defineProperty(document, "activeElement", { value: mockItems[startPosition], @@ -109,7 +110,7 @@ describe("useListKeyDown", () => { it.each([["ArrowDown"], ["ArrowUp"]])("should not handle %s when active element is not in list", (key) => { const result = render(); - mockList.contains = jest.fn().mockReturnValue(false); + mockList.contains = vi.fn().mockReturnValue(false); const outsideElement = document.createElement("button"); diff --git a/packages/shared-components/src/i18n/strings/cs.json b/packages/shared-components/src/i18n/strings/cs.json new file mode 100644 index 0000000000..c16a2e29b0 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/cs.json @@ -0,0 +1,113 @@ +{ + "a11y": { + "seek_bar_label": "Panel posunu zvuku" + }, + "action": { + "delete": "Smazat", + "dismiss": "Zavřít", + "edit": "Upravit", + "explore_rooms": "Procházet místnosti", + "invite": "Pozvat", + "new_conversation": "Nová konverzace", + "new_room": "Nová místnost", + "new_video_room": "Nová video místnost", + "open_menu": "Otevřít nabídku", + "pause": "Pozastavit", + "play": "Přehrát", + "remove": "Odstranit", + "retry": "Zkusit znovu", + "search": "Hledání", + "start_chat": "Zahájit konverzaci" + }, + "common": { + "preferences": "Předvolby" + }, + "left_panel": { + "open_dial_pad": "Otevřít číselník" + }, + "room": { + "context_menu": { + "title": "Možnosti místnosti" + }, + "history_visibility_badge": { + "private": "Noví členové nevidí historii", + "shared": "Noví členové vidí historii", + "world_readable": "Každý může vidět historii" + }, + "status_bar": { + "delete_all": "Smazat vše", + "exceeded_resource_limit_description": "Chcete-li službu nadále používat, obraťte se na správce služby.", + "exceeded_resource_limit_title": "Vaše zpráva nebyla odeslána, protože tento domovský server překročil limit zdrojů.", + "failed_to_create_room_title": "S tímto uživatelem se nepodařilo zahájit chat.", + "homeserver_blocked_title": "Vaše zpráva nebyla odeslána, protože tento domovský server byl zablokován jeho správcem.", + "monthly_user_limit_reached_title": "Vaše zpráva nebyla odeslána, protože tento domovský server dosáhl svého měsíčního limitu aktivních uživatelů.", + "requires_consent_agreement_title": "Nemůžete odesílat žádné zprávy, dokud si nepřečtete a nesouhlasíte s našimi obchodními podmínkami.", + "retry_all": "Zkusit vše znovu", + "select_messages_to_retry": "Můžete vybrat všechny nebo jednotlivé zprávy, které chcete znovu odeslat nebo smazat.", + "server_connectivity_lost_description": "Odeslané zprávy zůstanou uložené, dokud se spojení znovu neobnoví.", + "server_connectivity_lost_title": "Připojení k serveru bylo ztraceno.", + "some_messages_not_sent": "Některé z vašich zpráv nebyly odeslány" + } + }, + "room_list": { + "appearance": "Vzhled", + "open_space_menu": "Otevřít nabídku prostoru", + "room_options": "Možnosti místnosti", + "show_message_previews": "Zobrazit náhledy zpráv", + "sort": "Řadit", + "sort_type": { + "activity": "Aktivita", + "atoz": "A-Z", + "unread_first": "Nepřečtené jako první" + }, + "space_menu": { + "home": "Domov prostoru", + "space_settings": "Nastavení prostoru" + } + }, + "terms": { + "tac_button": "Přečíst smluvní podmínky" + }, + "time": { + "about_day_ago": "před jedním dnem", + "about_hour_ago": "asi před hodinou", + "about_minute_ago": "před minutou", + "few_seconds_ago": "před pár vteřinami", + "in_about_day": "asi za den", + "in_about_hour": "asi za hodinu", + "in_about_minute": "asi za minutu", + "in_few_seconds": "za pár vteřin", + "in_n_days": "za %(num)s dní", + "in_n_hours": "za %(num)s hodin", + "in_n_minutes": "za %(num)s minut", + "n_days_ago": "před %(num)s dny", + "n_hours_ago": "před %(num)s hodinami", + "n_minutes_ago": "před %(num)s minutami" + }, + "timeline": { + "decryption_failure": { + "blocked": "Odesílatel vám zablokoval příjem této zprávy, protože vaše zařízení není ověřeno", + "historical_event_no_key_backup": "Historické zprávy nejsou na tomto zařízení k dispozici", + "historical_event_unverified_device": "Pro přístup k historickým zprávám musíte toto zařízení ověřit", + "historical_event_user_not_joined": "Nemáte přístup k této zprávě", + "sender_identity_previously_verified": "Ověřená identita odesílatele se změnila", + "sender_unsigned_device": "Odesláno z nezabezpečeného zařízení.", + "unable_to_decrypt": "Nepodařilo se dešifrovat zprávu" + }, + "m.audio": { + "audio_player": "Audio přehrávač", + "error_downloading_audio": "Chyba při stahování audia", + "unnamed_audio": "Nepojmenovaný audio soubor" + } + }, + "widget": { + "context_menu": { + "move_left": "Posunout doleva", + "move_right": "Posunout doprava", + "remove": "Odstranit pro všechny", + "revoke": "Odvolat oprávnění", + "screenshot": "Vyfotit", + "start_audio_stream": "Zahájit audio přenos" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/cy.json b/packages/shared-components/src/i18n/strings/cy.json new file mode 100644 index 0000000000..3bd036f441 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/cy.json @@ -0,0 +1,59 @@ +{ + "a11y": { + "seek_bar_label": "Bar chwilio sain" + }, + "action": { + "delete": "Dileu", + "dismiss": "Gwrthod", + "explore_rooms": "Archwilio Ystafelloedd", + "pause": "Oedi", + "play": "Chwarae", + "retry": "Ceisio eto", + "search": "Chwilio" + }, + "left_panel": { + "open_dial_pad": "Agor y pad deialu" + }, + "room": { + "status_bar": { + "delete_all": "Dileu'r cyfan", + "exceeded_resource_limit_description": "Cysylltwch â gweinyddwr eich gwasanaeth i barhau i ddefnyddio'r gwasanaeth.", + "exceeded_resource_limit_title": "Dyw eich neges heb ei anfon oherwydd bod y gweinydd cartref hwn wedi mynd y tu hwnt i derfyn ei adnoddau.", + "failed_to_create_room_title": "Methwyd dechrau sgwrs gyda'r defnyddiwr hwn", + "homeserver_blocked_title": "Dyw eich neges heb ei anfon oherwydd bod y gweinydd cartref hwn wedi'i rwystro gan ei weinyddwr.", + "monthly_user_limit_reached_title": "Dyw eich neges heb ei anfon oherwydd bod y gweinydd cartref hwn wedi cyrraedd ei Derfyn Defnyddiwr Gweithredol Misol.", + "requires_consent_agreement_title": "Does dim modd i chi anfon unrhyw negeseuon nes i chi adolygu a chytuno â'n telerau ac amodau.", + "retry_all": "Ail-geisio popeth", + "select_messages_to_retry": "Gallwch ddewis pob neges neu negeseuon unigol i geisio eto neu eu dileu", + "server_connectivity_lost_description": "Bydd negeseuon sy'n cael eu hanfon yn cael eu cadw nes bod eich cysylltiad wedi dychwelyd.", + "server_connectivity_lost_title": "Mae'r cysylltiad â'r gweinydd wedi'i golli.", + "some_messages_not_sent": "Dyw rhai o'ch negeseuon heb eu hanfon" + } + }, + "terms": { + "tac_button": "Adolygwch y telerau a'r amodau" + }, + "time": { + "about_day_ago": "tua diwrnod yn ôl", + "about_hour_ago": "tua awr yn ol", + "about_minute_ago": "tua munud yn ôl", + "few_seconds_ago": "ychydig eiliadau yn ôl", + "in_about_day": "tua diwrnod o nawr", + "in_about_hour": "tuag awr o hyn", + "in_about_minute": "tua munud o nawr", + "in_few_seconds": "ychydig eiliadau o nawr", + "in_n_days": "%(num)s diwrnod o nawr", + "in_n_hours": "%(num)s awr o nawr", + "in_n_minutes": "%(num)s munud o nawr", + "n_days_ago": "%(num)s diwrnod yn ôl", + "n_hours_ago": "%(num)s awr yn ôl", + "n_minutes_ago": "%(num)s munud yn ôl" + }, + "timeline": { + "m.audio": { + "audio_player": "Chwaraewr sain", + "error_downloading_audio": "Gwall wrth llwytho i lawrsain", + "unnamed_audio": "Sain dienw" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/da.json b/packages/shared-components/src/i18n/strings/da.json new file mode 100644 index 0000000000..39eb6ccfa8 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/da.json @@ -0,0 +1,35 @@ +{ + "a11y": { + "seek_bar_label": "Progressionsmarkør for lydafspiller" + }, + "action": { + "delete": "Slet", + "dismiss": "Afvis", + "explore_rooms": "Udforsk rum", + "pause": "Pausér", + "play": "Afspil", + "search": "Søg" + }, + "time": { + "about_day_ago": "omkring en dag siden", + "about_hour_ago": "for omkring en time siden", + "about_minute_ago": "for omkring et minut siden", + "few_seconds_ago": "for et par sekunder siden", + "in_about_day": "om cirka en dag fra nu", + "in_about_hour": "omkring en time fra nu", + "in_about_minute": "omkring et minut fra nu", + "in_few_seconds": "et par sekunder fra nu", + "in_n_days": "%(num)s dage fra nu", + "in_n_hours": "%(num)s timer fra nu", + "in_n_minutes": "%(num)s minutter fra nu", + "n_days_ago": "%(num)s dage siden", + "n_hours_ago": "%(num)s timer siden", + "n_minutes_ago": "%(num)s minutter siden" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Fejl ved download af lyd", + "unnamed_audio": "Unavngiven lyd" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/de_DE.json b/packages/shared-components/src/i18n/strings/de_DE.json new file mode 100644 index 0000000000..158c75e6d5 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/de_DE.json @@ -0,0 +1,110 @@ +{ + "a11y": { + "seek_bar_label": "Audio-Suchleiste" + }, + "action": { + "delete": "Löschen", + "dismiss": "Ausblenden", + "edit": "Bearbeiten", + "explore_rooms": "Chats erkunden", + "invite": "Einladen", + "new_room": "Neue Gruppe", + "new_video_room": "Neuer Videochat", + "open_menu": "Menü öffnen", + "pause": "Pausieren", + "play": "Abspielen", + "remove": "Entfernen", + "retry": "Erneut versuchen", + "search": "Suchen", + "start_chat": "Neuer Chat" + }, + "common": { + "preferences": "Einstellungen" + }, + "left_panel": { + "open_dial_pad": "Wähltastatur öffnen" + }, + "room": { + "context_menu": { + "title": "Chatoptionen" + }, + "history_visibility_badge": { + "private": "Neue Mitglieder sehen keinen historischen Nachrichtenverlauf", + "shared": "Neue Mitglieder sehen den historischen Nachrichtenverlauf", + "world_readable": "Jeder kann vergangene Nachrichten lesen" + }, + "status_bar": { + "delete_all": "Alle löschen", + "exceeded_resource_limit_title": "Deine Nachricht konnte nicht versendet werden, da dein Homeserver ein Ressourcenlimit überschritten hat.", + "failed_to_create_room_title": "Es konnte kein Chat mit diesem Nutzer gestartet werden", + "homeserver_blocked_title": "Deine Nachricht konnte nicht versendet werden, da der Admin deinen Homeserver gesperrt hat.", + "monthly_user_limit_reached_title": "Deine Nachricht konnte nicht versendet werden, da dein Homeserver das monatliche Nutzerlimit erreicht hat.", + "requires_consent_agreement_title": "Du kannst erst dann Nachrichten verschicken, wenn du unsere Geschäftsbedingungen gelesen und akzeptiert hast.", + "select_messages_to_retry": "Du kannst einzelne oder alle Nachrichten erneut senden oder löschen", + "server_connectivity_lost_description": "Nachrichten werden gespeichert und gesendet, wenn die Internetverbindung wiederhergestellt ist.", + "server_connectivity_lost_title": "Verbindung zum Server wurde unterbrochen.", + "some_messages_not_sent": "Einige Nachrichten konnten nicht gesendet werden" + } + }, + "room_list": { + "appearance": "Darstellung", + "open_space_menu": "Menü für Spaces öffnen", + "room_options": "Chatoptionen", + "show_message_previews": "Nachrichtenvorschau anzeigen", + "sort": "Sortieren", + "sort_type": { + "activity": "Aktivität", + "atoz": "A–Z", + "unread_first": "Ungelesen zuerst" + }, + "space_menu": { + "home": "Space Home", + "space_settings": "Space Einstellungen" + } + }, + "terms": { + "tac_button": "Geschäftsbedingungen anzeigen" + }, + "time": { + "about_day_ago": "vor etwa einem Tag", + "about_hour_ago": "vor etwa einer Stunde", + "about_minute_ago": "vor etwa einer Minute", + "few_seconds_ago": "vor ein paar Sekunden", + "in_about_day": "in etwa einem Tag", + "in_about_hour": "in etwa einer Stunde", + "in_about_minute": "in etwa einer Minute", + "in_few_seconds": "in ein paar Sekunden", + "in_n_days": "in %(num)s Tagen", + "in_n_hours": "in %(num)s Stunden", + "in_n_minutes": "In etwa %(num)s Minuten", + "n_days_ago": "vor %(num)s Tagen", + "n_hours_ago": "vor %(num)s Stunden", + "n_minutes_ago": "vor %(num)s Minuten" + }, + "timeline": { + "decryption_failure": { + "blocked": "Der Absender hat den Empfang dieser Nachricht blockiert, da dein Gerät nicht verifiziert ist.", + "historical_event_no_key_backup": "Der Nachrichtenverlauf ist auf diesem Gerät nicht verfügbar", + "historical_event_unverified_device": "Für den Zugriff auf den Nachrichtenverlauf musst du dieses Gerät verifizieren", + "historical_event_user_not_joined": "Du hast keinen Zugriff auf diese Nachricht", + "sender_identity_previously_verified": "Die verifizierte Identität des Absenders wurde zurückgesetzt", + "sender_unsigned_device": "Von einem unsicheren Gerät verschickt.", + "unable_to_decrypt": "Nachricht kann nicht entschlüsselt werden" + }, + "m.audio": { + "audio_player": "Audio-Player", + "error_downloading_audio": "Fehler beim Herunterladen der Audiodatei", + "unnamed_audio": "Unbenannte Audiodatei" + } + }, + "widget": { + "context_menu": { + "move_left": "Nach links schieben", + "move_right": "Nach rechts schieben", + "remove": "Für alle entfernen", + "revoke": "Berechtigungen widerrufen", + "screenshot": "Bildschirmfoto", + "start_audio_stream": "Audiostream starten" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/el.json b/packages/shared-components/src/i18n/strings/el.json new file mode 100644 index 0000000000..1a9693f88e --- /dev/null +++ b/packages/shared-components/src/i18n/strings/el.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "Διαγραφή", + "dismiss": "Απόρριψη", + "explore_rooms": "Εξερευνήστε αίθουσες", + "pause": "Παύση", + "play": "Αναπαραγωγή", + "search": "Αναζήτηση" + }, + "left_panel": { + "open_dial_pad": "Άνοιγμα πληκτρολογίου κλήσης" + }, + "time": { + "about_day_ago": "σχεδόν μία μέρα πριν", + "about_hour_ago": "σχεδόν μία ώρα πριν", + "about_minute_ago": "σχεδόν ένα λεπτό πριν", + "few_seconds_ago": "λίγα δευτερόλεπτα πριν", + "in_about_day": "περίπου μια μέρα από τώρα", + "in_about_hour": "περίπου μία ώρα από τώρα", + "in_about_minute": "περίπου ένα λεπτό από τώρα", + "in_few_seconds": "λίγα δευτερόλεπτα από τώρα", + "in_n_days": "%(num)s μέρες από τώρα", + "in_n_hours": "%(num)s ώρες από τώρα", + "in_n_minutes": "%(num)s λεπτά από τώρα", + "n_days_ago": "%(num)s μέρες πριν", + "n_hours_ago": "%(num)s ώρες πριν", + "n_minutes_ago": "%(num)s λεπτά πριν" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Σφάλμα λήψης ήχου", + "unnamed_audio": "Ήχος χωρίς όνομα" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json new file mode 100644 index 0000000000..2ba72dd1c5 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -0,0 +1,174 @@ +{ + "a11y": { + "seek_bar_label": "Audio seek bar" + }, + "action": { + "delete": "Delete", + "dismiss": "Dismiss", + "edit": "Edit", + "explore_rooms": "Explore rooms", + "invite": "Invite", + "new_conversation": "New conversation", + "new_room": "New room", + "new_video_room": "New video room", + "open_menu": "Open menu", + "pause": "Pause", + "play": "Play", + "remove": "Remove", + "retry": "Retry", + "search": "Search", + "start_chat": "Start chat" + }, + "common": { + "preferences": "Preferences" + }, + "left_panel": { + "open_dial_pad": "Open dial pad" + }, + "notifications": { + "all_messages": "All messages", + "default_settings": "Match default settings", + "mentions_keywords": "Mentions and keywords", + "mute_room": "Mute room" + }, + "room": { + "context_menu": { + "title": "Room options" + }, + "history_visibility_badge": { + "private": "New members don't see history", + "shared": "New members see history", + "world_readable": "Anyone can see history" + }, + "status_bar": { + "delete_all": "Delete all", + "exceeded_resource_limit_description": "Please contact your service administrator to continue using the service.", + "exceeded_resource_limit_title": "Your message wasn't sent because this homeserver has exceeded a resource limit.", + "failed_to_create_room_title": "Could not start a chat with this user", + "homeserver_blocked_title": "Your message wasn't sent because this homeserver has been blocked by its administrator.", + "monthly_user_limit_reached_title": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit.", + "requires_consent_agreement_title": "You can't send any messages until you review and agree to our terms and conditions.", + "retry_all": "Retry all", + "select_messages_to_retry": "You can select all or individual messages to retry or delete", + "server_connectivity_lost_description": "Sent messages will be stored until your connection has returned.", + "server_connectivity_lost_title": "Connectivity to the server has been lost.", + "some_messages_not_sent": "Some of your messages have not been sent" + } + }, + "room_list": { + "a11y": { + "default": "Open room %(roomName)s", + "invitation": "Open room %(roomName)s invitation.", + "mention": { + "one": "Open room %(roomName)s with 1 unread mention.", + "other": "Open room %(roomName)s with %(count)s unread mentions." + }, + "unread": { + "one": "Open room %(roomName)s with 1 unread message.", + "other": "Open room %(roomName)s with %(count)s unread messages." + }, + "unsent_message": "Open room %(roomName)s with an unsent message." + }, + "appearance": "Appearance", + "collapse_filters": "Collapse filter list", + "empty": { + "no_chats": "No chats yet", + "no_chats_description": "Get started by messaging someone or by creating a room", + "no_chats_description_no_room_rights": "Get started by messaging someone", + "no_favourites": "You don't have favourite chats yet", + "no_favourites_description": "You can add a chat to your favourites in the chat settings", + "no_invites": "You don't have any unread invites", + "no_lowpriority": "You don't have any low priority rooms", + "no_mentions": "You don't have any unread mentions", + "no_people": "You don’t have direct chats with anyone yet", + "no_people_description": "You can deselect filters in order to see your other chats", + "no_rooms": "You’re not in any room yet", + "no_rooms_description": "You can deselect filters in order to see your other chats", + "no_unread": "Congrats! You don’t have any unread messages", + "show_activity": "See all activity", + "show_chats": "Show all chats" + }, + "expand_filters": "Expand filter list", + "filters": { + "favourite": "Favourites", + "invites": "Invites", + "low_priority": "Low priority", + "mentions": "Mentions", + "people": "People", + "rooms": "Rooms", + "unread": "Unreads" + }, + "list_title": "Room list", + "more_options": { + "copy_link": "Copy room link", + "favourited": "Favourited", + "leave_room": "Leave room", + "low_priority": "Low priority", + "mark_read": "Mark as read", + "mark_unread": "Mark as unread" + }, + "notification_options": "Notification options", + "open_space_menu": "Open space menu", + "primary_filters": "Room list filters", + "room": { + "more_options": "More Options" + }, + "room_options": "Room Options", + "show_message_previews": "Show message previews", + "sort": "Sort", + "sort_type": { + "activity": "Activity", + "atoz": "A-Z", + "unread_first": "Unread first" + }, + "space_menu": { + "home": "Space home", + "space_settings": "Space settings" + } + }, + "terms": { + "tac_button": "Review terms and conditions" + }, + "time": { + "about_day_ago": "about a day ago", + "about_hour_ago": "about an hour ago", + "about_minute_ago": "about a minute ago", + "few_seconds_ago": "a few seconds ago", + "in_about_day": "about a day from now", + "in_about_hour": "about an hour from now", + "in_about_minute": "about a minute from now", + "in_few_seconds": "a few seconds from now", + "in_n_days": "%(num)s days from now", + "in_n_hours": "%(num)s hours from now", + "in_n_minutes": "%(num)s minutes from now", + "n_days_ago": "%(num)s days ago", + "n_hours_ago": "%(num)s hours ago", + "n_minutes_ago": "%(num)s minutes ago" + }, + "timeline": { + "decryption_failure": { + "blocked": "The sender has blocked you from receiving this message because your device is unverified", + "historical_event_no_key_backup": "Historical messages are not available on this device", + "historical_event_unverified_device": "You need to verify this device for access to historical messages", + "historical_event_user_not_joined": "You don't have access to this message", + "sender_identity_previously_verified": "Sender's verified identity was reset", + "sender_unsigned_device": "Sent from an insecure device.", + "unable_to_decrypt": "Unable to decrypt message" + }, + "m.audio": { + "audio_player": "Audio player", + "error_downloading_audio": "Error downloading audio", + "unnamed_audio": "Unnamed audio" + } + }, + "widget": { + "context_menu": { + "move_left": "Move left", + "move_right": "Move right", + "remove": "Remove for everyone", + "revoke": "Revoke permissions", + "screenshot": "Take a picture", + "start_audio_stream": "Start audio stream" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/eo.json b/packages/shared-components/src/i18n/strings/eo.json new file mode 100644 index 0000000000..83c7485327 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/eo.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "Forigi", + "dismiss": "Rezigni", + "explore_rooms": "Esplori ĉambrojn", + "pause": "Paŭzigi", + "play": "Ludi", + "search": "Serĉi" + }, + "left_panel": { + "open_dial_pad": "Malfermi ciferplaton" + }, + "time": { + "about_day_ago": "antaŭ ĉirkaŭ tago", + "about_hour_ago": "antaŭ ĉirkaŭ horo", + "about_minute_ago": "antaŭ ĉirkaŭ minuto", + "few_seconds_ago": "antaŭ kelkaj sekundoj", + "in_about_day": "ĉirkaŭ tagon de nun", + "in_about_hour": "ĉirkaŭ horon de nun", + "in_about_minute": "ĉirkaŭ minuton de nun", + "in_few_seconds": "kelkajn sekundojn de nun", + "in_n_days": "%(num)s tagojn de nun", + "in_n_hours": "%(num)s horojn de nun", + "in_n_minutes": "%(num)s minutojn de nun", + "n_days_ago": "antaŭ %(num)s tagoj", + "n_hours_ago": "antaŭ %(num)s horoj", + "n_minutes_ago": "antaŭ %(num)s minutoj" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Eraris elŝuto de sondosiero", + "unnamed_audio": "Sennoma sondosiero" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/es.json b/packages/shared-components/src/i18n/strings/es.json new file mode 100644 index 0000000000..3efb5cdeea --- /dev/null +++ b/packages/shared-components/src/i18n/strings/es.json @@ -0,0 +1,39 @@ +{ + "a11y": { + "seek_bar_label": "Barra de búsqueda de audio" + }, + "action": { + "delete": "Borrar", + "dismiss": "Omitir", + "explore_rooms": "Explorar salas", + "pause": "Pausar", + "play": "Reproducir", + "search": "Buscar" + }, + "left_panel": { + "open_dial_pad": "Abrir teclado numérico" + }, + "time": { + "about_day_ago": "hace aprox. un día", + "about_hour_ago": "hace aprox. una hora", + "about_minute_ago": "hace aproximadamente un minuto", + "few_seconds_ago": "hace unos segundos", + "in_about_day": "dentro de un día", + "in_about_hour": "dentro de una hora", + "in_about_minute": "dentro de un minuto", + "in_few_seconds": "dentro de unos segundos", + "in_n_days": "dentro de %(num)s días", + "in_n_hours": "dentro de %(num)s horas", + "in_n_minutes": "dentro de %(num)s minutos", + "n_days_ago": "hace %(num)s días", + "n_hours_ago": "hace %(num)s horas", + "n_minutes_ago": "hace %(num)s minutos" + }, + "timeline": { + "m.audio": { + "audio_player": "Reproductor de audio", + "error_downloading_audio": "Error al descargar el audio", + "unnamed_audio": "Audio sin título" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/et.json b/packages/shared-components/src/i18n/strings/et.json new file mode 100644 index 0000000000..6310387318 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/et.json @@ -0,0 +1,113 @@ +{ + "a11y": { + "seek_bar_label": "Heli kerimisriba" + }, + "action": { + "delete": "Kustuta", + "dismiss": "Loobu", + "edit": "Muuda", + "explore_rooms": "Tutvu jututubadega", + "invite": "Kutsu", + "new_conversation": "Uus vestlus", + "new_room": "Uus jututuba", + "new_video_room": "Uus videotuba", + "open_menu": "Ava menüü", + "pause": "Peata", + "play": "Esita", + "remove": "Eemalda", + "retry": "Proovi uuesti", + "search": "Otsing", + "start_chat": "Alusta vestlust" + }, + "common": { + "preferences": "Eelistused" + }, + "left_panel": { + "open_dial_pad": "Ava numbriklahvistik" + }, + "room": { + "context_menu": { + "title": "Jututoa eelistused" + }, + "history_visibility_badge": { + "private": "Uued liikmed ei näe ajalugu", + "shared": "Uued liikmed näevad ajalugu", + "world_readable": "Kõik võivad ajalugu näha" + }, + "status_bar": { + "delete_all": "Kustuta kõik", + "exceeded_resource_limit_description": "Teenuse kasutamise jätkamiseks võta ühendust oma teenuse haldajaga või peakasutajaga.", + "exceeded_resource_limit_title": "Kuna see koduserver on ületanud etteantud ressursside ülempiiri, siis sinu sõnum on saatmata.", + "failed_to_create_room_title": "Selle kasutajaga ei õnnestunud alustada vestlust", + "homeserver_blocked_title": "Kuna see koduserver on peakasutaja või haldaaja poolt blokeerinud, siis sinu sõnum on saatmata.", + "monthly_user_limit_reached_title": "Sinu sõnum on saatmata, kuna see koduserver on saavutanud oma igakuise aktiivsete kasutajate ülempiiri.", + "requires_consent_agreement_title": "Sa ei saa sõnumeid saata enne, kui oled meie kasutustingimustega tutvunud ja nendega nõustunud.", + "retry_all": "Proovi kõikidega uuesti", + "select_messages_to_retry": "Sa võid valida kas kõik või mõned sõnumid kas kustutamiseks või uuesti saatmiseks", + "server_connectivity_lost_description": "Saadetud sõnumid salvestatakse seniks, kuni võrguühendus on taastunud.", + "server_connectivity_lost_title": "Võrguühendus serveriga on katkenud.", + "some_messages_not_sent": "Mõned sinu sõnumid on saatmata" + } + }, + "room_list": { + "appearance": "Välimus", + "open_space_menu": "Ava kogukonna menüü", + "room_options": "Jututoa valikud", + "show_message_previews": "Näita sõnumite eelvaateid", + "sort": "Järjesta", + "sort_type": { + "activity": "Aktiivsuse alusel", + "atoz": "Tähestiku järjekorras", + "unread_first": "Esmalt lugemata" + }, + "space_menu": { + "home": "Kogukonna avaleht", + "space_settings": "Kogukonna seadistused" + } + }, + "terms": { + "tac_button": "Vaata üle kasutustingimused" + }, + "time": { + "about_day_ago": "umbes päev tagasi", + "about_hour_ago": "umbes tund aega tagasi", + "about_minute_ago": "umbes minut tagasi", + "few_seconds_ago": "mõni sekund tagasi", + "in_about_day": "umbes päeva pärast", + "in_about_hour": "umbes tunni pärast", + "in_about_minute": "umbes minuti pärast", + "in_few_seconds": "mõne sekundi pärast", + "in_n_days": "%(num)s päeva pärast", + "in_n_hours": "%(num)s tunni pärast", + "in_n_minutes": "%(num)s minuti pärast", + "n_days_ago": "%(num)s päeva tagasi", + "n_hours_ago": "%(num)s tundi tagasi", + "n_minutes_ago": "%(num)s minutit tagasi" + }, + "timeline": { + "decryption_failure": { + "blocked": "Kuna sinu seade on verifitseerimata, siis saatja on blokeerinud võimaluse, et sa saaksid selle sõnumi", + "historical_event_no_key_backup": "Varasemad sõnumid pole selles seadmes loetavad", + "historical_event_unverified_device": "Ligipääsuks vanadele sõnumitele pead selle seadme verifitseerima", + "historical_event_user_not_joined": "Sul pole ligipääsu antud sõnumile", + "sender_identity_previously_verified": "Saatja verifitseeritud võrguidentiteet on lähtestatud", + "sender_unsigned_device": "Saadetud ebaturvalisest seadmest.", + "unable_to_decrypt": "Sõnumi dekrüptimine ei õnnestunud" + }, + "m.audio": { + "audio_player": "Meediaesitaja", + "error_downloading_audio": "Helifaili allalaadimine ei õnnestunud", + "unnamed_audio": "Nimetu helifail" + } + }, + "widget": { + "context_menu": { + "move_left": "Liigu vasakule", + "move_right": "Liigu paremale", + "remove": "Eemalda kõigilt", + "revoke": "Tühista õigused", + "screenshot": "Pildista", + "start_audio_stream": "Käivita audiovoog" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/fa.json b/packages/shared-components/src/i18n/strings/fa.json new file mode 100644 index 0000000000..0871bcdc0b --- /dev/null +++ b/packages/shared-components/src/i18n/strings/fa.json @@ -0,0 +1,29 @@ +{ + "action": { + "delete": "پاک‌کردن", + "dismiss": "نادیده بگیر", + "explore_rooms": "جستجو در اتاق ها", + "pause": "متوقف‌کردن", + "play": "اجرا کردن", + "search": "جستجو" + }, + "left_panel": { + "open_dial_pad": "باز کردن صفحه شماره‌گیری" + }, + "time": { + "about_day_ago": "حدود یک روز قبل", + "about_hour_ago": "حدود یک ساعت قبل", + "about_minute_ago": "حدود یک دقیقه قبل", + "few_seconds_ago": "چند ثانیه قبل", + "in_about_day": "حدود یک روز دیگر", + "in_about_hour": "حدود یک ساعت دیگر", + "in_about_minute": "حدود یک دقیقه دیگر", + "in_few_seconds": "چند ثانیه دیگر", + "in_n_days": "%(num)s روز دیگر", + "in_n_hours": "%(num)s ساعت دیگر", + "in_n_minutes": "%(num)s دقیقه دیگر", + "n_days_ago": "%(num)s روز قبل", + "n_hours_ago": "%(num)s ساعت قبل", + "n_minutes_ago": "%(num)s دقیقه قبل" + } +} diff --git a/packages/shared-components/src/i18n/strings/fi.json b/packages/shared-components/src/i18n/strings/fi.json new file mode 100644 index 0000000000..d178b17307 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/fi.json @@ -0,0 +1,38 @@ +{ + "a11y": { + "seek_bar_label": "Äänen siirtymispalkki" + }, + "action": { + "delete": "Poista", + "dismiss": "Hylkää", + "explore_rooms": "Selaa huoneita", + "pause": "Keskeytä", + "play": "Toista", + "search": "Haku" + }, + "left_panel": { + "open_dial_pad": "Avaa näppäimistö" + }, + "time": { + "about_day_ago": "noin päivä sitten", + "about_hour_ago": "noin tunti sitten", + "about_minute_ago": "noin minuutti sitten", + "few_seconds_ago": "muutama sekunti sitten", + "in_about_day": "noin päivä sitten", + "in_about_hour": "noin tunti sitten", + "in_about_minute": "noin minuutti sitten", + "in_few_seconds": "muutama sekunti sitten", + "in_n_days": "%(num)s päivää sitten", + "in_n_hours": "%(num)s tuntia sitten", + "in_n_minutes": "%(num)s minuuttia sitten", + "n_days_ago": "%(num)s päivää sitten", + "n_hours_ago": "%(num)s tuntia sitten", + "n_minutes_ago": "%(num)s minuuttia sitten" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Virhe ääntä ladattaessa", + "unnamed_audio": "Nimetön ääni" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/fr.json b/packages/shared-components/src/i18n/strings/fr.json new file mode 100644 index 0000000000..674f99cac8 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/fr.json @@ -0,0 +1,84 @@ +{ + "a11y": { + "seek_bar_label": "Barre de recherche audio" + }, + "action": { + "delete": "Supprimer", + "dismiss": "Ignorer", + "explore_rooms": "Parcourir les salons", + "invite": "Inviter", + "new_conversation": "Nouvelle conversation", + "new_room": "Nouveau salon", + "new_video_room": "Nouveau salon visio", + "open_menu": "Ouvrir le menu", + "pause": "Pause", + "play": "Lecture", + "retry": "Réessayer", + "search": "Rechercher", + "start_chat": "Démarrer une discussion" + }, + "common": { + "preferences": "Préférences" + }, + "left_panel": { + "open_dial_pad": "Ouvrir le pavé de numérotation" + }, + "room": { + "context_menu": { + "title": "Options du salon" + }, + "status_bar": { + "delete_all": "Tout supprimer", + "exceeded_resource_limit_description": "Veuillez contacter votre administrateur pour continuer à utiliser le service.", + "exceeded_resource_limit_title": "Votre message n'a pas pu être envoyé car ce serveur d'accueil a dépassé sa limite de ressources.", + "failed_to_create_room_title": "Impossible de démarrer une discussion avec cet utilisateur", + "homeserver_blocked_title": "Votre message n'a pas pu être envoyé car ce serveur d'accueil a été bloqué par son administrateur.", + "monthly_user_limit_reached_title": "Votre message n'a pas pu être envoyé car ce serveur d'accueil a atteint sa limite mensuelle d'utilisateurs actifs.", + "requires_consent_agreement_title": "Vous ne pouvez envoyer aucun message tant que vous n'aurez pas consulté et accepté nos conditions générales.", + "retry_all": "Tout réessayer", + "select_messages_to_retry": "Vous pouvez choisir de renvoyer ou supprimer tous les messages ou seulement certains", + "server_connectivity_lost_description": "Les messages envoyés seront stockés jusqu’à ce que votre connexion revienne.", + "server_connectivity_lost_title": "La connexion avec le serveur a été perdue.", + "some_messages_not_sent": "Certains de vos messages n’ont pas été envoyés" + } + }, + "room_list": { + "open_space_menu": "Ouvrir le menu de l’espace", + "room_options": "Options du salon", + "sort": "Trier", + "sort_type": { + "activity": "Activité", + "atoz": "A-Z" + }, + "space_menu": { + "home": "Accueil de l’espace", + "space_settings": "Paramètres de l'espace" + } + }, + "terms": { + "tac_button": "Voir les conditions générales" + }, + "time": { + "about_day_ago": "il y a environ un jour", + "about_hour_ago": "il y a environ une heure", + "about_minute_ago": "il y a environ une minute", + "few_seconds_ago": "il y a quelques secondes", + "in_about_day": "dans un jour environ", + "in_about_hour": "dans une heure environ", + "in_about_minute": "dans une minute environ", + "in_few_seconds": "dans quelques secondes", + "in_n_days": "dans %(num)s jours", + "in_n_hours": "dans %(num)s heures", + "in_n_minutes": "dans %(num)s minutes", + "n_days_ago": "il y a %(num)s jours", + "n_hours_ago": "il y a %(num)s heures", + "n_minutes_ago": "il y a %(num)s minutes" + }, + "timeline": { + "m.audio": { + "audio_player": "Lecteur audio", + "error_downloading_audio": "Erreur lors du téléchargement de l’audio", + "unnamed_audio": "Audio sans nom" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/gl.json b/packages/shared-components/src/i18n/strings/gl.json new file mode 100644 index 0000000000..2acc9ba7ca --- /dev/null +++ b/packages/shared-components/src/i18n/strings/gl.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "Eliminar", + "dismiss": "Rexeitar", + "explore_rooms": "Explorar salas", + "pause": "Deter", + "play": "Reproducir", + "search": "Busca" + }, + "left_panel": { + "open_dial_pad": "Abrir marcador" + }, + "time": { + "about_day_ago": "onte", + "about_hour_ago": "fai unha hora", + "about_minute_ago": "fai un minuto", + "few_seconds_ago": "fai uns segundos", + "in_about_day": "foi onte", + "in_about_hour": "fará unha hora", + "in_about_minute": "haberá un minuto", + "in_few_seconds": "hai só uns segundos", + "in_n_days": "fará %(num)s días", + "in_n_hours": "fará %(num)s horas", + "in_n_minutes": "fará %(num)s minutos", + "n_days_ago": "fai %(num)s días", + "n_hours_ago": "fai %(num)s horas", + "n_minutes_ago": "fai %(num)s minutos" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Erro ao descargar o audio", + "unnamed_audio": "Audio sen nome" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/he.json b/packages/shared-components/src/i18n/strings/he.json new file mode 100644 index 0000000000..8d19269362 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/he.json @@ -0,0 +1,27 @@ +{ + "action": { + "delete": "מחק", + "dismiss": "התעלם", + "explore_rooms": "גלה חדרים", + "search": "חפש" + }, + "left_panel": { + "open_dial_pad": "פתח לוח חיוג" + }, + "time": { + "about_day_ago": "בערך לפני יום", + "about_hour_ago": "בערך לפני כשעה", + "about_minute_ago": "לפני בערך דקה", + "few_seconds_ago": "לפני מספר שניות", + "in_about_day": "בערך בעוד יום מעכשיו", + "in_about_hour": "בערך בעוד כשעה", + "in_about_minute": "בערך עוד דקה אחת", + "in_few_seconds": "בעוד מספר שניות מעכשיו", + "in_n_days": "בעוד %(num)s ימים מעכשיו", + "in_n_hours": "בעוד %(num)s שעות", + "in_n_minutes": "בעוד %(num)s דקות", + "n_days_ago": "לפני %(num)s ימים", + "n_hours_ago": "לפני %(num)s שעות", + "n_minutes_ago": "לפני %(num)s דקות" + } +} diff --git a/packages/shared-components/src/i18n/strings/hu.json b/packages/shared-components/src/i18n/strings/hu.json new file mode 100644 index 0000000000..3ea03a5aef --- /dev/null +++ b/packages/shared-components/src/i18n/strings/hu.json @@ -0,0 +1,87 @@ +{ + "a11y": { + "seek_bar_label": "Hang keresősávja" + }, + "action": { + "delete": "Törlés", + "dismiss": "Eltüntetés", + "explore_rooms": "Szobák felderítése", + "invite": "Meghívás", + "new_conversation": "Új beszélgetés", + "new_room": "Új szoba", + "new_video_room": "Új videószoba", + "open_menu": "Menü megnyitása", + "pause": "Szünet", + "play": "Lejátszás", + "retry": "Újra", + "search": "Keresés", + "start_chat": "Csevegés indítása" + }, + "common": { + "preferences": "Beállítások" + }, + "left_panel": { + "open_dial_pad": "Számlap megnyitása" + }, + "room": { + "context_menu": { + "title": "Szobabeállítások" + }, + "status_bar": { + "delete_all": "Összes törlése", + "exceeded_resource_limit_description": "A szolgáltatás további használatához vegye fel a kapcsolatot a szolgáltatóval.", + "exceeded_resource_limit_title": "Az üzenet nem került elküldésre, mert ez a Matrix-kiszolgáló túllépte az erőforráskorlátot.", + "failed_to_create_room_title": "Nem sikerült csevegést indítani ezzel a felhasználóval", + "homeserver_blocked_title": "Az üzenet nem került elküldésre, mert ezt a Matrix-kiszolgálót az adminisztrátora blokkolta.", + "monthly_user_limit_reached_title": "Az üzenet nem került elküldésre, mert ez a Matrix-kiszolgáló elérte a havi aktív felhasználói korlátját.", + "requires_consent_agreement_title": "Addig nem küldhet üzeneteket, amíg nem olvasta el és nem fogadta el a felhasználási feltételeinket.", + "retry_all": "Összes újraküldése", + "select_messages_to_retry": "Újraküldéshez vagy törléshez kiválaszthatja az üzeneteket egyenként vagy az összeset együtt", + "server_connectivity_lost_description": "Az elküldött üzenetek tárolva lesznek, amíg a kapcsolata újra elérhető nem lesz.", + "server_connectivity_lost_title": "Megszakadt a kapcsolat a kiszolgálóval.", + "some_messages_not_sent": "Néhány üzenete nem lett elküldve" + } + }, + "room_list": { + "appearance": "Megjelenítés", + "open_space_menu": "Tér menü megnyitása", + "room_options": "Szobabeállítások", + "show_message_previews": "Üzenetelőnézetek megjelenítése", + "sort": "Rendezés", + "sort_type": { + "activity": "Tevékenység", + "atoz": "A–Z", + "unread_first": "Olvasatlan elöl" + }, + "space_menu": { + "home": "Kezdő tér", + "space_settings": "Térbeállítások" + } + }, + "terms": { + "tac_button": "Általános Szerződési Feltételek elolvasása" + }, + "time": { + "about_day_ago": "egy napja", + "about_hour_ago": "egy órája", + "about_minute_ago": "egy perce", + "few_seconds_ago": "néhány másodperce", + "in_about_day": "egy nap múlva", + "in_about_hour": "egy óra múlva", + "in_about_minute": "egy perc múlva", + "in_few_seconds": "másodpercek múlva", + "in_n_days": "%(num)s nap múlva", + "in_n_hours": "%(num)s óra múlva", + "in_n_minutes": "%(num)s perc múlva", + "n_days_ago": "%(num)s nappal ezelőtt", + "n_hours_ago": "%(num)s órával ezelőtt", + "n_minutes_ago": "%(num)s perccel ezelőtt" + }, + "timeline": { + "m.audio": { + "audio_player": "Hanglejátszó", + "error_downloading_audio": "Hiba a hang letöltésekor", + "unnamed_audio": "Névtelen hang" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/hy.json b/packages/shared-components/src/i18n/strings/hy.json new file mode 100644 index 0000000000..cb1a1432bc --- /dev/null +++ b/packages/shared-components/src/i18n/strings/hy.json @@ -0,0 +1,39 @@ +{ + "a11y": { + "seek_bar_label": "Աուդիո որոնման գոտի" + }, + "action": { + "delete": "Ջնջել", + "dismiss": "Հեռացնել", + "explore_rooms": "Փնտրել սենյակներ", + "pause": "Դադար", + "play": "Միացնել", + "search": "Որոնել" + }, + "left_panel": { + "open_dial_pad": "Բացեք թվերի հավաքման վահանակը" + }, + "time": { + "about_day_ago": "մոտ մեկ օր առաջ", + "about_hour_ago": "մոտ մեկ ժամ առաջ", + "about_minute_ago": "մոտ մեկ րոպե առաջ", + "few_seconds_ago": "մի քանի վայրկյան առաջ", + "in_about_day": "մոտ մեկ օր անց", + "in_about_hour": "մոտ մեկ ժամ անց", + "in_about_minute": "մոտ մեկ րոպե անց", + "in_few_seconds": "մի քանի վայրկյան անց", + "in_n_days": "%(num)s օր անց", + "in_n_hours": "%(num)s ժամ անց", + "in_n_minutes": "%(num)s րոպեներ անց", + "n_days_ago": "%(num)s օր առաջ", + "n_hours_ago": "%(num)s ժամ առաջ", + "n_minutes_ago": "%(num)s րոպե առաջ" + }, + "timeline": { + "m.audio": { + "audio_player": "Աուդիո նվագարկիչ", + "error_downloading_audio": "Աուդիո ներբեռնման սխալ", + "unnamed_audio": "Անանուն աուդիո" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/id.json b/packages/shared-components/src/i18n/strings/id.json new file mode 100644 index 0000000000..8db15ffcac --- /dev/null +++ b/packages/shared-components/src/i18n/strings/id.json @@ -0,0 +1,84 @@ +{ + "a11y": { + "seek_bar_label": "Bilah pencarian audio" + }, + "action": { + "delete": "Hapus", + "dismiss": "Abaikan", + "explore_rooms": "Jelajahi ruangan", + "invite": "Undang", + "new_conversation": "Percakapan baru", + "new_room": "Ruangan baru", + "new_video_room": "Ruangan video baru", + "open_menu": "Buka menu", + "pause": "Jeda", + "play": "Mainkan", + "retry": "Coba lagi", + "search": "Cari", + "start_chat": "Mulai obrolan" + }, + "common": { + "preferences": "Preferensi" + }, + "left_panel": { + "open_dial_pad": "Buka tombol penyetel" + }, + "room": { + "context_menu": { + "title": "Opsi ruangan" + }, + "status_bar": { + "delete_all": "Hapus semua", + "exceeded_resource_limit_description": "Silakan hubungi administrator layanan Anda untuk melanjutkan penggunaan layanan.", + "exceeded_resource_limit_title": "Pesan Anda tidak terkirim karena homeserver ini telah melampaui batas sumber daya.", + "failed_to_create_room_title": "Tidak dapat memulai obrolan dengan pengguna ini", + "homeserver_blocked_title": "Pesan Anda tidak terkirim karena homeserver ini telah diblokir oleh administratornya.", + "monthly_user_limit_reached_title": "Pesan Anda tidak terkirim karena homeserver ini telah mencapai Batas Pengguna Aktif Bulanan.", + "requires_consent_agreement_title": "Anda tidak dapat mengirim pesan apa pun sampai Anda meninjau dan menyetujui syarat dan ketentuan kami.", + "retry_all": "Coba ulang semua", + "select_messages_to_retry": "Anda dapat memilih semua atau beberapa pesan untuk dicoba lagi atau dihapus", + "server_connectivity_lost_description": "Pesan yang akan dikirim akan disimpan sampai koneksi Anda telah kembali.", + "server_connectivity_lost_title": "Koneksi ke server telah hilang.", + "some_messages_not_sent": "Beberapa pesan Anda tidak terkirim" + } + }, + "room_list": { + "open_space_menu": "Buka menu space", + "room_options": "Opsi Ruangan", + "sort": "Urutan", + "sort_type": { + "activity": "Aktivitas", + "atoz": "A-Z" + }, + "space_menu": { + "home": "Beranda space", + "space_settings": "Pengaturan space" + } + }, + "terms": { + "tac_button": "Lihat syarat dan ketentuan" + }, + "time": { + "about_day_ago": "1 hari yang lalu", + "about_hour_ago": "1 jam yang lalu", + "about_minute_ago": "1 menit yang lalu", + "few_seconds_ago": "beberapa detik yang lalu", + "in_about_day": "1 hari dari sekarang", + "in_about_hour": "1 jam dari sekarang", + "in_about_minute": "1 menit dari sekarang", + "in_few_seconds": "beberapa detik dari sekarang", + "in_n_days": "%(num)s hari dari sekarang", + "in_n_hours": "%(num)s jam dari sekarang", + "in_n_minutes": "%(num)s dari sekarang", + "n_days_ago": "%(num)s hari yang lalu", + "n_hours_ago": "%(num)s jam yang lalu", + "n_minutes_ago": "%(num)s menit yang lalu" + }, + "timeline": { + "m.audio": { + "audio_player": "Pemutar audio", + "error_downloading_audio": "Terjadi kesalahan mengunduh audio", + "unnamed_audio": "Audio tidak dinamai" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/is.json b/packages/shared-components/src/i18n/strings/is.json new file mode 100644 index 0000000000..284bb0df06 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/is.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "Eyða", + "dismiss": "Hunsa", + "explore_rooms": "Kanna spjallrásir", + "pause": "Bið", + "play": "Spila", + "search": "Leita" + }, + "left_panel": { + "open_dial_pad": "Opna talnaborð" + }, + "time": { + "about_day_ago": "fyrir um degi síðan", + "about_hour_ago": "fyrir um klukkustund síðan", + "about_minute_ago": "fyrir um það bil mínútu síðan", + "few_seconds_ago": "fyrir örfáum sekúndum síðan", + "in_about_day": "eftir um það bil einn dag", + "in_about_hour": "eftir um það bil klukkustund", + "in_about_minute": "eftir um það bil mínútu", + "in_few_seconds": "eftir nokkrar sekúndur", + "in_n_days": "eftir %(num)s daga", + "in_n_hours": "eftir %(num)s klukkustundir", + "in_n_minutes": "eftir %(num)s mínútur", + "n_days_ago": "fyrir %(num)s dögum síðan", + "n_hours_ago": "fyrir %(num)s klukkustundum síðan", + "n_minutes_ago": "fyrir %(num)s mínútum síðan" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Villa við að sækja hljóð", + "unnamed_audio": "Nafnlaust hljóð" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/it.json b/packages/shared-components/src/i18n/strings/it.json new file mode 100644 index 0000000000..523d5798c1 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/it.json @@ -0,0 +1,38 @@ +{ + "a11y": { + "seek_bar_label": "Barra di ricerca audio" + }, + "action": { + "delete": "Elimina", + "dismiss": "Chiudi", + "explore_rooms": "Esplora stanze", + "pause": "Pausa", + "play": "Riproduci", + "search": "Cerca" + }, + "left_panel": { + "open_dial_pad": "Apri tastierino" + }, + "time": { + "about_day_ago": "circa un giorno fa", + "about_hour_ago": "circa un'ora fa", + "about_minute_ago": "circa un minuto fa", + "few_seconds_ago": "pochi secondi fa", + "in_about_day": "circa un giorno da adesso", + "in_about_hour": "circa un'ora da adesso", + "in_about_minute": "circa un minuto da adesso", + "in_few_seconds": "pochi secondi da adesso", + "in_n_days": "%(num)s giorni da adesso", + "in_n_hours": "%(num)s ore da adesso", + "in_n_minutes": "%(num)s minuti da adesso", + "n_days_ago": "%(num)s giorni fa", + "n_hours_ago": "%(num)s ore fa", + "n_minutes_ago": "%(num)s minuti fa" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Errore di scaricamento dell'audio", + "unnamed_audio": "Audio senza nome" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/ja.json b/packages/shared-components/src/i18n/strings/ja.json new file mode 100644 index 0000000000..e803863efe --- /dev/null +++ b/packages/shared-components/src/i18n/strings/ja.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "削除", + "dismiss": "閉じる", + "explore_rooms": "ルームを探す", + "pause": "一時停止", + "play": "再生", + "search": "検索" + }, + "left_panel": { + "open_dial_pad": "ダイヤルパッドを開く" + }, + "time": { + "about_day_ago": "約1日前", + "about_hour_ago": "約1時間前", + "about_minute_ago": "約1分前", + "few_seconds_ago": "数秒前", + "in_about_day": "今から約1日前", + "in_about_hour": "今から約1時間前", + "in_about_minute": "今から約1分前", + "in_few_seconds": "今から数秒前", + "in_n_days": "今から%(num)s日前", + "in_n_hours": "今から%(num)s時間前", + "in_n_minutes": "今から%(num)s分前", + "n_days_ago": "%(num)s日前", + "n_hours_ago": "%(num)s時間前", + "n_minutes_ago": "%(num)s分前" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "音声をダウンロードする際にエラーが発生しました", + "unnamed_audio": "名前のない音声" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/ka.json b/packages/shared-components/src/i18n/strings/ka.json new file mode 100644 index 0000000000..5e7482c72a --- /dev/null +++ b/packages/shared-components/src/i18n/strings/ka.json @@ -0,0 +1,32 @@ +{ + "action": { + "delete": "წაშლა", + "dismiss": "დახურვა", + "explore_rooms": "ოთახების დათავლიერება", + "pause": "პაუზა", + "play": "დაკვრა", + "search": "ძიება" + }, + "time": { + "about_day_ago": "დაახლოებით ერთი დღის წინ", + "about_hour_ago": "დაახლოებით ერთი საათის წინ", + "about_minute_ago": "დაახლოებით ერთი წუთის წინ", + "few_seconds_ago": "რამდენიმე წამის წინ", + "in_about_day": "დაახლოებით ერთი დღის შემდეგ", + "in_about_hour": "დაახლოებით ერთი საათის შემდეგ", + "in_about_minute": "დაახლოებით ერთი წუთის შემდეგ", + "in_few_seconds": "რამდენიმე წამის შემდეგ", + "in_n_days": "%(num)sდღეებიდან", + "in_n_hours": "%(num)sსაათის შემდეგ", + "in_n_minutes": "%(num)sწუთის შემდეგ", + "n_days_ago": "%(num)sდღის წინ", + "n_hours_ago": "%(num)sსაათის წინ", + "n_minutes_ago": "%(num)sწუთის წინ" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "შეცდომა აუდიოს ჩამოტვირთვისას", + "unnamed_audio": "უსახელო აუდიო" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/ko.json b/packages/shared-components/src/i18n/strings/ko.json new file mode 100644 index 0000000000..1cddba1df7 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/ko.json @@ -0,0 +1,113 @@ +{ + "a11y": { + "seek_bar_label": "오디오 탐색 바" + }, + "action": { + "delete": "삭제", + "dismiss": "버리기", + "edit": "편집", + "explore_rooms": "방 검색", + "invite": "초대", + "new_conversation": "새로운 대화", + "new_room": "새로운 방", + "new_video_room": "새로운 비디오룸", + "open_menu": "메뉴 열기", + "pause": "일시중지", + "play": "재생", + "remove": "제거", + "retry": "재시도", + "search": "찾기", + "start_chat": "채팅 시작" + }, + "common": { + "preferences": "환경설정" + }, + "left_panel": { + "open_dial_pad": "다이얼 패드 열기" + }, + "room": { + "context_menu": { + "title": "채팅방 옵션" + }, + "history_visibility_badge": { + "private": "신규 회원은 이전 기록을 볼 수 없습니다.", + "shared": "신규 회원은 기록을 볼 수 있습니다.", + "world_readable": "누구나 히스토리를 볼 수 있습니다" + }, + "status_bar": { + "delete_all": "전체 삭제", + "exceeded_resource_limit_description": "서비스를 계속 이용하시려면 서비스 관리자에게 문의하십시오.", + "exceeded_resource_limit_title": "이 홈 서버가 리소스 한도를 초과하여 귀하의 메시지가 전송되지 않았습니다.", + "failed_to_create_room_title": "이 사용자와 채팅을 시작할 수 없습니다", + "homeserver_blocked_title": "귀하의 메시지는 이 홈서버 관리자에 의해 차단되었기 때문에 전송되지 않았습니다.", + "monthly_user_limit_reached_title": "해당 홈서버의 월간 활성 사용자 수 제한에 도달하여 메시지가 전송되지 않았습니다.", + "requires_consent_agreement_title": "이용약관을 검토하고 동의하기 전까지는 메시지를 보낼 수 없습니다.", + "retry_all": "전체 재시도", + "select_messages_to_retry": "모든 메시지 또는 개별 메시지를 선택하여 재시도하거나 삭제할 수 있습니다.", + "server_connectivity_lost_description": "보낸 메시지는 연결이 복구될 때까지 저장됩니다.", + "server_connectivity_lost_title": "서버와의 연결이 끊어졌습니다.", + "some_messages_not_sent": "일부 메시지가 전송되지 않았습니다." + } + }, + "room_list": { + "appearance": "모양", + "open_space_menu": "스페이스 메뉴 열기", + "room_options": "채팅방 옵션", + "show_message_previews": "메시지 미리보기 표시", + "sort": "정렬", + "sort_type": { + "activity": "활동내역", + "atoz": "A-Z", + "unread_first": "안읽은 글 우선" + }, + "space_menu": { + "home": "스페이스 홈", + "space_settings": "스페이스 설정" + } + }, + "terms": { + "tac_button": "이용 약관을 검토하세요." + }, + "time": { + "about_day_ago": "약 1일 전", + "about_hour_ago": "약 1 시간 전", + "about_minute_ago": "약 1분 전", + "few_seconds_ago": "몇 초 전", + "in_about_day": "하루 정도 후", + "in_about_hour": "지금부터 한 시간 정도 후에", + "in_about_minute": "지금부터 약 1분 후", + "in_few_seconds": "몇 초 후", + "in_n_days": "지금부터 %(num)s 일 후에", + "in_n_hours": "지금부터 %(num)s 시간 후", + "in_n_minutes": "지금부터 %(num)s분 후", + "n_days_ago": "%(num)s일 전", + "n_hours_ago": "%(num)s 시간 전", + "n_minutes_ago": "%(num)s분 전" + }, + "timeline": { + "decryption_failure": { + "blocked": "발신자가 귀하의 기기가 인증되지 않았기 때문에 이 메시지를 수신하지 못하도록 차단했습니다.", + "historical_event_no_key_backup": "이 기기에서는 이전 메시지를 볼 수 없습니다.", + "historical_event_unverified_device": "이전 메시지에 액세스하려면 이 장치를 인증해야 합니다.", + "historical_event_user_not_joined": "이 메시지에 액세스할 수 없습니다.", + "sender_identity_previously_verified": "발신자의 확인된 신원이 재설정되었습니다.", + "sender_unsigned_device": "보안되지 않은 기기에서 전송됨.", + "unable_to_decrypt": "메시지를 해독할 수 없습니다." + }, + "m.audio": { + "audio_player": "오디오 플레이어", + "error_downloading_audio": "오디오 다운로드 중 오류 발생", + "unnamed_audio": "이름 없는 오디오" + } + }, + "widget": { + "context_menu": { + "move_left": "왼쪽으로 이동", + "move_right": "오른쪽으로 이동", + "remove": "모든 사람에게서 제거", + "revoke": "권한 취소", + "screenshot": "사진 촬영", + "start_audio_stream": "오디오 스트림 시작" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/lo.json b/packages/shared-components/src/i18n/strings/lo.json new file mode 100644 index 0000000000..9889020efb --- /dev/null +++ b/packages/shared-components/src/i18n/strings/lo.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "ລຶບ", + "dismiss": "ຍົກເລີກ", + "explore_rooms": "ການສຳຫຼວດຫ້ອງ", + "pause": "ຢຸດຊົ່ວຄາວ", + "play": "ຫຼິ້ນ", + "search": "ຊອກຫາ" + }, + "left_panel": { + "open_dial_pad": "ເປີດແຜ່ນປັດ" + }, + "time": { + "about_day_ago": "ປະມານຫນຶ່ງມື້ກ່ອນຫນ້ານີ້", + "about_hour_ago": "ປະມານຫນຶ່ງຊົ່ວໂມງກ່ອນຫນ້ານີ້", + "about_minute_ago": "ປະມານໜຶ່ງວິນາທີກ່ອນຫນ້ານີ້", + "few_seconds_ago": "ສອງສາມວິນາທີກ່ອນຫນ້ານີ້", + "in_about_day": "ປະມານນຶ່ງມື້ຈາກນີ້", + "in_about_hour": "ປະມານຫນຶ່ງຊົ່ວໂມງຈາກປະຈຸບັນນີ້", + "in_about_minute": "ປະມານໜຶ່ງນາທີຕໍ່ຈາກນີ້", + "in_few_seconds": "ສອງສາມວິນາທີຕໍ່ຈາກນີ້ໄປ", + "in_n_days": "%(num)s ມື້ຕໍ່ຈາກນີ້", + "in_n_hours": "%(num)s ຊົ່ວໂມງຈາກປະຈຸບັນນີ້", + "in_n_minutes": "%(num)s ນາທີຕໍ່ຈາກນີ້", + "n_days_ago": "%(num)sມື້ກ່ອນຫນ້ານີ້", + "n_hours_ago": "%(num)s ຊົ່ວໂມງກ່ອນ", + "n_minutes_ago": "%(num)s ນາທີກ່ອນ" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "ເກີດຄວາມຜິດພາດໃນການດາວໂຫຼດສຽງ", + "unnamed_audio": "ສຽງບໍ່ມີຊື່" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/lt.json b/packages/shared-components/src/i18n/strings/lt.json new file mode 100644 index 0000000000..de50724d9f --- /dev/null +++ b/packages/shared-components/src/i18n/strings/lt.json @@ -0,0 +1,24 @@ +{ + "action": { + "delete": "Ištrinti", + "dismiss": "Atmesti", + "explore_rooms": "Žvalgyti kambarius", + "search": "Ieškoti" + }, + "time": { + "about_day_ago": "maždaug prieš dieną", + "about_hour_ago": "maždaug prieš valandą", + "about_minute_ago": "maždaug prieš minutę", + "few_seconds_ago": "prieš kelias sekundes", + "in_about_day": "apie dieną nuo dabar", + "in_about_hour": "apie valandą nuo dabar", + "in_about_minute": "apie minutę nuo dabar", + "in_few_seconds": "keletą sekundžių nuo dabar", + "in_n_days": "%(num)s dienas(-ų) nuo dabar", + "in_n_hours": "%(num)s valandas(-ų) nuo dabar", + "in_n_minutes": "%(num)s minutes(-ų) nuo dabar", + "n_days_ago": "prieš %(num)s dienas(-ų)", + "n_hours_ago": "prieš %(num)s valandas(-ų)", + "n_minutes_ago": "prieš %(num)s minutes(-ų)" + } +} diff --git a/packages/shared-components/src/i18n/strings/lv.json b/packages/shared-components/src/i18n/strings/lv.json new file mode 100644 index 0000000000..5f2a3c04f1 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/lv.json @@ -0,0 +1,35 @@ +{ + "a11y": { + "seek_bar_label": "Audio meklēšanas josla" + }, + "action": { + "delete": "Izdzēst", + "dismiss": "Atmest", + "explore_rooms": "Pārlūkot istabas", + "pause": "Pauzēt", + "play": "Atskaņot", + "search": "Meklēt" + }, + "time": { + "about_day_ago": "aptuveni dienu iepriekš", + "about_hour_ago": "aptuveni stundu iepriekš", + "about_minute_ago": "aptuveni minūti iepriekš", + "few_seconds_ago": "pirms dažām sekundēm", + "in_about_day": "aptuveni dienu kopš šī brīža", + "in_about_hour": "aptuveni stundu kopš šī brīža", + "in_about_minute": "aptuveni minūti kopš šī brīža", + "in_few_seconds": "dažas sekundes kopš šī brīža", + "in_n_days": "%(num)s dienas kopš šī brīža", + "in_n_hours": "%(num)s stundas kopš šī brīža", + "in_n_minutes": "%(num)s minūtes kopš šī brīža", + "n_days_ago": "%(num)s dienas iepriekš", + "n_hours_ago": "%(num)s stundas iepriekš", + "n_minutes_ago": "%(num)s minūtes iepriekš" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Kļūda skaņas lejupielādēšanā", + "unnamed_audio": "Nenosaukts audio" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/mg_MG.json b/packages/shared-components/src/i18n/strings/mg_MG.json new file mode 100644 index 0000000000..7960042a96 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/mg_MG.json @@ -0,0 +1,38 @@ +{ + "a11y": { + "seek_bar_label": "Audio mitady bar" + }, + "action": { + "delete": "Esorina", + "dismiss": "Hanario", + "explore_rooms": "Tsidiho ny efitrano", + "pause": "Mihato", + "play": "Milalao", + "search": "Karohina" + }, + "left_panel": { + "open_dial_pad": "Sokafy ny dial pad" + }, + "time": { + "about_day_ago": "Tokony ho iray andro izay", + "about_hour_ago": "Manakaiky adin'iray Teo ho eo", + "about_minute_ago": "Misy iray minitra Teo izay", + "few_seconds_ago": "Segondra vitsy lasa", + "in_about_day": "Anatiny iray andro eo ho eo", + "in_about_hour": "Adiny iray eo ho eo", + "in_about_minute": "Afaka iray minitra eo ho eo", + "in_few_seconds": "Afaka segondra vitsy", + "in_n_days": "%(num) s andro manomboka izao", + "in_n_hours": "% (num) sAnatiny ora vitsivitsy", + "in_n_minutes": "% (Num) sAfaka minitra vitsy", + "n_days_ago": "%(num)s Andro vitsivitsy izay", + "n_hours_ago": "%(num)sOra maromaro", + "n_minutes_ago": "%(Num)s Minitra vitsivitsy izay" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Hadisoana tamin'ny fampidinana feo", + "unnamed_audio": "Audio tsy voatonona anarana" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/nb_NO.json b/packages/shared-components/src/i18n/strings/nb_NO.json new file mode 100644 index 0000000000..e3b53725c0 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/nb_NO.json @@ -0,0 +1,59 @@ +{ + "a11y": { + "seek_bar_label": "Søkelinje for lyd" + }, + "action": { + "delete": "Slett", + "dismiss": "Avvis", + "explore_rooms": "Se alle rom", + "pause": "Sett på pause", + "play": "Spill av", + "retry": "Prøv på nytt", + "search": "Søk" + }, + "left_panel": { + "open_dial_pad": "Åpne nummerpanelet" + }, + "room": { + "status_bar": { + "delete_all": "Slett alle", + "exceeded_resource_limit_description": "Kontakt tjenesteadministratoren din for å fortsette å bruke tjenesten.", + "exceeded_resource_limit_title": "Meldingen din ble ikke sendt fordi denne hjemmeserveren har overskredet en ressursgrense.", + "failed_to_create_room_title": "Kunne ikke starte en chat med denne brukeren", + "homeserver_blocked_title": "Meldingen din ble ikke sendt fordi denne hjemmeserveren er blokkert av dens administrator.", + "monthly_user_limit_reached_title": "Meldingen din ble ikke sendt fordi denne hjemmeserveren har nådd sin månedlige grense for aktive brukere.", + "requires_consent_agreement_title": "Du kan ikke sende noen meldinger før du har lest og godtatt våre vilkår og betingelser.", + "retry_all": "Prøv alle på nytt", + "select_messages_to_retry": "Du kan velge alle eller individuelle meldinger for å prøve på nytt eller slette", + "server_connectivity_lost_description": "Sendte meldinger vil bli lagret til tilkoblingen er tilbake.", + "server_connectivity_lost_title": "Tilkoblingen til tjeneren er nede.", + "some_messages_not_sent": "Noen av meldingene dine er ikke sendt" + } + }, + "terms": { + "tac_button": "Gå gjennom betingelser og vilkår" + }, + "time": { + "about_day_ago": "cirka 1 dag siden", + "about_hour_ago": "cirka 1 time siden", + "about_minute_ago": "cirka 1 minutt siden", + "few_seconds_ago": "noen sekunder siden", + "in_about_day": "rundt en dag fra nå", + "in_about_hour": "rundt en time fra nå", + "in_about_minute": "rundt et minutt fra nå", + "in_few_seconds": "om noen sekunder fra nå", + "in_n_days": "%(num)s dager fra nå", + "in_n_hours": "%(num)s timer fra nå", + "in_n_minutes": "%(num)s minutter fra nå", + "n_days_ago": "%(num)s dager siden", + "n_hours_ago": "%(num)s timer siden", + "n_minutes_ago": "%(num)s minutter siden" + }, + "timeline": { + "m.audio": { + "audio_player": "Lydavspiller", + "error_downloading_audio": "Feil ved nedlasting av lyd", + "unnamed_audio": "Ikke navngitt lyd" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/nl.json b/packages/shared-components/src/i18n/strings/nl.json new file mode 100644 index 0000000000..26d4febbbc --- /dev/null +++ b/packages/shared-components/src/i18n/strings/nl.json @@ -0,0 +1,38 @@ +{ + "a11y": { + "seek_bar_label": "Audio zoekbalk" + }, + "action": { + "delete": "Verwijderen", + "dismiss": "Sluiten", + "explore_rooms": "Kamers ontdekken", + "pause": "Pauze", + "play": "Afspelen", + "search": "Zoeken" + }, + "left_panel": { + "open_dial_pad": "Kiestoetsen openen" + }, + "time": { + "about_day_ago": "ongeveer een dag geleden", + "about_hour_ago": "ongeveer een uur geleden", + "about_minute_ago": "ongeveer een minuut geleden", + "few_seconds_ago": "enige tellen geleden", + "in_about_day": "over een dag of zo", + "in_about_hour": "over ongeveer een uur", + "in_about_minute": "over ongeveer een minuut", + "in_few_seconds": "over een paar tellen", + "in_n_days": "over %(num)s dagen", + "in_n_hours": "over %(num)s uur", + "in_n_minutes": "over %(num)s minuten", + "n_days_ago": "%(num)s dagen geleden", + "n_hours_ago": "%(num)s uur geleden", + "n_minutes_ago": "%(num)s minuten geleden" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Fout bij downloaden van audio", + "unnamed_audio": "Naamloze audio" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/pl.json b/packages/shared-components/src/i18n/strings/pl.json new file mode 100644 index 0000000000..a87e86e30a --- /dev/null +++ b/packages/shared-components/src/i18n/strings/pl.json @@ -0,0 +1,39 @@ +{ + "a11y": { + "seek_bar_label": "Pasek wyszukiwania audio" + }, + "action": { + "delete": "Usuń", + "dismiss": "Pomiń", + "explore_rooms": "Przeglądaj pokoje", + "pause": "Wstrzymaj", + "play": "Odtwórz", + "search": "Szukaj" + }, + "left_panel": { + "open_dial_pad": "Otwórz klawiaturę numeryczną" + }, + "time": { + "about_day_ago": "około dzień temu", + "about_hour_ago": "około godziny temu", + "about_minute_ago": "około minuty temu", + "few_seconds_ago": "kilka sekund temu", + "in_about_day": "około dnia od teraz", + "in_about_hour": "około godziny od teraz", + "in_about_minute": "około minuty od teraz", + "in_few_seconds": "za kilka sekund", + "in_n_days": "za %(num)s dni", + "in_n_hours": "za %(num)s godzin", + "in_n_minutes": "za %(num)s minut", + "n_days_ago": "%(num)s dni temu", + "n_hours_ago": "%(num)s godzin temu", + "n_minutes_ago": "%(num)s minut temu" + }, + "timeline": { + "m.audio": { + "audio_player": "Odtwarzacz audio", + "error_downloading_audio": "Wystąpił błąd w trakcie pobierania audio", + "unnamed_audio": "Audio bez nazwy" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/pt.json b/packages/shared-components/src/i18n/strings/pt.json new file mode 100644 index 0000000000..8021f07e41 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/pt.json @@ -0,0 +1,38 @@ +{ + "a11y": { + "seek_bar_label": "Barra de procura de áudio" + }, + "action": { + "delete": "Apagar", + "dismiss": "Descartar", + "explore_rooms": "Explorar rooms", + "pause": "Pausar", + "play": "Reproduzir", + "search": "Pesquisar" + }, + "left_panel": { + "open_dial_pad": "Abre o teclado de marcação" + }, + "time": { + "about_day_ago": "há cerca de um dia", + "about_hour_ago": "há cerca de uma hora", + "about_minute_ago": "há cerca de um minuto", + "few_seconds_ago": "há alguns segundos atrás", + "in_about_day": "daqui a um dia", + "in_about_hour": "daqui a uma hora", + "in_about_minute": "daqui a um minuto", + "in_few_seconds": "daqui a alguns segundos", + "in_n_days": "daqui a %(num)s dias", + "in_n_hours": "daqui a %(num)s horas", + "in_n_minutes": "daqui a %(num)s minutos", + "n_days_ago": "%(num)s dias atrás", + "n_hours_ago": "%(num)s horas atrás", + "n_minutes_ago": "%(num)s minutos atrás" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Erro ao descarregar áudio", + "unnamed_audio": "Áudio sem nome" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/pt_BR.json b/packages/shared-components/src/i18n/strings/pt_BR.json new file mode 100644 index 0000000000..3716eac288 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/pt_BR.json @@ -0,0 +1,39 @@ +{ + "a11y": { + "seek_bar_label": "Barra de busca de áudio" + }, + "action": { + "delete": "Excluir", + "dismiss": "Dispensar", + "explore_rooms": "Explorar salas", + "pause": "Pausar", + "play": "Reproduzir", + "search": "Buscar" + }, + "left_panel": { + "open_dial_pad": "Abrir o teclado de discagem" + }, + "time": { + "about_day_ago": "há aproximadamente um dia", + "about_hour_ago": "há aproximadamente uma hora", + "about_minute_ago": "há aproximadamente um minuto", + "few_seconds_ago": "há alguns segundos", + "in_about_day": "dentro de aproximadamente um dia", + "in_about_hour": "dentro de aproximadamente uma hora", + "in_about_minute": "dentro de aproximadamente um minuto", + "in_few_seconds": "dentro de alguns segundos", + "in_n_days": "dentro de %(num)s dias", + "in_n_hours": "dentro de %(num)s horas", + "in_n_minutes": "dentro de %(num)s minutos", + "n_days_ago": "há %(num)s dias", + "n_hours_ago": "há %(num)s horas", + "n_minutes_ago": "há %(num)s minutos" + }, + "timeline": { + "m.audio": { + "audio_player": "Reprodutor de Áudio", + "error_downloading_audio": "Erro ao baixar o áudio", + "unnamed_audio": "Áudio sem nome" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/ru.json b/packages/shared-components/src/i18n/strings/ru.json new file mode 100644 index 0000000000..3396855dfa --- /dev/null +++ b/packages/shared-components/src/i18n/strings/ru.json @@ -0,0 +1,58 @@ +{ + "a11y": { + "seek_bar_label": "Панель поиска аудио" + }, + "action": { + "delete": "Удалить", + "dismiss": "Закрыть", + "explore_rooms": "Обзор комнат", + "pause": "Пауза", + "play": "Воспроизведение", + "search": "Поиск" + }, + "left_panel": { + "open_dial_pad": "Открыть панель набора номера" + }, + "room": { + "status_bar": { + "delete_all": "Удалить всё", + "exceeded_resource_limit_description": "Свяжись с администратором сервиса, чтобы продолжить пользоваться услугой.", + "exceeded_resource_limit_title": "Ваше сообщение не было отправлено, поскольку на этом домашнем сервере превышен лимит ресурсов.", + "failed_to_create_room_title": "Не удалось начать чат с этим пользователем.", + "homeserver_blocked_title": "Ваше сообщение не было отправлено, потому что этот домашний сервер заблокирован его администратором.", + "monthly_user_limit_reached_title": "Ваше сообщение не было отправлено, потому что на этом домашнем сервере достигнут лимит ежемесячных активных пользователей.", + "requires_consent_agreement_title": "Вы не сможете отправлять сообщения, пока не ознакомитесь с условиями и положениями и не согласитесь с ними.", + "retry_all": "Повторить попытку для всех", + "select_messages_to_retry": "Вы можете выбрать все или отдельные сообщения для повторной попытки или удаления", + "server_connectivity_lost_description": "Отправленные сообщения будут сохранены, пока соединение не восстановится.", + "server_connectivity_lost_title": "Соединение с сервером потеряно", + "some_messages_not_sent": "Некоторые сообщения не были отправлены" + } + }, + "terms": { + "tac_button": "Просмотр условий и положений" + }, + "time": { + "about_day_ago": "около суток назад", + "about_hour_ago": "около часа назад", + "about_minute_ago": "около минуты назад", + "few_seconds_ago": "несколько секунд назад", + "in_about_day": "примерно через день", + "in_about_hour": "примерно через час", + "in_about_minute": "примерно через минуту", + "in_few_seconds": "несколько секунд назад", + "in_n_days": "%(num)s дней спустя", + "in_n_hours": "%(num)s часов спустя", + "in_n_minutes": "%(num)s минут спустя", + "n_days_ago": "%(num)s дней назад", + "n_hours_ago": "%(num)s часов назад", + "n_minutes_ago": "%(num)s минут назад" + }, + "timeline": { + "m.audio": { + "audio_player": "Аудиоплеер", + "error_downloading_audio": "Ошибка загрузки аудио", + "unnamed_audio": "Безымянное аудио" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/sk.json b/packages/shared-components/src/i18n/strings/sk.json new file mode 100644 index 0000000000..16a359b810 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/sk.json @@ -0,0 +1,84 @@ +{ + "a11y": { + "seek_bar_label": "Panel vyhľadávania zvuku" + }, + "action": { + "delete": "Vymazať", + "dismiss": "Zamietnuť", + "explore_rooms": "Preskúmať miestnosti", + "invite": "Pozvať", + "new_conversation": "Nová konverzácia", + "new_room": "Nová miestnosť", + "new_video_room": "Nová video miestnosť", + "open_menu": "Otvoriť ponuku", + "pause": "Pozastaviť", + "play": "Prehrať", + "retry": "Skúsiť znova", + "search": "Hľadať", + "start_chat": "Začať konverzáciu" + }, + "common": { + "preferences": "Predvoľby" + }, + "left_panel": { + "open_dial_pad": "Otvoriť číselník" + }, + "room": { + "context_menu": { + "title": "Možnosti miestnosti" + }, + "status_bar": { + "delete_all": "Vymazať všetko", + "exceeded_resource_limit_description": "Ak chcete službu naďalej používať, kontaktujte svojho správcu služby.", + "exceeded_resource_limit_title": "Vaša správa nebola odoslaná, pretože tento domovský server prekročil limit zdrojov.", + "failed_to_create_room_title": "Nepodarilo sa začať konverzáciu s týmto používateľom", + "homeserver_blocked_title": "Vaša správa nebola odoslaná, pretože tento domovský server bol blokovaný jeho správcom.", + "monthly_user_limit_reached_title": "Vaša správa nebola odoslaná, pretože tento domovský server dosiahol svoj mesačný limit aktívnych používateľov.", + "requires_consent_agreement_title": "Nemôžete odosielať žiadne správy, kým si neprečítate a nesúhlasíte s našimi zmluvnými podmienkami.", + "retry_all": "Skúsiť všetko znova", + "select_messages_to_retry": "Môžete vybrať všetky alebo jednotlivé správy, ktoré chcete opakovane odoslať alebo vymazať", + "server_connectivity_lost_description": "Odoslané správy budú uložené, kým sa neobnoví pripojenie.", + "server_connectivity_lost_title": "Spojenie so serverom bolo prerušené.", + "some_messages_not_sent": "Niektoré vaše správy ešte neboli odoslané" + } + }, + "room_list": { + "open_space_menu": "Otvoriť ponuku priestoru", + "room_options": "Možnosti miestnosti", + "sort": "Zoradiť", + "sort_type": { + "activity": "Aktivita", + "atoz": "A-Z" + }, + "space_menu": { + "home": "Domov priestoru", + "space_settings": "Nastavenia priestoru" + } + }, + "terms": { + "tac_button": "Prečítať zmluvné podmienky" + }, + "time": { + "about_day_ago": "asi pred jedným dňom", + "about_hour_ago": "približne pred hodinou", + "about_minute_ago": "približne pred minútou", + "few_seconds_ago": "pred pár sekundami", + "in_about_day": "približne o deň", + "in_about_hour": "približne o hodinu", + "in_about_minute": "približne o minútu", + "in_few_seconds": "o pár sekúnd", + "in_n_days": "o %(num)s dní", + "in_n_hours": "o %(num)s hodín", + "in_n_minutes": "o %(num)s minút", + "n_days_ago": "pred %(num)s dňami", + "n_hours_ago": "pred %(num)s hodinami", + "n_minutes_ago": "pred %(num)s minútami" + }, + "timeline": { + "m.audio": { + "audio_player": "Prehrávač zvuku", + "error_downloading_audio": "Chyba pri sťahovaní zvuku", + "unnamed_audio": "Nepomenovaný zvukový záznam" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/sq.json b/packages/shared-components/src/i18n/strings/sq.json new file mode 100644 index 0000000000..64e80a6300 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/sq.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "Fshije", + "dismiss": "Mos e merr parasysh", + "explore_rooms": "Eksploroni dhoma", + "pause": "Ndalesë", + "play": "Luaje", + "search": "Kërkoni" + }, + "left_panel": { + "open_dial_pad": "Hap butona numrash" + }, + "time": { + "about_day_ago": "rreth një ditë më parë", + "about_hour_ago": "rreth një orë më parë", + "about_minute_ago": "rreth një minutë më parë", + "few_seconds_ago": "pak sekonda më parë", + "in_about_day": "rreth një ditë nga tani", + "in_about_hour": "rreth një orë nga tani", + "in_about_minute": "rreth një minutë nga tani", + "in_few_seconds": "pak sekonda nga tani", + "in_n_days": "%(num)s ditë nga tani", + "in_n_hours": "%(num)s orë nga tani", + "in_n_minutes": "%(num)s minuta nga tani", + "n_days_ago": "%(num)s ditë më parë", + "n_hours_ago": "%(num)s orë më parë", + "n_minutes_ago": "%(num)s minuta më parë" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Gabim në shkarkim audioje", + "unnamed_audio": "Audio pa emër" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/sv.json b/packages/shared-components/src/i18n/strings/sv.json new file mode 100644 index 0000000000..62fd22c182 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/sv.json @@ -0,0 +1,39 @@ +{ + "a11y": { + "seek_bar_label": "Förloppsfält för ljud" + }, + "action": { + "delete": "Radera", + "dismiss": "Avvisa", + "explore_rooms": "Utforska rum", + "pause": "Pausa", + "play": "Spela", + "search": "Sök" + }, + "left_panel": { + "open_dial_pad": "Öppna knappsats" + }, + "time": { + "about_day_ago": "cirka en dag sedan", + "about_hour_ago": "cirka en timme sedan", + "about_minute_ago": "cirka en minut sedan", + "few_seconds_ago": "några sekunder sedan", + "in_about_day": "om cirka en dag", + "in_about_hour": "om cirka en timme", + "in_about_minute": "om cirka en minut", + "in_few_seconds": "om några sekunder", + "in_n_days": "om %(num)s dagar", + "in_n_hours": "om %(num)s timmar", + "in_n_minutes": "om %(num)s minuter", + "n_days_ago": "%(num)s dagar sedan", + "n_hours_ago": "%(num)s timmar sedan", + "n_minutes_ago": "%(num)s minuter sedan" + }, + "timeline": { + "m.audio": { + "audio_player": "Ljudspelare", + "error_downloading_audio": "Fel vid nedladdning av ljud", + "unnamed_audio": "Namnlöst ljud" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/tr.json b/packages/shared-components/src/i18n/strings/tr.json new file mode 100644 index 0000000000..5fabdf72f2 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/tr.json @@ -0,0 +1,38 @@ +{ + "a11y": { + "seek_bar_label": "Ses arama çubuğu" + }, + "action": { + "delete": "Sil", + "dismiss": "Kapat", + "explore_rooms": "Odaları keşfet", + "pause": "Durdur", + "play": "Oynat", + "search": "Ara" + }, + "left_panel": { + "open_dial_pad": "Arama tuşlarını aç" + }, + "time": { + "about_day_ago": "yaklaşık bir gün önce", + "about_hour_ago": "yaklaşık bir saat önce", + "about_minute_ago": "yaklaşık bir dakika önce", + "few_seconds_ago": "bir kaç saniye önce", + "in_about_day": "şu andan itibaren yaklaşık bir gün", + "in_about_hour": "şu andan itibaren yaklaşık bir saat", + "in_about_minute": "şu andan itibaren yaklaşık bir dakika", + "in_few_seconds": "şu andan itibaren bir kaç saniye", + "in_n_days": "şu andan itibaren %(num)s gün", + "in_n_hours": "şu andan itibaren %(num)s saat", + "in_n_minutes": "şu andan itibaren %(num)s dakika", + "n_days_ago": "%(num)s gün önce", + "n_hours_ago": "%(num)s saat önce", + "n_minutes_ago": "%(num)s dakika önce" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Ses dosyası indirilirken hata oluştu", + "unnamed_audio": "İsimsiz ses" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/uk.json b/packages/shared-components/src/i18n/strings/uk.json new file mode 100644 index 0000000000..9470ff2299 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/uk.json @@ -0,0 +1,85 @@ +{ + "a11y": { + "seek_bar_label": "Панель гортання аудіо" + }, + "action": { + "delete": "Видалити", + "dismiss": "Відхилити", + "edit": "Змінити", + "explore_rooms": "Каталог кімнат", + "invite": "Запросити", + "new_conversation": "Нова розмова", + "new_room": "Нова кімната", + "new_video_room": "Нова відеокімната", + "open_menu": "Відкрити меню", + "pause": "Призупинити", + "play": "Відтворити", + "remove": "Вилучити", + "retry": "Повторити спробу", + "search": "Пошук", + "start_chat": "Розпочати бесіду" + }, + "common": { + "preferences": "Налаштування" + }, + "left_panel": { + "open_dial_pad": "Відкрити номеронабирач" + }, + "room": { + "context_menu": { + "title": "Параметри кімнати" + }, + "status_bar": { + "delete_all": "Видалити все", + "server_connectivity_lost_title": "Втрачено зв'язок з сервером." + } + }, + "room_list": { + "appearance": "Вигляд", + "open_space_menu": "Відкрити меню простору", + "room_options": "Параметри кімнати", + "show_message_previews": "Показати попередній перегляд повідомлень", + "sort": "Сортувати", + "sort_type": { + "activity": "Діяльність", + "atoz": "А-Я", + "unread_first": "Спочатку непрочитані" + }, + "space_menu": { + "home": "Домівка простору", + "space_settings": "Налаштування простору" + } + }, + "time": { + "about_day_ago": "близько доби тому", + "about_hour_ago": "близько години тому", + "about_minute_ago": "близько хвилини тому", + "few_seconds_ago": "Декілька секунд тому", + "in_about_day": "приблизно через день", + "in_about_hour": "приблизно через годину", + "in_about_minute": "приблизно через хвилинку", + "in_few_seconds": "декілька секунд тому", + "in_n_days": "%(num)s днів по тому", + "in_n_hours": "%(num)s годин по тому", + "in_n_minutes": "%(num)s хвилин по тому", + "n_days_ago": "%(num)s днів тому", + "n_hours_ago": "%(num)s годин тому", + "n_minutes_ago": "%(num)s хвилин тому" + }, + "timeline": { + "m.audio": { + "audio_player": "Звуковий програвач", + "error_downloading_audio": "Помилка завантаження аудіо", + "unnamed_audio": "Аудіо без назви" + } + }, + "widget": { + "context_menu": { + "move_left": "Перемістити вліво", + "move_right": "Перемістити вправо", + "remove": "Вилучити для всіх", + "revoke": "Відкликати дозволи", + "screenshot": "Зробити знімок" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/vi.json b/packages/shared-components/src/i18n/strings/vi.json new file mode 100644 index 0000000000..edb065ffe8 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/vi.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "Xoá", + "dismiss": "Bỏ qua", + "explore_rooms": "Khám phá các phòng", + "pause": "Tạm dừng", + "play": "Chạy", + "search": "Tìm kiếm" + }, + "left_panel": { + "open_dial_pad": "Mở bàn phím quay số" + }, + "time": { + "about_day_ago": "khoảng một ngày trước", + "about_hour_ago": "khoảng một giờ trước", + "about_minute_ago": "khoảng một phút trước", + "few_seconds_ago": "vài giây trước", + "in_about_day": "khoảng một ngày kể từ bây giờ", + "in_about_hour": "khoảng một giờ kể từ bây giờ", + "in_about_minute": "khoảng một phút kể từ bây giờ", + "in_few_seconds": "một vài giây kể từ bây giờ", + "in_n_days": "%(num)s ngày kể từ bây giờ", + "in_n_hours": "%(num)s giờ kể từ bây giờ", + "in_n_minutes": "%(num)s phút kể từ bây giờ", + "n_days_ago": "%(num)s ngày trước", + "n_hours_ago": "%(num)s giờ trước", + "n_minutes_ago": "%(num)s phút trước" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "Lỗi khi tải xuống âm thanh", + "unnamed_audio": "Âm thanh không tên" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/zh_Hans.json b/packages/shared-components/src/i18n/strings/zh_Hans.json new file mode 100644 index 0000000000..7377fbbf45 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/zh_Hans.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "删除", + "dismiss": "忽略", + "explore_rooms": "查找房间", + "pause": "暂停", + "play": "播放", + "search": "搜索" + }, + "left_panel": { + "open_dial_pad": "打开拨号键盘" + }, + "time": { + "about_day_ago": "约一天前", + "about_hour_ago": "约一小时前", + "about_minute_ago": "约一分钟前", + "few_seconds_ago": "数秒前", + "in_about_day": "从现在开始约一天", + "in_about_hour": "从现在开始约一小时", + "in_about_minute": "从现在开始约一分钟", + "in_few_seconds": "从现在开始数秒", + "in_n_days": "从现在开始%(num)s天", + "in_n_hours": "从现在开始%(num)s小时", + "in_n_minutes": "从现在开始%(num)s分钟", + "n_days_ago": "%(num)s天前", + "n_hours_ago": "%(num)s小时前", + "n_minutes_ago": "%(num)s分钟前" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "下载音频时出错", + "unnamed_audio": "未命名的音频" + } + } +} diff --git a/packages/shared-components/src/i18n/strings/zh_Hant.json b/packages/shared-components/src/i18n/strings/zh_Hant.json new file mode 100644 index 0000000000..8f5e3ab9b3 --- /dev/null +++ b/packages/shared-components/src/i18n/strings/zh_Hant.json @@ -0,0 +1,35 @@ +{ + "action": { + "delete": "刪除", + "dismiss": "關閉", + "explore_rooms": "探索聊天室", + "pause": "暫停", + "play": "播放", + "search": "搜尋" + }, + "left_panel": { + "open_dial_pad": "開啟撥號鍵盤" + }, + "time": { + "about_day_ago": "大約一天前", + "about_hour_ago": "大約一小時前", + "about_minute_ago": "大約一分鐘前", + "few_seconds_ago": "數秒前", + "in_about_day": "從現在開始大約一天", + "in_about_hour": "從現在開始大約一小時", + "in_about_minute": "從現在開始大約一分鐘", + "in_few_seconds": "從現在開始數秒鐘", + "in_n_days": "從現在開始 %(num)s 天", + "in_n_hours": "從現在開始 %(num)s 小時", + "in_n_minutes": "從現在開始 %(num)s 分鐘", + "n_days_ago": "%(num)s 天前", + "n_hours_ago": "%(num)s 小時前", + "n_minutes_ago": "%(num)s 分鐘前" + }, + "timeline": { + "m.audio": { + "error_downloading_audio": "下載音訊時發生錯誤", + "unnamed_audio": "未命名的音訊" + } + } +} diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index 849634e9da..96216b8ee2 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -12,16 +12,29 @@ export * from "./audio/PlayPauseButton"; export * from "./audio/SeekBar"; export * from "./avatar/AvatarWithDetails"; export * from "./composer/Banner"; -export * from "./composer/HistoryVisibleBannerView"; +export * from "./crypto/SasEmoji"; +export * from "./event-tiles/EventTileBubble"; export * from "./event-tiles/TextualEventView"; export * from "./message-body/MediaBody"; +export * from "./message-body/DecryptionFailureBodyView"; +export * from "./message-body/ReactionsRowButtonTooltip"; +export * from "./message-body/TimelineSeparator/"; export * from "./pill-input/Pill"; export * from "./pill-input/PillInput"; +export * from "./room/RoomStatusBar"; +export * from "./room/HistoryVisibilityBadge"; export * from "./rich-list/RichItem"; export * from "./rich-list/RichList"; +export * from "./room-list/RoomListHeaderView"; export * from "./room-list/RoomListSearchView"; +export * from "./room-list/RoomListView"; +export * from "./room-list/RoomListItem"; +export * from "./room-list/RoomListPrimaryFilters"; +export * from "./room-list/VirtualizedRoomListView"; export * from "./utils/Box"; export * from "./utils/Flex"; +export * from "./right-panel/WidgetContextMenu"; +export * from "./utils/VirtualizedList"; // Utils export * from "./utils/i18n"; @@ -31,12 +44,5 @@ export * from "./utils/DateUtils"; export * from "./utils/numbers"; export * from "./utils/FormattingUtils"; export * from "./utils/I18nApi"; - // MVVM export * from "./viewmodel"; -export * from "./useMockedViewModel"; -export * from "./useViewModel"; - -// i18n (we must export this directly in order to not confuse the type bundler, it seems, -// otherwise it will leave it as a relative import rather than bundling it) -export type * from "./i18nKeys.d.ts"; diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.module.css b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.module.css new file mode 100644 index 0000000000..45e21088e4 --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.module.css @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.content { + color: var(--cpd-color-text-secondary); + font-style: italic; +} + +/* Formatting for errors due to sender trust requirement failures */ +.error > span { + /* some space between the (/) icon and text */ + display: inline-flex; + gap: var(--cpd-space-1x); + + /* Center vertically */ + align-items: center; +} + +.icon { + box-sizing: border-box; + flex: 0 0 16px; +} diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx new file mode 100644 index 0000000000..741f7420de --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; + +import type { Meta, StoryFn } from "@storybook/react-vite"; +import { + DecryptionFailureBodyView, + DecryptionFailureReason, + type DecryptionFailureBodyViewSnapshot, +} from "./DecryptionFailureBodyView"; +import { useMockedViewModel } from "../../viewmodel/useMockedViewModel"; + +type DecryptionFailureBodyProps = DecryptionFailureBodyViewSnapshot; + +const DecryptionFailureBodyViewWrapper = ({ ...rest }: DecryptionFailureBodyProps): JSX.Element => { + const vm = useMockedViewModel(rest, {}); + + return ; +}; + +export default { + title: "MessageBody/DecryptionFailureBodyView", + component: DecryptionFailureBodyViewWrapper, + tags: ["autodocs"], + argTypes: { + decryptionFailureReason: { + options: Object.entries(DecryptionFailureReason) + .filter(([key, value]) => key === value) + .map(([key]) => key), + control: { type: "select" }, + }, + }, + args: { + decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + isLocalDeviceVerified: true, + extraClassNames: ["extra_class"], + }, +} as Meta; + +const Template: StoryFn = (args) => ( + +); + +export const Default = Template.bind({}); + +export const HasExtraClassNames = Template.bind({}); +HasExtraClassNames.args = { + decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + extraClassNames: ["extra_class_1", "extra_class_2"], +}; + +export const HasErrorClassName = Template.bind({}); +HasErrorClassName.args = { + decryptionFailureReason: DecryptionFailureReason.UNSIGNED_SENDER_DEVICE, + extraClassNames: undefined, +}; + +export const HasErrorBlockIcon = Template.bind({}); +HasErrorBlockIcon.args = { + decryptionFailureReason: DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED, + extraClassNames: undefined, +}; + +export const HasBackupConfiguredVerifiedFalse = Template.bind({}); +HasBackupConfiguredVerifiedFalse.args = { + decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + isLocalDeviceVerified: false, + extraClassNames: undefined, +}; + +export const HasBackupConfiguredVerifiedTrue = Template.bind({}); +HasBackupConfiguredVerifiedTrue.args = { + decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + isLocalDeviceVerified: true, + extraClassNames: undefined, +}; diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.test.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.test.tsx new file mode 100644 index 0000000000..b886584c35 --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.test.tsx @@ -0,0 +1,149 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { composeStories } from "@storybook/react-vite"; +import { render } from "@test-utils"; +import React from "react"; +import { describe, it, expect } from "vitest"; + +import { DecryptionFailureBodyView, DecryptionFailureReason } from "./DecryptionFailureBodyView"; +import { MockViewModel } from "../../viewmodel"; +import * as stories from "./DecryptionFailureBodyView.stories"; + +const { HasExtraClassNames } = composeStories(stories); + +describe("DecryptionFailureBodyView", () => { + function customRender( + decryptionFailureReason: DecryptionFailureReason, + isLocalDeviceVerified: boolean = false, + extraClassNames: string[] | undefined = undefined, + ): ReturnType { + return render( + , + ); + } + + function customRenderWithRef(ref: React.RefObject): ReturnType { + return render( + , + ); + } + + it("Should display with extra class names", () => { + // When + const { container } = render(); + + // Then + expect(container).toMatchSnapshot(); + }); + + it.each([true, false])(`Should display "Unable to decrypt message and device verification is %s"`, (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.UNABLE_TO_DECRYPT, verified); + + // Then + expect(container).toHaveTextContent("Unable to decrypt message"); + expect(container).toMatchSnapshot(); + }); + + it.each([true, false])( + `Should display "The sender has blocked you from receiving this message and device verification is %s"`, + (verified) => { + // When + const { container } = customRender( + DecryptionFailureReason.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE, + verified, + ); + + // Then + expect(container).toHaveTextContent( + "The sender has blocked you from receiving this message because your device is unverified", + ); + expect(container).toMatchSnapshot(); + }, + ); + + it.each([true, false])( + "should handle historical messages with no key backup and device verification is %s", + (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.HISTORICAL_MESSAGE_NO_KEY_BACKUP, verified); + + // Then + expect(container).toHaveTextContent("Historical messages are not available on this device"); + expect(container).toMatchSnapshot(); + }, + ); + + it.each([true, false])( + "should handle historical messages when there is a backup and device verification is %s", + (verified) => { + // When + const { container } = customRender( + DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + verified, + ); + + // Then + expect(container).toHaveTextContent( + verified ? "Unable to decrypt" : "You need to verify this device for access to historical messages", + ); + }, + ); + + it.each([true, false])( + "should handle undecryptable pre-join messages and device verification is %s", + (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.HISTORICAL_MESSAGE_USER_NOT_JOINED, verified); + + // Then + expect(container).toHaveTextContent("You don't have access to this message"); + expect(container).toMatchSnapshot(); + }, + ); + + it.each([true, false])( + "should handle messages from users who change identities after verification and device verification is %s", + (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED, verified); + + // Then + expect(container).toHaveTextContent("Sender's verified identity was reset"); + expect(container).toMatchSnapshot(); + }, + ); + + it.each([true, false])( + "should handle messages from unverified devices and device verification is %s", + (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.UNSIGNED_SENDER_DEVICE, verified); + + // Then + expect(container).toHaveTextContent("Sent from an insecure device"); + expect(container).toMatchSnapshot(); + }, + ); + + it("should handle ref input", async () => { + const ref = React.createRef(); + // When + const { container } = customRenderWithRef(ref); + + // Then + expect(container).toBeInstanceOf(HTMLDivElement); + expect(container.firstChild).toHaveTextContent("Unable to decrypt message"); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); +}); diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.tsx new file mode 100644 index 0000000000..23b0d639d5 --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.tsx @@ -0,0 +1,176 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import classNames from "classnames"; +import React, { type JSX } from "react"; +import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { type I18nApi } from "@element-hq/element-web-module-api"; + +import { type ViewModel } from "../../viewmodel/ViewModel"; +import { useViewModel } from "../../viewmodel/useViewModel"; +import styles from "./DecryptionFailureBodyView.module.css"; +import { useI18n } from "../../utils/i18nContext"; + +/** + * A reason code for a failure to decrypt an event. + */ +export enum DecryptionFailureReason { + /** A special case of {@link MEGOLM_KEY_WITHHELD}: the sender has told us it is withholding the key, because the current device is unverified. */ + MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE = "MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE", + + /** + * Message was sent before the current device was created; there is no key backup on the server, so this + * decryption failure is expected. + */ + HISTORICAL_MESSAGE_NO_KEY_BACKUP = "HISTORICAL_MESSAGE_NO_KEY_BACKUP", + + /** + * Message was sent before the current device was created; there was a key backup on the server, but we don't + * seem to have access to the backup. (Probably we don't have the right key.) + */ + HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED = "HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED", + + /** + * Message was sent when the user was not a member of the room. + */ + HISTORICAL_MESSAGE_USER_NOT_JOINED = "HISTORICAL_MESSAGE_USER_NOT_JOINED", + + /** + * The sender's identity is not verified, but was previously verified. + */ + SENDER_IDENTITY_PREVIOUSLY_VERIFIED = "SENDER_IDENTITY_PREVIOUSLY_VERIFIED", + + /** + * The sender device is not cross-signed. This will only be used if the + * device isolation mode is set to `OnlySignedDevicesIsolationMode`. + */ + UNSIGNED_SENDER_DEVICE = "UNSIGNED_SENDER_DEVICE", + + /** + * Default message for decryption failures. + */ + UNABLE_TO_DECRYPT = "UNABLE_TO_DECRYPT", +} + +export interface DecryptionFailureBodyViewSnapshot { + /** + * The decryption failure reason of the event. + */ + decryptionFailureReason: DecryptionFailureReason; + /** + * The local device verification state. + */ + isLocalDeviceVerified?: boolean; + /** + * Extra CSS classes to apply to the component + */ + extraClassNames?: string[]; +} + +/** + * The view model for the component. + */ +export type DecryptionFailureBodyViewModel = ViewModel; + +interface DecryptionFailureBodyViewProps { + /** + * The view model for the component. + */ + vm: DecryptionFailureBodyViewModel; + /** + * React ref to attach to any React components returned + */ + ref?: React.RefObject; +} + +/** + * Resolve the localized error message for a decryption failure reason. + * + * @param i18nApi - I18n API used to translate message keys. + * @param decryptionFailureReason - Reason code for the decryption failure. + * @param isLocalDeviceVerified - Whether the local device is verified, used for certain historical cases. + */ +function getErrorMessage( + i18nApi: I18nApi, + decryptionFailureReason: DecryptionFailureReason, + isLocalDeviceVerified?: boolean, +): string | JSX.Element { + const _t = i18nApi.translate; + + switch (decryptionFailureReason) { + case DecryptionFailureReason.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE: + return _t("timeline|decryption_failure|blocked"); + + case DecryptionFailureReason.HISTORICAL_MESSAGE_NO_KEY_BACKUP: + return _t("timeline|decryption_failure|historical_event_no_key_backup"); + + case DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED: + if (isLocalDeviceVerified === false) { + // The user seems to have a key backup, so prompt them to verify in the hope that doing so will + // mean we can restore from backup and we'll get the key for this message. + return _t("timeline|decryption_failure|historical_event_unverified_device"); + } + // otherwise, use the default. + break; + + case DecryptionFailureReason.HISTORICAL_MESSAGE_USER_NOT_JOINED: + // TODO: event should be hidden instead of showing this error. + // To be revisited as part of https://github.com/element-hq/element-meta/issues/2449 + return _t("timeline|decryption_failure|historical_event_user_not_joined"); + + case DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: + return ( + + + {_t("timeline|decryption_failure|sender_identity_previously_verified")} + + ); + + case DecryptionFailureReason.UNSIGNED_SENDER_DEVICE: + // TODO: event should be hidden instead of showing this error. + // To be revisited as part of https://github.com/element-hq/element-meta/issues/2449 + return ( + + + {_t("timeline|decryption_failure|sender_unsigned_device")} + + ); + } + return _t("timeline|decryption_failure|unable_to_decrypt"); +} + +/** + * Get the extra CSS class for the given decryption failure reason, when one applies. + */ +function errorClassName(decryptionFailureReason: DecryptionFailureReason): string | null { + switch (decryptionFailureReason) { + case DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: + case DecryptionFailureReason.UNSIGNED_SENDER_DEVICE: + return styles.error; + } + return null; +} + +/** + * A placeholder element for messages that could not be decrypted + * + * @example + * ```tsx + * + * ``` + */ +export function DecryptionFailureBodyView({ vm, ref }: Readonly): JSX.Element { + const i18nApi = useI18n(); + const { decryptionFailureReason, isLocalDeviceVerified, extraClassNames } = useViewModel(vm); + const classes = classNames(styles.content, errorClassName(decryptionFailureReason), extraClassNames); + + return ( +
+ {getErrorMessage(i18nApi, decryptionFailureReason, isLocalDeviceVerified)} +
+ ); +} diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/__snapshots__/DecryptionFailureBodyView.test.tsx.snap b/packages/shared-components/src/message-body/DecryptionFailureBodyView/__snapshots__/DecryptionFailureBodyView.test.tsx.snap new file mode 100644 index 0000000000..7bd5899caf --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/__snapshots__/DecryptionFailureBodyView.test.tsx.snap @@ -0,0 +1,187 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DecryptionFailureBodyView > Should display "The sender has blocked you from receiving this message and device verification is false" 1`] = ` +
+
+ The sender has blocked you from receiving this message because your device is unverified +
+
+`; + +exports[`DecryptionFailureBodyView > Should display "The sender has blocked you from receiving this message and device verification is true" 1`] = ` +
+
+ The sender has blocked you from receiving this message because your device is unverified +
+
+`; + +exports[`DecryptionFailureBodyView > Should display "Unable to decrypt message and device verification is false" 1`] = ` +
+
+ Unable to decrypt message +
+
+`; + +exports[`DecryptionFailureBodyView > Should display "Unable to decrypt message and device verification is true" 1`] = ` +
+
+ Unable to decrypt message +
+
+`; + +exports[`DecryptionFailureBodyView > Should display with extra class names 1`] = ` +
+
+ Unable to decrypt message +
+
+`; + +exports[`DecryptionFailureBodyView > should handle historical messages with no key backup and device verification is false 1`] = ` +
+
+ Historical messages are not available on this device +
+
+`; + +exports[`DecryptionFailureBodyView > should handle historical messages with no key backup and device verification is true 1`] = ` +
+
+ Historical messages are not available on this device +
+
+`; + +exports[`DecryptionFailureBodyView > should handle messages from unverified devices and device verification is false 1`] = ` +
+
+ + + + + Sent from an insecure device. + +
+
+`; + +exports[`DecryptionFailureBodyView > should handle messages from unverified devices and device verification is true 1`] = ` +
+
+ + + + + Sent from an insecure device. + +
+
+`; + +exports[`DecryptionFailureBodyView > should handle messages from users who change identities after verification and device verification is false 1`] = ` +
+
+ + + + + Sender's verified identity was reset + +
+
+`; + +exports[`DecryptionFailureBodyView > should handle messages from users who change identities after verification and device verification is true 1`] = ` +
+
+ + + + + Sender's verified identity was reset + +
+
+`; + +exports[`DecryptionFailureBodyView > should handle undecryptable pre-join messages and device verification is false 1`] = ` +
+
+ You don't have access to this message +
+
+`; + +exports[`DecryptionFailureBodyView > should handle undecryptable pre-join messages and device verification is true 1`] = ` +
+
+ You don't have access to this message +
+
+`; diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/index.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/index.tsx new file mode 100644 index 0000000000..bc533a89fd --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/index.tsx @@ -0,0 +1,13 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { + DecryptionFailureBodyView, + DecryptionFailureReason, + type DecryptionFailureBodyViewModel, + type DecryptionFailureBodyViewSnapshot, +} from "./DecryptionFailureBodyView"; diff --git a/packages/shared-components/src/message-body/MediaBody/MediaBody.test.tsx b/packages/shared-components/src/message-body/MediaBody/MediaBody.test.tsx index 9d405e1af3..303c7a05f0 100644 --- a/packages/shared-components/src/message-body/MediaBody/MediaBody.test.tsx +++ b/packages/shared-components/src/message-body/MediaBody/MediaBody.test.tsx @@ -6,8 +6,9 @@ */ import { composeStories } from "@storybook/react-vite"; -import { render } from "jest-matrix-react"; +import { render } from "@test-utils"; import React from "react"; +import { describe, it, expect } from "vitest"; import * as stories from "./MediaBody.stories"; diff --git a/packages/shared-components/src/message-body/MediaBody/__snapshots__/MediaBody.test.tsx.snap b/packages/shared-components/src/message-body/MediaBody/__snapshots__/MediaBody.test.tsx.snap index d86c677390..34027a651b 100644 --- a/packages/shared-components/src/message-body/MediaBody/__snapshots__/MediaBody.test.tsx.snap +++ b/packages/shared-components/src/message-body/MediaBody/__snapshots__/MediaBody.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`MediaBody renders the media body 1`] = ` +exports[`MediaBody > renders the media body 1`] = `
{ + const vm = useMockedViewModel(snapshotProps, {}); + return {children}; +}; + +export default { + title: "MessageBody/ReactionsRowButtonTooltip", + component: ReactionsRowButtonTooltipViewWrapper, + tags: ["autodocs"], + argTypes: { + formattedSenders: { control: "text" }, + caption: { control: "text" }, + }, + args: { + children: , + }, +} as Meta; + +const Template: StoryFn = (args) => ( + +); + +export const Default = Template.bind({}); +Default.args = { + formattedSenders: "Alice, Bob and Charlie", + caption: ":thumbsup:", + tooltipOpen: true, +}; + +export const ManySenders = Template.bind({}); +ManySenders.args = { + formattedSenders: "Alice, Bob, Charlie, David, Eve, Frank and 2 others", + caption: ":heart:", + children: , + tooltipOpen: true, +}; + +export const WithoutCaption = Template.bind({}); +WithoutCaption.args = { + formattedSenders: "Alice and Bob", + caption: undefined, + children: , + tooltipOpen: true, +}; + +export const NoTooltip = Template.bind({}); +NoTooltip.args = { + formattedSenders: undefined, + caption: undefined, + children: , +}; diff --git a/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.test.tsx b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.test.tsx new file mode 100644 index 0000000000..4c48d0d789 --- /dev/null +++ b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { composeStories } from "@storybook/react-vite"; +import { render } from "@test-utils"; +import React from "react"; +import { describe, it, expect } from "vitest"; + +import * as stories from "./ReactionsRowButtonTooltip.stories"; + +const { Default, ManySenders } = composeStories(stories); + +describe("ReactionsRowButtonTooltip", () => { + it("renders the tooltip with formatted senders and caption", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the tooltip with many senders", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltipView.tsx b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltipView.tsx new file mode 100644 index 0000000000..b8eb50f03c --- /dev/null +++ b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltipView.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type PropsWithChildren, type JSX } from "react"; +import React from "react"; +import { Tooltip } from "@vector-im/compound-web"; + +import { type ViewModel, useViewModel } from "../../viewmodel"; + +/** + * Snapshot interface for the ReactionsRowButtonTooltip view. + */ +export interface ReactionsRowButtonTooltipViewSnapshot { + /** + * The formatted list of sender names who reacted. + */ + formattedSenders?: string; + /** + * The caption to display (e.g., the shortcode of the reaction). + */ + caption?: string; + /** + * Whether the tooltip should be forced open. + */ + tooltipOpen?: boolean; +} + +export type ReactionsRowButtonTooltipViewModel = ViewModel; + +interface ReactionsRowButtonTooltipViewProps { + /** + * The view model for the reactions row button tooltip. + */ + vm: ReactionsRowButtonTooltipViewModel; + /** + * The children to wrap with the tooltip. + */ + children?: PropsWithChildren["children"]; +} + +/** + * Type alias for the ReactionsRowButtonTooltip view model. + */ +export function ReactionsRowButtonTooltipView({ + vm, + children, +}: Readonly): JSX.Element { + const { formattedSenders, caption, tooltipOpen } = useViewModel(vm); + + if (formattedSenders) { + return ( + + {children} + + ); + } + + return <>{children}; +} diff --git a/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/__snapshots__/ReactionsRowButtonTooltip.test.tsx.snap b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/__snapshots__/ReactionsRowButtonTooltip.test.tsx.snap new file mode 100644 index 0000000000..4940b975dd --- /dev/null +++ b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/__snapshots__/ReactionsRowButtonTooltip.test.tsx.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ReactionsRowButtonTooltip > renders the tooltip with formatted senders and caption 1`] = ` +
+ +
+`; + +exports[`ReactionsRowButtonTooltip > renders the tooltip with many senders 1`] = ` +
+ +
+`; diff --git a/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/index.tsx b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/index.tsx new file mode 100644 index 0000000000..92a8a8d611 --- /dev/null +++ b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/index.tsx @@ -0,0 +1,12 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { + ReactionsRowButtonTooltipView, + type ReactionsRowButtonTooltipViewSnapshot, + type ReactionsRowButtonTooltipViewModel, +} from "./ReactionsRowButtonTooltipView"; diff --git a/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.module.css b/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.module.css new file mode 100644 index 0000000000..fea4615390 --- /dev/null +++ b/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.module.css @@ -0,0 +1,21 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.timelineSeparator { + clear: both; + margin: var(--cpd-space-1x) 0; + font: var(--cpd-font-body-md-regular); + letter-spacing: var(--cpd-font-letter-spacing-body-md); + color: var(--cpd-color-text-primary); +} + +.timelineSeparator > hr { + flex: 1 1 0; + height: 0; + border: none; + border-bottom: 1px solid var(--cpd-color-gray-400); +} diff --git a/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.stories.tsx b/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.stories.tsx new file mode 100644 index 0000000000..cf067f4ecd --- /dev/null +++ b/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.stories.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; + +import type { Meta, StoryFn } from "@storybook/react-vite"; +import TimelineSeparator from "./TimelineSeparator"; +import styles from "./TimelineSeparator.module.css"; + +export default { + title: "MessageBody/TimelineSeparator", + component: TimelineSeparator, + tags: ["autodocs"], + args: { + label: "Label Separator", + children: "Timeline Separator", + }, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); + +export const WithHtmlChild = Template.bind({}); +WithHtmlChild.args = { + label: "Custom Label", + children: ( + + ), +}; + +export const WithDateEvent = Template.bind({}); +WithDateEvent.args = { + label: "Date Event Separator", + children: "Wednesday", +}; + +export const WithLateEvent = Template.bind({}); +WithLateEvent.args = { + label: "Late Event Separator", + children: "Fri, Jan 9, 2026", +}; + +export const WithoutChildren = Template.bind({}); +WithoutChildren.args = { + children: undefined, + label: "Separator without children", +}; diff --git a/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.test.tsx b/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.test.tsx new file mode 100644 index 0000000000..859142b49c --- /dev/null +++ b/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { render } from "@test-utils"; +import { composeStories } from "@storybook/react-vite"; +import React from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import * as stories from "./TimelineSeparator.stories.tsx"; + +const { Default, WithHtmlChild, WithoutChildren, WithDateEvent, WithLateEvent } = composeStories(stories); + +describe("TimelineSeparator", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("Snapshot tests", () => { + it("renders the timeline separator in default state", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the timeline separator with HTML child", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the timeline separator with date event", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the timeline separator with late event", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders the timeline separator without children", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.tsx b/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.tsx new file mode 100644 index 0000000000..cd3af1c1a7 --- /dev/null +++ b/packages/shared-components/src/message-body/TimelineSeparator/TimelineSeparator.tsx @@ -0,0 +1,54 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { type PropsWithChildren } from "react"; +import classNames from "classnames"; + +import styles from "./TimelineSeparator.module.css"; +import { Flex } from "../.."; + +/** + * Timeline separator props + */ +export interface TimelineSeparatorProps { + /** + * Accessible label for the separator (for example: "Today", "Yesterday", or a date). + */ + label: string; + /** + * The CSS class name. + */ + className?: string; + /** + * Optional children to render inside the timeline separator + */ + children?: PropsWithChildren["children"]; +} + +/** + * Generic timeline separator component to render within a MessagePanel + * + * @param label the accessible label string describing the separator + * @param children the children to draw within the timeline separator + */ +const TimelineSeparator: React.FC = ({ label, className, children }) => { + // ARIA treats
s as separators, here we abuse them slightly so manually treat this entire thing as one + return ( + +
+ {children} +
+
+ ); +}; + +export default TimelineSeparator; diff --git a/packages/shared-components/src/message-body/TimelineSeparator/__snapshots__/TimelineSeparator.test.tsx.snap b/packages/shared-components/src/message-body/TimelineSeparator/__snapshots__/TimelineSeparator.test.tsx.snap new file mode 100644 index 0000000000..e45501e14d --- /dev/null +++ b/packages/shared-components/src/message-body/TimelineSeparator/__snapshots__/TimelineSeparator.test.tsx.snap @@ -0,0 +1,100 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`TimelineSeparator > Snapshot tests > renders the timeline separator in default state 1`] = ` +
+ +
+`; + +exports[`TimelineSeparator > Snapshot tests > renders the timeline separator with HTML child 1`] = ` +
+ +
+`; + +exports[`TimelineSeparator > Snapshot tests > renders the timeline separator with date event 1`] = ` +
+ +
+`; + +exports[`TimelineSeparator > Snapshot tests > renders the timeline separator with late event 1`] = ` +
+ +
+`; + +exports[`TimelineSeparator > Snapshot tests > renders the timeline separator without children 1`] = ` +
+ +
+`; diff --git a/packages/shared-components/src/message-body/TimelineSeparator/index.ts b/packages/shared-components/src/message-body/TimelineSeparator/index.ts new file mode 100644 index 0000000000..c5812abb07 --- /dev/null +++ b/packages/shared-components/src/message-body/TimelineSeparator/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { default as TimelineSeparator, type TimelineSeparatorProps } from "./TimelineSeparator"; diff --git a/packages/shared-components/src/pill-input/Pill/Pill.test.tsx b/packages/shared-components/src/pill-input/Pill/Pill.test.tsx index a539f6c295..867af1a9d4 100644 --- a/packages/shared-components/src/pill-input/Pill/Pill.test.tsx +++ b/packages/shared-components/src/pill-input/Pill/Pill.test.tsx @@ -6,8 +6,9 @@ */ import { composeStories } from "@storybook/react-vite"; -import { render } from "jest-matrix-react"; +import { render } from "@test-utils"; import React from "react"; +import { describe, it, expect } from "vitest"; import * as stories from "./Pill.stories"; diff --git a/packages/shared-components/src/pill-input/Pill/Pill.tsx b/packages/shared-components/src/pill-input/Pill/Pill.tsx index 64fdac3d2d..2b04fcd54c 100644 --- a/packages/shared-components/src/pill-input/Pill/Pill.tsx +++ b/packages/shared-components/src/pill-input/Pill/Pill.tsx @@ -61,7 +61,7 @@ export function Pill({ className, children, label, onClick, ...props }: PropsWit aria-label={_t("action|delete")} className="mx_Dialog_nonDialogButton" > - + )} diff --git a/packages/shared-components/src/pill-input/Pill/__snapshots__/Pill.test.tsx.snap b/packages/shared-components/src/pill-input/Pill/__snapshots__/Pill.test.tsx.snap index 2ef6575b20..0574e9ba39 100644 --- a/packages/shared-components/src/pill-input/Pill/__snapshots__/Pill.test.tsx.snap +++ b/packages/shared-components/src/pill-input/Pill/__snapshots__/Pill.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Pill renders the pill 1`] = ` +exports[`Pill > renders the pill 1`] = `
`; -exports[`Pill renders the pill without close button 1`] = ` +exports[`Pill > renders the pill without close button 1`] = `
{ it("calls onRemoveChildren when backspace is pressed and input is empty", async () => { const user = userEvent.setup(); - const mockOnRemoveChildren = jest.fn(); + const mockOnRemoveChildren = vi.fn(); render(); diff --git a/packages/shared-components/src/pill-input/PillInput/__snapshots__/PillInput.test.tsx.snap b/packages/shared-components/src/pill-input/PillInput/__snapshots__/PillInput.test.tsx.snap index 37a1d08c8b..64f85adc8b 100644 --- a/packages/shared-components/src/pill-input/PillInput/__snapshots__/PillInput.test.tsx.snap +++ b/packages/shared-components/src/pill-input/PillInput/__snapshots__/PillInput.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`PillInput renders only the input without children 1`] = ` +exports[`PillInput > renders only the input without children 1`] = `
`; -exports[`PillInput renders the pill input 1`] = ` +exports[`PillInput > renders the pill input 1`] = `
{ beforeAll(() => { - jest.useFakeTimers().setSystemTime(new Date("2025-08-01T12:00:00Z")); + vi.useFakeTimers().setSystemTime(new Date("2025-08-01T12:00:00Z")); }); it("renders the item in default state", () => { diff --git a/packages/shared-components/src/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap b/packages/shared-components/src/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap index 6ccc190d8c..b66ad0b495 100644 --- a/packages/shared-components/src/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap +++ b/packages/shared-components/src/rich-list/RichItem/__snapshots__/RichItem.test.tsx.snap @@ -1,10 +1,10 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`RichItem renders the item in default state 1`] = ` +exports[`RichItem > renders the item in default state 1`] = `
  • `; -exports[`RichItem renders the item in selected state 1`] = ` +exports[`RichItem > renders the item in selected state 1`] = `
    • `; -exports[`RichItem renders the item without timestamp 1`] = ` +exports[`RichItem > renders the item without timestamp 1`] = `
      • renders the list 1`] = `
        `; -exports[`RichItem renders the list with isEmpty=true 1`] = ` +exports[`RichItem > renders the list with isEmpty=true 1`] = `
        { + const vm = useMockedViewModel(rest, { + onStreamAudioClick, + onEditClick, + onSnapshotClick, + onDeleteClick, + onRevokeClick, + onFinished, + onMoveButton, + }); + return ; +}; + +export default { + title: "RightPanel/WidgetContextMenuView", + component: WidgetContextMenuViewWrapper, + tags: ["autodocs"], + args: { + showStreamAudioStreamButton: true, + showEditButton: true, + showRevokeButton: true, + showDeleteButton: true, + showSnapshotButton: true, + showMoveButtons: [true, true], + canModify: true, + widgetMessaging: undefined, + isMenuOpened: true, + trigger: ( + + + + ), + onStreamAudioClick: fn(), + onEditClick: fn(), + onSnapshotClick: fn(), + onDeleteClick: fn(), + onRevokeClick: fn(), + onFinished: fn(), + onMoveButton: fn(), + }, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); + +export const OnlyBasicModification = Template.bind({}); +OnlyBasicModification.args = { + showSnapshotButton: false, + showMoveButtons: [false, false], + showStreamAudioStreamButton: false, + showEditButton: false, +}; diff --git a/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.test.tsx b/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.test.tsx new file mode 100644 index 0000000000..e590e8d8d3 --- /dev/null +++ b/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { screen, render } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { IconButton } from "@vector-im/compound-web"; +import { composeStories } from "@storybook/react-vite"; +import TriggerIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal"; +import { describe, vi, expect, it, afterEach } from "vitest"; + +import { + type WidgetContextMenuAction, + type WidgetContextMenuSnapshot, + WidgetContextMenuView, +} from "./WidgetContextMenuView"; +import * as stories from "./WidgetContextMenuView.stories.tsx"; +import { MockViewModel } from "../../viewmodel/MockViewModel.ts"; +import { I18nApi } from "../../utils/I18nApi.ts"; +import { I18nContext } from "../../utils/i18nContext.ts"; + +const { Default, OnlyBasicModification } = composeStories(stories); + +describe("", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("renders widget contextmenu with all options", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders widget contextmenu without only basic modification", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + const onKeyDown = vi.fn(); + const togglePlay = vi.fn(); + const onSeekbarChange = vi.fn(); + + const onStreamAudioClick = vi.fn(); + const onEditClick = vi.fn(); + const onSnapshotClick = vi.fn(); + const onDeleteClick = vi.fn(); + const onRevokeClick = vi.fn(); + const onFinished = vi.fn(); + const onMoveButton = vi.fn(); + class WidgetContextMenuViewModel + extends MockViewModel + implements WidgetContextMenuAction + { + public onKeyDown = onKeyDown; + public togglePlay = togglePlay; + public onSeekbarChange = onSeekbarChange; + + public onStreamAudioClick = onStreamAudioClick; + public onEditClick = onEditClick; + public onSnapshotClick = onSnapshotClick; + public onDeleteClick = onDeleteClick; + public onRevokeClick = onRevokeClick; + public onFinished = onFinished; + public onMoveButton = onMoveButton; + } + + const defaultValue: WidgetContextMenuSnapshot = { + showStreamAudioStreamButton: true, + showEditButton: true, + showRevokeButton: true, + showDeleteButton: true, + showSnapshotButton: true, + showMoveButtons: [true, true], + canModify: true, + isMenuOpened: true, + userWidget: false, + trigger: ( + + + + ), + }; + + it("should attach vm methods", async () => { + const vm = new WidgetContextMenuViewModel(defaultValue); + + render(, { + wrapper: ({ children }) => {children}, + }); + + await userEvent.click(screen.getByRole("menuitem", { name: "Start audio stream" })); + expect(onStreamAudioClick).toHaveBeenCalled(); + + await userEvent.click(screen.getByRole("menuitem", { name: "Edit" })); + expect(onEditClick).toHaveBeenCalled(); + + await userEvent.click(screen.getByRole("menuitem", { name: "Take a picture" })); + expect(onSnapshotClick).toHaveBeenCalled(); + + await userEvent.click(screen.getByRole("menuitem", { name: "Revoke permissions" })); + expect(onRevokeClick).toHaveBeenCalled(); + + await userEvent.click(screen.getByRole("menuitem", { name: "Remove for everyone" })); + expect(onDeleteClick).toHaveBeenCalled(); + + await userEvent.click(screen.getByRole("menuitem", { name: "Move left" })); + expect(onMoveButton).toHaveBeenCalledWith(-1); + + await userEvent.click(screen.getByRole("menuitem", { name: "Move right" })); + expect(onMoveButton).toHaveBeenCalledWith(1); + }); +}); diff --git a/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.tsx b/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.tsx new file mode 100644 index 0000000000..c8a150e041 --- /dev/null +++ b/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.tsx @@ -0,0 +1,197 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type ReactNode, type JSX } from "react"; +import { IconButton, Menu, MenuItem } from "@vector-im/compound-web"; +import TriggerIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal"; + +import { type ViewModel } from "../../viewmodel/ViewModel.ts"; +import { useI18n } from "../../utils/i18nContext.ts"; +import { useViewModel } from "../../viewmodel/useViewModel.ts"; + +export interface WidgetContextMenuSnapshot { + /** + * Indicates if the audio stream button needs to be shown or not + * depending on the config value audio_stream_url and widget type jitsi + */ + showStreamAudioStreamButton: boolean; + /** + * Indicates if the edit button is shown depending the user permission to modify + */ + showEditButton: boolean; + /** + * Indicates if revoke widget button needs to be shown or not + */ + showRevokeButton: boolean; + /** + * Indicates if delete widget button needs to be shown or not + */ + showDeleteButton: boolean; + /** + * Show take screenshot button or not dependning on config value enableWidgetScreenshots + */ + showSnapshotButton: boolean; + /** + * show move widget position button + */ + showMoveButtons: [boolean, boolean]; + /** + * Indicates if user can modify the widget settings + */ + canModify: boolean; + /** + * Indicates if the widget menu is opened or not + */ + isMenuOpened: boolean; + /** + * A component that is displayed which trigger the menu to open or close + */ + trigger: ReactNode; + /** + * If it's an instance of a user widget + */ + userWidget: boolean; +} + +export interface WidgetContextMenuAction { + /** + * Function triggered when stream audio is clicked + */ + onStreamAudioClick: () => Promise; + /** + * Function triggered when edit button is clicked + */ + onEditClick: () => void; + /** + * Function triggered when snapshot button is clicked + */ + onSnapshotClick: () => void; + /** + * Function triggered when delete button is clicked + */ + onDeleteClick: () => void; + /** + * Function triggered when revoke button is clicked + */ + onRevokeClick: () => void; + /** + * Called when the action is finished, to close the menu + */ + onFinished: () => void; + /** + * Button used to move up or down in the list the widget position + * @param direction 1 or -1 + */ + onMoveButton: (direction: number) => void; +} + +export type WidgetContextMenuViewModel = ViewModel & WidgetContextMenuAction; + +interface WidgetContextMenuViewProps { + vm: WidgetContextMenuViewModel; +} + +/** + * A context menu component used to display the correct items that needs to be displayed for a widget item menu + */ +export const WidgetContextMenuView: React.FC = ({ vm }) => { + const { translate: _t } = useI18n(); + + const { + showStreamAudioStreamButton, + showEditButton, + showSnapshotButton, + showDeleteButton, + showRevokeButton, + showMoveButtons, + isMenuOpened, + userWidget, + trigger, + } = useViewModel(vm); + + let streamAudioStreamButton: JSX.Element | undefined; + if (showStreamAudioStreamButton) { + streamAudioStreamButton = ( + + ); + } + + let editButton: JSX.Element | undefined; + if (showEditButton) { + editButton = ; + } + + let snapshotButton: JSX.Element | undefined; + if (showSnapshotButton) { + snapshotButton = ; + } + + let deleteButton: JSX.Element | undefined; + if (showDeleteButton) { + deleteButton = ( + + ); + } + + let revokeButton: JSX.Element | undefined; + if (showRevokeButton) { + revokeButton = ; + } + + const [showMoveLeftButton, showMoveRightButton] = showMoveButtons; + let moveLeftButton: JSX.Element | undefined; + if (showMoveLeftButton) { + moveLeftButton = vm.onMoveButton(-1)} label={_t("widget|context_menu|move_left")} />; + } + + let moveRightButton: JSX.Element | undefined; + if (showMoveRightButton) { + moveRightButton = vm.onMoveButton(1)} label={_t("widget|context_menu|move_right")} />; + } + + // Only render menu items when the menu is open to prevent focusable elements in aria-hidden container + const renderMenuItems = (): React.ReactNode => { + if (!isMenuOpened) return null; + return ( + <> + {streamAudioStreamButton} + {editButton} + {revokeButton} + {deleteButton} + {snapshotButton} + {moveLeftButton} + {moveRightButton} + + ); + }; + + // Default trigger icon if no valid trigger element was passed + const wrappedTrigger = React.isValidElement(trigger) ? ( + trigger + ) : ( + + + + ); + + return ( + + {renderMenuItems()} + + ); +}; diff --git a/packages/shared-components/src/right-panel/WidgetContextMenu/__snapshots__/WidgetContextMenuView.test.tsx.snap b/packages/shared-components/src/right-panel/WidgetContextMenu/__snapshots__/WidgetContextMenuView.test.tsx.snap new file mode 100644 index 0000000000..f60036c561 --- /dev/null +++ b/packages/shared-components/src/right-panel/WidgetContextMenu/__snapshots__/WidgetContextMenuView.test.tsx.snap @@ -0,0 +1,83 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders widget contextmenu with all options 1`] = ` + +`; + +exports[` > renders widget contextmenu without only basic modification 1`] = ` + +`; diff --git a/packages/shared-components/src/right-panel/WidgetContextMenu/index.ts b/packages/shared-components/src/right-panel/WidgetContextMenu/index.ts new file mode 100644 index 0000000000..fadbd317e5 --- /dev/null +++ b/packages/shared-components/src/right-panel/WidgetContextMenu/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export type { WidgetContextMenuSnapshot, WidgetContextMenuViewModel } from "./WidgetContextMenuView"; +export { WidgetContextMenuView } from "./WidgetContextMenuView"; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.module.css b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.module.css new file mode 100644 index 0000000000..0ed647c777 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.module.css @@ -0,0 +1,23 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.header { + flex: 0 0 60px; + padding: 0 var(--cpd-space-3x); +} + +.title { + min-width: 0; + + h1 { + /* Remove default h1 margin */ + margin: unset; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } +} diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx new file mode 100644 index 0000000000..bca9e709d2 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx @@ -0,0 +1,84 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryFn } from "@storybook/react-vite"; +import { + RoomListHeaderView, + type RoomListHeaderViewActions, + type RoomListHeaderViewSnapshot, +} from "./RoomListHeaderView"; +import { useMockedViewModel } from "../../viewmodel"; +import { defaultSnapshot } from "./default-snapshot"; + +type RoomListHeaderProps = RoomListHeaderViewSnapshot & RoomListHeaderViewActions; + +const RoomListHeaderViewWrapper = ({ + createChatRoom, + createRoom, + createVideoRoom, + openSpaceHome, + openSpaceSettings, + inviteInSpace, + openSpacePreferences, + sort, + toggleMessagePreview, + ...rest +}: RoomListHeaderProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + createChatRoom, + createRoom, + createVideoRoom, + openSpaceHome, + openSpaceSettings, + inviteInSpace, + sort, + openSpacePreferences, + toggleMessagePreview, + }); + return ; +}; + +export default { + title: "Room List/RoomListHeaderView", + component: RoomListHeaderViewWrapper, + tags: ["autodocs"], + args: { + ...defaultSnapshot, + createChatRoom: fn(), + createRoom: fn(), + createVideoRoom: fn(), + openSpaceHome: fn(), + openSpaceSettings: fn(), + inviteInSpace: fn(), + sort: fn(), + openSpacePreferences: fn(), + toggleMessagePreview: fn(), + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=2925-19173", + }, + }, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); + +export const NoSpaceMenu = Template.bind({}); +NoSpaceMenu.args = { + displaySpaceMenu: false, +}; + +export const NoComposeMenu = Template.bind({}); +NoComposeMenu.args = { + displayComposeMenu: false, +}; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.test.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.test.tsx new file mode 100644 index 0000000000..48904171cd --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { composeStories } from "@storybook/react-vite"; +import { render } from "@test-utils"; +import { describe, it, expect } from "vitest"; +import React from "react"; + +import * as stories from "./RoomListHeaderView.stories"; + +const { Default, NoComposeMenu, NoSpaceMenu } = composeStories(stories); + +describe("RoomListHeaderView", () => { + it("renders the default state", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders without compose menu", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders without space menu", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx new file mode 100644 index 0000000000..5b75de23b5 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx @@ -0,0 +1,160 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { IconButton, H1 } from "@vector-im/compound-web"; +import ComposeIcon from "@vector-im/compound-design-tokens/assets/web/icons/compose"; + +import { type ViewModel, useViewModel } from "../../viewmodel"; +import { Flex } from "../../utils/Flex"; +import { useI18n } from "../../utils/i18nContext"; +import { ComposeMenuView, OptionMenuView, SpaceMenuView } from "./menu"; +import styles from "./RoomListHeaderView.module.css"; + +/** + * The available sorting options for the room list. + */ +export type SortOption = "recent" | "alphabetical" | "unread-first"; + +export interface RoomListHeaderViewSnapshot { + /** + * The title of the room list + */ + title: string; + /** + * Whether to display the compose menu + * True if the user can create rooms + */ + displayComposeMenu: boolean; + /** + * Whether to display the space menu + * True if there is an active space + */ + displaySpaceMenu: boolean; + /** + * Whether the user can create rooms + */ + canCreateRoom: boolean; + /** + * Whether the user can create video rooms + */ + canCreateVideoRoom: boolean; + /** + * Whether the user can invite in the active space + */ + canInviteInSpace: boolean; + /** + * Whether the user can access space settings + */ + canAccessSpaceSettings: boolean; + /** + * The currently active sort option. + */ + activeSortOption: SortOption; + /** + * Whether message previews are enabled in the room list. + */ + isMessagePreviewEnabled: boolean; +} + +export interface RoomListHeaderViewActions { + /** + * Create a chat room + */ + createChatRoom: (e: Event) => void; + /** + * Create a room + */ + createRoom: (e: Event) => void; + /** + * Create a video room + */ + createVideoRoom: () => void; + /** + * Open the active space home + */ + openSpaceHome: () => void; + /** + * Display the space invite dialog + */ + inviteInSpace: () => void; + /** + * Open the space preferences + */ + openSpacePreferences: () => void; + /** + * Open the space settings + */ + openSpaceSettings: () => void; + /** + * Change the sort order of the room-list. + */ + sort: (option: SortOption) => void; + /** + * Toggle message preview display in the room list. + */ + toggleMessagePreview: () => void; +} + +/** + * The view model for the room list header component. + */ +export type RoomListHeaderViewModel = ViewModel & RoomListHeaderViewActions; + +interface RoomListHeaderViewProps { + /** + * The view model for the room list header component. + */ + vm: RoomListHeaderViewModel; +} + +/** + * The header view for the room list + * The space name is displayed and a compose menu is shown if the user can create rooms + * + * @example + * ```tsx + * + * ``` + */ +export function RoomListHeaderView({ vm }: Readonly): JSX.Element { + const { translate: _t } = useI18n(); + const { title, displaySpaceMenu, displayComposeMenu } = useViewModel(vm); + + return ( + + +

        + {title} +

        + {displaySpaceMenu && } +
        + + + + {/* If we don't display the compose menu, it means that the user can only send DM */} + {displayComposeMenu ? ( + + ) : ( + vm.createChatRoom(e.nativeEvent)} + tooltip={_t("action|new_conversation")} + > + + + )} + +
        + ); +} diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/__snapshots__/RoomListHeaderView.test.tsx.snap b/packages/shared-components/src/room-list/RoomListHeaderView/__snapshots__/RoomListHeaderView.test.tsx.snap new file mode 100644 index 0000000000..bd1cb09e56 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/__snapshots__/RoomListHeaderView.test.tsx.snap @@ -0,0 +1,349 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`RoomListHeaderView > renders the default state 1`] = ` +
        +
        +
        +

        + Rooms +

        + +
        +
        + + +
        +
        +
        +`; + +exports[`RoomListHeaderView > renders without compose menu 1`] = ` +
        +
        +
        +

        + Rooms +

        + +
        +
        + + +
        +
        +
        +`; + +exports[`RoomListHeaderView > renders without space menu 1`] = ` +
        +
        +
        +

        + Rooms +

        +
        +
        + + +
        +
        +
        +`; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts b/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts new file mode 100644 index 0000000000..ab02dbfe76 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type RoomListHeaderViewSnapshot } from "./RoomListHeaderView"; + +export const defaultSnapshot: RoomListHeaderViewSnapshot = { + title: "Rooms", + displayComposeMenu: true, + displaySpaceMenu: true, + canCreateRoom: true, + canCreateVideoRoom: true, + canInviteInSpace: true, + canAccessSpaceSettings: true, + activeSortOption: "recent", + isMessagePreviewEnabled: true, +}; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/index.ts b/packages/shared-components/src/room-list/RoomListHeaderView/index.ts new file mode 100644 index 0000000000..a0b6edee11 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export type { + RoomListHeaderViewModel, + RoomListHeaderViewSnapshot, + RoomListHeaderViewActions, + SortOption, +} from "./RoomListHeaderView"; +export { RoomListHeaderView } from "./RoomListHeaderView"; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.test.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.test.tsx new file mode 100644 index 0000000000..db06051cc2 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { vi, describe, it, afterEach, expect } from "vitest"; + +import { ComposeMenuView } from "./ComposeMenuView"; +import { defaultSnapshot, MockedViewModel } from "../test-utils"; + +describe("", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should match snapshot", () => { + const vm = new MockedViewModel(defaultSnapshot); + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("should display all menu options when fully enabled", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + // Open the menu + const button = screen.getByRole("button", { name: "New conversation" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Start chat" })).toBeInTheDocument(); + expect(screen.getByRole("menuitem", { name: "New room" })).toBeInTheDocument(); + expect(screen.getByRole("menuitem", { name: "New video room" })).toBeInTheDocument(); + }); + + it("should hide new room option when canCreateRoom is false", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, canCreateRoom: false }); + render(); + + const button = screen.getByRole("button", { name: "New conversation" }); + await user.click(button); + + expect(screen.queryByRole("menuitem", { name: "New room" })).not.toBeInTheDocument(); + expect(screen.getByRole("menuitem", { name: "Start chat" })).toBeInTheDocument(); + }); + + it("should hide video room option when canCreateVideoRoom is false", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, canCreateVideoRoom: false }); + render(); + + const button = screen.getByRole("button", { name: "New conversation" }); + await user.click(button); + + expect(screen.queryByRole("menuitem", { name: "New video room" })).not.toBeInTheDocument(); + expect(screen.getByRole("menuitem", { name: "Start chat" })).toBeInTheDocument(); + }); + + it("should call createChatRoom when Start chat is clicked", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + await user.click(screen.getByRole("button", { name: "New conversation" })); + await user.click(screen.getByRole("menuitem", { name: "Start chat" })); + + expect(vm.createChatRoom).toHaveBeenCalledTimes(1); + }); + + it("should call createRoom when New room is clicked", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + await user.click(screen.getByRole("button", { name: "New conversation" })); + await user.click(screen.getByRole("menuitem", { name: "New room" })); + + expect(vm.createRoom).toHaveBeenCalledTimes(1); + }); + + it("should call createVideoRoom when New video room is clicked", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + await user.click(screen.getByRole("button", { name: "New conversation" })); + await user.click(screen.getByRole("menuitem", { name: "New video room" })); + + expect(vm.createVideoRoom).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx new file mode 100644 index 0000000000..1da96cf3e4 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { useState, type JSX } from "react"; +import { IconButton, Menu, MenuItem } from "@vector-im/compound-web"; +import ComposeIcon from "@vector-im/compound-design-tokens/assets/web/icons/compose"; +import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call"; +import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat"; +import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room"; + +import { type RoomListHeaderViewModel } from "../RoomListHeaderView"; +import { useI18n } from "../../../utils/i18nContext"; +import { useViewModel } from "../../../viewmodel"; + +interface ComposeMenuViewProps { + /** + * The view model for the room list header + */ + vm: RoomListHeaderViewModel; +} + +/** + * A menu component that provides options for creating new conversations. + * Displays a dropdown menu with options to start a chat, create a room, or create a video room. + * + * @example + * ```tsx + * + * ``` + */ +export function ComposeMenuView({ vm }: ComposeMenuViewProps): JSX.Element { + const { translate: _t } = useI18n(); + const [open, setOpen] = useState(false); + const { canCreateRoom, canCreateVideoRoom } = useViewModel(vm); + + return ( + + + + } + > + + {canCreateRoom && ( + + )} + {canCreateVideoRoom && ( + + )} + + ); +} diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.module.css b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.module.css new file mode 100644 index 0000000000..11a81da947 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.module.css @@ -0,0 +1,11 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.title { + /* For first title, there is already enough space at the top */ + margin-top: 0 !important; +} diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx new file mode 100644 index 0000000000..f961d83109 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.test.tsx @@ -0,0 +1,124 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { vi, describe, it, afterEach, expect } from "vitest"; + +import { OptionMenuView } from "./OptionMenuView"; +import { defaultSnapshot, MockedViewModel } from "../test-utils"; + +describe("", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should match snapshot", () => { + const vm = new MockedViewModel(defaultSnapshot); + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("should show A to Z selected if activeSortOption is alphabetical", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, activeSortOption: "alphabetical" }); + render(); + + // Open the menu + const button = screen.getByRole("button", { name: "Room Options" }); + await user.click(button); + + expect(screen.getByRole("menuitemradio", { name: "A-Z" })).toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Activity" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Unread first" })).not.toBeChecked(); + }); + + it("should show Activity selected if activeSortOption is recent", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, activeSortOption: "recent" }); + render(); + + // Open the menu + const button = screen.getByRole("button", { name: "Room Options" }); + await user.click(button); + + expect(screen.getByRole("menuitemradio", { name: "A-Z" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Unread first" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Activity" })).toBeChecked(); + }); + + it("should show `Unread First` selected if activeSortOption is unread-first", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, activeSortOption: "unread-first" }); + render(); + + // Open the menu + const button = screen.getByRole("button", { name: "Room Options" }); + await user.click(button); + + expect(screen.getByRole("menuitemradio", { name: "A-Z" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Activity" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Unread first" })).toBeChecked(); + }); + + it("should sort A to Z", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + + await user.click(screen.getByRole("menuitemradio", { name: "A-Z" })); + + expect(vm.sort).toHaveBeenCalledWith("alphabetical"); + }); + + it("should sort by activity", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, activeSortOption: "recent" }); + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + + await user.click(screen.getByRole("menuitemradio", { name: "Activity" })); + + expect(vm.sort).toHaveBeenCalledWith("recent"); + }); + + it("should sort by unread first", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, activeSortOption: "recent" }); + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + + await user.click(screen.getByRole("menuitemradio", { name: "Unread first" })); + + expect(vm.sort).toHaveBeenCalledWith("unread-first"); + }); + + it("should toggle message preview", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, isMessagePreviewEnabled: true }); + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + expect(screen.getByRole("menuitemcheckbox", { name: "Show message previews" })).toBeChecked(); + + await user.click(screen.getByRole("menuitemcheckbox", { name: "Show message previews" })); + expect(vm.toggleMessagePreview).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx new file mode 100644 index 0000000000..2447bf05f0 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { CheckboxMenuItem, IconButton, Menu, MenuTitle, RadioMenuItem } from "@vector-im/compound-web"; +import React, { type JSX, useState } from "react"; +import OverflowHorizontalIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal"; + +import { type RoomListHeaderViewModel } from "../RoomListHeaderView"; +import { useViewModel } from "../../../viewmodel"; +import { useI18n } from "../../../utils/i18nContext"; +import styles from "./OptionMenuView.module.css"; + +interface OptionMenuViewProps { + /** + * The view model for the room list header + */ + vm: RoomListHeaderViewModel; +} + +/** + * A menu component that provides sorting options for the room list. + * Displays a dropdown menu with radio buttons to sort rooms by activity or alphabetically. + * + * @example + * ```tsx + * + * ``` + */ +export function OptionMenuView({ vm }: OptionMenuViewProps): JSX.Element { + const { translate: _t } = useI18n(); + const [open, setOpen] = useState(false); + const { activeSortOption, isMessagePreviewEnabled } = useViewModel(vm); + + return ( + + + + } + > + + vm.sort("recent")} + /> + vm.sort("unread-first")} + /> + vm.sort("alphabetical")} + /> + + + + ); +} diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/SpaceMenuView.module.css b/packages/shared-components/src/room-list/RoomListHeaderView/menu/SpaceMenuView.module.css new file mode 100644 index 0000000000..ab29d4bc2a --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/SpaceMenuView.module.css @@ -0,0 +1,18 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.button { + svg { + transition: transform 0.1s linear; + } +} + +.button[aria-expanded="true"] { + svg { + transform: rotate(180deg); + } +} diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/SpaceMenuView.test.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/SpaceMenuView.test.tsx new file mode 100644 index 0000000000..b2fe8adf31 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/SpaceMenuView.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { vi, describe, it, afterEach, expect } from "vitest"; + +import { SpaceMenuView } from "./SpaceMenuView"; +import { defaultSnapshot, MockedViewModel } from "../test-utils"; + +describe("", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should match snapshot", () => { + const vm = new MockedViewModel(defaultSnapshot); + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("should display the menu when button is clicked", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + const button = screen.getByRole("button", { name: "Open space menu" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Space home" })).toBeInTheDocument(); + expect(screen.getByRole("menuitem", { name: "Invite" })).toBeInTheDocument(); + expect(screen.getByRole("menuitem", { name: "Preferences" })).toBeInTheDocument(); + expect(screen.getByRole("menuitem", { name: "Space settings" })).toBeInTheDocument(); + }); + + it("should hide invite option when canInviteInSpace is false", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, canInviteInSpace: false }); + render(); + + const button = screen.getByRole("button", { name: "Open space menu" }); + await user.click(button); + + expect(screen.queryByRole("menuitem", { name: "Invite" })).not.toBeInTheDocument(); + expect(screen.getByRole("menuitem", { name: "Space home" })).toBeInTheDocument(); + }); + + it("should hide space settings option when canAccessSpaceSettings is false", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel({ ...defaultSnapshot, canAccessSpaceSettings: false }); + render(); + + const button = screen.getByRole("button", { name: "Open space menu" }); + await user.click(button); + + expect(screen.queryByRole("menuitem", { name: "Space settings" })).not.toBeInTheDocument(); + expect(screen.getByRole("menuitem", { name: "Space home" })).toBeInTheDocument(); + }); + + it("should call openSpaceHome when Home is clicked", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + await user.click(screen.getByRole("button", { name: "Open space menu" })); + await user.click(screen.getByRole("menuitem", { name: "Space home" })); + + expect(vm.openSpaceHome).toHaveBeenCalledTimes(1); + }); + + it("should call inviteInSpace when Invite is clicked", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + await user.click(screen.getByRole("button", { name: "Open space menu" })); + await user.click(screen.getByRole("menuitem", { name: "Invite" })); + + expect(vm.inviteInSpace).toHaveBeenCalledTimes(1); + }); + + it("should call openSpacePreferences when Preferences is clicked", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + await user.click(screen.getByRole("button", { name: "Open space menu" })); + await user.click(screen.getByRole("menuitem", { name: "Preferences" })); + + expect(vm.openSpacePreferences).toHaveBeenCalledTimes(1); + }); + + it("should call openSpaceSettings when Space settings is clicked", async () => { + const user = userEvent.setup(); + + const vm = new MockedViewModel(defaultSnapshot); + render(); + + await user.click(screen.getByRole("button", { name: "Open space menu" })); + await user.click(screen.getByRole("menuitem", { name: "Space settings" })); + + expect(vm.openSpaceSettings).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/SpaceMenuView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/SpaceMenuView.tsx new file mode 100644 index 0000000000..72d3d2521b --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/SpaceMenuView.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, useState } from "react"; +import { IconButton, Menu, MenuItem } from "@vector-im/compound-web"; +import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; +import HomeIcon from "@vector-im/compound-design-tokens/assets/web/icons/home"; +import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/settings"; +import PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences"; +import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add"; + +import styles from "./SpaceMenuView.module.css"; +import { useViewModel } from "../../../viewmodel"; +import { useI18n } from "../../../utils/i18nContext"; +import { type RoomListHeaderViewModel } from "../RoomListHeaderView"; + +interface SpaceMenuViewProps { + /** + * The view model for the room list header + */ + vm: RoomListHeaderViewModel; +} + +/** + * A menu component that provides space-specific actions. + * Displays a dropdown menu with options to navigate to space home, invite users, + * access preferences, and manage space settings. + * + * @example + * ```tsx + * + * ``` + */ +export function SpaceMenuView({ vm }: SpaceMenuViewProps): JSX.Element { + const { translate: _t } = useI18n(); + const { canInviteInSpace, canAccessSpaceSettings, title } = useViewModel(vm); + const [open, setOpen] = useState(false); + + return ( + + + + } + > + + {canInviteInSpace && ( + + )} + + {canAccessSpaceSettings && ( + + )} + + ); +} diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/__snapshots__/ComposeMenuView.test.tsx.snap b/packages/shared-components/src/room-list/RoomListHeaderView/menu/__snapshots__/ComposeMenuView.test.tsx.snap new file mode 100644 index 0000000000..01097e411a --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/__snapshots__/ComposeMenuView.test.tsx.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > should match snapshot 1`] = ` + + + +`; diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap b/packages/shared-components/src/room-list/RoomListHeaderView/menu/__snapshots__/OptionMenuView.test.tsx.snap similarity index 81% rename from test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap rename to packages/shared-components/src/room-list/RoomListHeaderView/menu/__snapshots__/OptionMenuView.test.tsx.snap index 574fcbd094..3733795dca 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/__snapshots__/OptionMenuView.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[` should match snapshot 1`] = ` +exports[` > should match snapshot 1`] = ` + +`; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/index.ts b/packages/shared-components/src/room-list/RoomListHeaderView/menu/index.ts new file mode 100644 index 0000000000..b3f90d914b --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { OptionMenuView } from "./OptionMenuView"; +export { SpaceMenuView } from "./SpaceMenuView"; +export { ComposeMenuView } from "./ComposeMenuView"; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts b/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts new file mode 100644 index 0000000000..467bfbb16f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { vi } from "vitest"; + +import { MockViewModel } from "../../viewmodel"; +import { type RoomListHeaderViewActions, type RoomListHeaderViewSnapshot } from "./RoomListHeaderView"; + +/** + * A mocked ViewModel for the RoomListHeaderView, for use in tests. + */ +export class MockedViewModel extends MockViewModel implements RoomListHeaderViewActions { + public createChatRoom = vi.fn<() => void>(); + public createRoom = vi.fn<() => void>(); + public createVideoRoom = vi.fn<() => void>(); + public openSpaceHome = vi.fn<() => void>(); + public openSpaceSettings = vi.fn<() => void>(); + public inviteInSpace = vi.fn<() => void>(); + public sort = vi.fn<() => void>(); + public openSpacePreferences = vi.fn<() => void>(); + public toggleMessagePreview = vi.fn<() => void>(); +} + +export { defaultSnapshot } from "./default-snapshot"; diff --git a/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx new file mode 100644 index 0000000000..cca9e5eef3 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.stories.tsx @@ -0,0 +1,126 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { NotificationDecoration, type NotificationDecorationProps } from "./NotificationDecoration"; + +const defaultProps: NotificationDecorationProps = { + hasAnyNotificationOrActivity: false, + isUnsentMessage: false, + invited: false, + isMention: false, + isActivityNotification: false, + isNotification: false, + hasUnreadCount: false, + count: 0, + muted: false, +}; + +const meta = { + title: "Room List/NotificationDecoration", + component: NotificationDecoration, + tags: ["autodocs"], + decorators: [ + (Story) => ( +
        + +
        + ), + ], + args: defaultProps, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=101-13062", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const NoNotification: Story = {}; + +export const UnsentMessage: Story = { + args: { + hasAnyNotificationOrActivity: true, + isUnsentMessage: true, + }, +}; + +export const VideoCall: Story = { + args: { + hasAnyNotificationOrActivity: true, + callType: "video", + }, +}; + +export const VoiceCall: Story = { + args: { + hasAnyNotificationOrActivity: true, + callType: "voice", + }, +}; + +export const Invited: Story = { + args: { + hasAnyNotificationOrActivity: true, + invited: true, + }, +}; + +export const Mention: Story = { + args: { + hasAnyNotificationOrActivity: true, + isMention: true, + }, +}; + +export const MentionWithCount: Story = { + args: { + hasAnyNotificationOrActivity: true, + isMention: true, + count: 5, + }, +}; + +export const NotificationWithCount: Story = { + args: { + hasAnyNotificationOrActivity: true, + isNotification: true, + count: 3, + }, +}; + +export const ActivityIndicator: Story = { + args: { + hasAnyNotificationOrActivity: true, + isActivityNotification: true, + }, +}; + +export const Muted: Story = { + args: { + muted: true, + }, +}; + +export const MutedWithoutActivity: Story = { + args: { + hasAnyNotificationOrActivity: false, + muted: true, + }, +}; + +export const VideoCallWithoutActivity: Story = { + args: { + hasAnyNotificationOrActivity: false, + callType: "video", + }, +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.test.tsx b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.test.tsx new file mode 100644 index 0000000000..f79e092f4d --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render } from "@test-utils"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; + +import * as stories from "./NotificationDecoration.stories"; + +const { + NoNotification, + UnsentMessage, + VideoCall, + VoiceCall, + Invited, + Mention, + MentionWithCount, + NotificationWithCount, + ActivityIndicator, + Muted, +} = composeStories(stories); + +describe("", () => { + describe("snapshots", () => { + it("renders NoNotification story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders UnsentMessage story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders VideoCall story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders VoiceCall story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders Invited story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders Mention story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders MentionWithCount story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NotificationWithCount story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders ActivityIndicator story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders Muted story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.tsx b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.tsx new file mode 100644 index 0000000000..03be962fbf --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/NotificationDecoration.tsx @@ -0,0 +1,90 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { + MentionIcon, + ErrorSolidIcon, + NotificationsOffSolidIcon, + VideoCallSolidIcon, + EmailSolidIcon, + VoiceCallSolidIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; +import { UnreadCounter, Unread } from "@vector-im/compound-web"; + +import { Flex } from "../../../utils/Flex"; + +/** + * Data representing the notification state for a room or item. + * Used in snapshots and passed to the NotificationDecoration component. + */ +export interface NotificationDecorationData { + /** Whether there is any notification or activity to display */ + hasAnyNotificationOrActivity: boolean; + /** Whether there's an unsent message */ + isUnsentMessage: boolean; + /** Whether the user is invited to the room */ + invited: boolean; + /** Whether the notification is a mention */ + isMention: boolean; + /** Whether there's activity (not a full notification) */ + isActivityNotification: boolean; + /** Whether there's a notification (not just activity) */ + isNotification: boolean; + /** Whether there are unread messages with a count */ + hasUnreadCount: boolean; + /** Notification count */ + count: number; + /** Whether notifications are muted */ + muted: boolean; + /** Optional call type indicator */ + callType?: "video" | "voice"; +} + +/** + * Props for the NotificationDecoration component. + */ +export interface NotificationDecorationProps extends NotificationDecorationData {} + +/** + * Renders notification badges and indicators for rooms/items + */ +export const NotificationDecoration: React.FC = ({ + hasAnyNotificationOrActivity, + muted, + callType, + isUnsentMessage, + invited, + isMention, + isNotification, + isActivityNotification, + count, +}) => { + // Don't render anything if there's nothing to show + if (!hasAnyNotificationOrActivity && !muted && !callType) { + return null; + } + + return ( + + {isUnsentMessage && ( + + )} + {callType === "video" && ( + + )} + {callType === "voice" && ( + + )} + {invited && } + {isMention && } + {(isMention || isNotification) && } + {isActivityNotification && } + {muted && } + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/__snapshots__/NotificationDecoration.test.tsx.snap b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/__snapshots__/NotificationDecoration.test.tsx.snap new file mode 100644 index 0000000000..a7c7da94f4 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/__snapshots__/NotificationDecoration.test.tsx.snap @@ -0,0 +1,242 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > snapshots > renders ActivityIndicator story 1`] = ` +
        +
        +
        +
        +
        +
        +
        +
        +
        +`; + +exports[` > snapshots > renders Invited story 1`] = ` +
        +
        +
        + + + +
        +
        +
        +`; + +exports[` > snapshots > renders Mention story 1`] = ` +
        +
        +
        + + + +
        +
        +
        +
        +`; + +exports[` > snapshots > renders MentionWithCount story 1`] = ` +
        +
        +
        + + + + + 5 + +
        +
        +
        +`; + +exports[` > snapshots > renders Muted story 1`] = ` +
        +
        +
        + + + + +
        +
        +
        +`; + +exports[` > snapshots > renders NoNotification story 1`] = ` +
        +
        +
        +`; + +exports[` > snapshots > renders NotificationWithCount story 1`] = ` +
        +
        +
        + + 3 + +
        +
        +
        +`; + +exports[` > snapshots > renders UnsentMessage story 1`] = ` +
        +
        +
        + + + +
        +
        +
        +`; + +exports[` > snapshots > renders VideoCall story 1`] = ` +
        +
        +
        + + + +
        +
        +
        +`; + +exports[` > snapshots > renders VoiceCall story 1`] = ` +
        +
        +
        + + + +
        +
        +
        +`; diff --git a/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/index.tsx b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/index.tsx new file mode 100644 index 0000000000..42c7a3451f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/NotificationDecoration/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { NotificationDecoration } from "./NotificationDecoration"; +export type { NotificationDecorationProps, NotificationDecorationData } from "./NotificationDecoration"; diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItem.module.css b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.module.css new file mode 100644 index 0000000000..008a4462ac --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.module.css @@ -0,0 +1,106 @@ +/* + * Copyright 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +/** + * The RoomListItem has the following structure: + * button--------------------------------------------------| + * | <-12px-> container------------------------------------| + * | | room avatar <-8px-> content----------------| + * | | | room_name <- 20px ->| + * | | | --------------------| <-- border + * |-------------------------------------------------------| + */ +.roomListItem { + /* Remove button default style */ + background: unset; + border: none; + padding: 0; + text-align: unset; + + cursor: pointer; + height: 48px; + width: 100%; + + padding-left: var(--cpd-space-3x); + font: var(--cpd-font-body-md-regular); + color: var(--cpd-color-text-primary); + + /* Hide the menu by default */ + .hoverMenu { + display: none; + } +} + +/* Show hover menu and background on hover/focus/menu-open states */ +.roomListItem:hover, +.roomListItem:focus-visible, +/* When the context menu is opened */ +.roomListItem[data-state="open"], +/* When the options and notifications menu are opened */ +.roomListItem:has(.hoverMenu > button[data-state="open"]) { + background-color: var(--cpd-color-bg-action-secondary-hovered); + + .hoverMenu { + display: flex; + } + + /* When the menu is visible, hide the notification decoration to avoid clutter */ + .notificationDecoration { + display: none; + } + + /** + * The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331 + * the icon size of the menu is 18px instead of 20px with a different internal padding + * We need to use 18px to align the icon with the others icons + * 18px is not available in compound spacing + */ + .content { + padding-right: 18px; + } +} + +.content { + height: 100%; + flex: 1; + /* The border is only under the room name and the future hover menu */ + border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary); + box-sizing: border-box; + min-width: 0; + padding-right: var(--cpd-space-5x); +} + +.text { + min-width: 0; +} + +.roomName { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.messagePreview { + font: var(--cpd-font-body-sm-regular); + color: var(--cpd-color-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.selected { + background-color: var(--cpd-color-bg-action-secondary-pressed); +} + +.bold .roomName { + font: var(--cpd-font-body-md-semibold); +} + +/* Set icon color for hover menu buttons */ +.hoverMenu svg { + fill: var(--cpd-color-icon-primary); +} diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItem.stories.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.stories.tsx new file mode 100644 index 0000000000..fcc4017fb1 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.stories.tsx @@ -0,0 +1,213 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Room } from "./RoomListItem"; +import { RoomListItemView, type RoomListItemSnapshot, type RoomListItemActions } from "./RoomListItem"; +import { useMockedViewModel } from "../../viewmodel"; +import { defaultSnapshot } from "./default-snapshot"; +import { renderAvatar } from "../story-mocks"; + +type RoomListItemProps = RoomListItemSnapshot & + RoomListItemActions & { + isSelected: boolean; + isFocused: boolean; + onFocus: (room: Room, e: React.FocusEvent) => void; + roomIndex: number; + roomCount: number; + renderAvatar: (room: Room) => React.ReactElement; + }; + +// Wrapper component that creates a mocked ViewModel +const RoomListItemWrapper = ({ + onOpenRoom, + onMarkAsRead, + onMarkAsUnread, + onToggleFavorite, + onToggleLowPriority, + onInvite, + onCopyRoomLink, + onLeaveRoom, + onSetRoomNotifState, + isSelected, + isFocused, + onFocus, + roomIndex, + roomCount, + renderAvatar: renderAvatarProp, + ...rest +}: RoomListItemProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + onOpenRoom, + onMarkAsRead, + onMarkAsUnread, + onToggleFavorite, + onToggleLowPriority, + onInvite, + onCopyRoomLink, + onLeaveRoom, + onSetRoomNotifState, + }); + return ( + + ); +}; + +const meta = { + title: "Room List/RoomListItem", + component: RoomListItemWrapper, + tags: ["autodocs"], + decorators: [ + (Story) => ( +
        +
        + +
        +
        + ), + ], + args: { + ...defaultSnapshot, + isSelected: false, + isFocused: false, + roomIndex: 0, + roomCount: 10, + onOpenRoom: fn(), + onMarkAsRead: fn(), + onMarkAsUnread: fn(), + onToggleFavorite: fn(), + onToggleLowPriority: fn(), + onInvite: fn(), + onCopyRoomLink: fn(), + onLeaveRoom: fn(), + onSetRoomNotifState: fn(), + onFocus: fn(), + renderAvatar, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=101-13062", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Selected: Story = { + args: { + isSelected: true, + }, +}; + +export const Bold: Story = { + args: { + isBold: true, + name: "Team Updates", + }, +}; + +export const WithNotification: Story = { + args: { + isBold: true, + notification: { + hasAnyNotificationOrActivity: true, + isUnsentMessage: false, + invited: false, + isMention: false, + isActivityNotification: false, + isNotification: true, + hasUnreadCount: true, + count: 3, + muted: false, + }, + }, +}; + +export const WithMention: Story = { + args: { + isBold: true, + notification: { + hasAnyNotificationOrActivity: true, + isUnsentMessage: false, + invited: false, + isMention: true, + isActivityNotification: false, + isNotification: true, + hasUnreadCount: true, + count: 1, + muted: false, + }, + }, +}; + +export const Invitation: Story = { + args: { + name: "Secret Project", + messagePreview: "Bob invited you", + notification: { + hasAnyNotificationOrActivity: true, + isUnsentMessage: false, + invited: true, + isMention: false, + isActivityNotification: false, + isNotification: false, + hasUnreadCount: false, + count: 0, + muted: false, + }, + }, +}; + +export const UnsentMessage: Story = { + args: { + messagePreview: "Failed to send message", + notification: { + hasAnyNotificationOrActivity: true, + isUnsentMessage: true, + invited: false, + isMention: false, + isActivityNotification: false, + isNotification: false, + hasUnreadCount: false, + count: 0, + muted: false, + }, + }, +}; + +export const NoMessagePreview: Story = { + args: { + messagePreview: undefined, + }, +}; + +export const WithHoverMenu: Story = { + args: { + showMoreOptionsMenu: true, + }, +}; + +export const WithoutHoverMenu: Story = { + args: { + showMoreOptionsMenu: false, + }, +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItem.test.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.test.tsx new file mode 100644 index 0000000000..788c9f317f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; + +import * as stories from "./RoomListItem.stories"; + +const { + Default, + Selected, + Bold, + WithNotification, + WithMention, + Invitation, + UnsentMessage, + NoMessagePreview, + WithHoverMenu, + WithoutHoverMenu, +} = composeStories(stories); + +describe("", () => { + it("renders Default story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders Selected story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders Bold story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders WithNotification story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders WithMention story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders Invitation story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders UnsentMessage story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NoMessagePreview story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders WithHoverMenu story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("should call onOpenRoom when clicked", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("option")); + expect(Default.args.onOpenRoom).toHaveBeenCalled(); + }); + + it("should have aria-selected true when selected", () => { + render(); + expect(screen.getByRole("option")).toHaveAttribute("aria-selected", "true"); + }); + + it("should have aria-selected false when not selected", () => { + render(); + expect(screen.getByRole("option")).toHaveAttribute("aria-selected", "false"); + }); + + it("should have tabIndex -1 when not focused", () => { + render(); + expect(screen.getByRole("option")).toHaveAttribute("tabIndex", "-1"); + }); + + it("should call onFocus when focused", () => { + render(); + screen.getByRole("option").focus(); + expect(Default.args.onFocus).toHaveBeenCalled(); + }); + + it("should display notification decoration when present", () => { + render(); + expect(screen.getByTestId("notification-decoration")).toBeInTheDocument(); + }); + + it("should hide notification decoration when not present", () => { + render(); + expect(screen.queryByTestId("notification-decoration")).toBeNull(); + }); + + it("should show hover menu when showMoreOptionsMenu is true", () => { + const { container } = render(); + expect(container.querySelector('[aria-label="More Options"]')).not.toBeNull(); + }); + + it("should hide hover menu when showMoreOptionsMenu is false", () => { + const { container } = render(); + expect(container.querySelector('[aria-label="More Options"]')).toBeNull(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItem.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.tsx new file mode 100644 index 0000000000..bc16d15cd3 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItem.tsx @@ -0,0 +1,207 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, memo, useEffect, useRef, type ReactNode } from "react"; +import classNames from "classnames"; + +import { Flex } from "../../utils/Flex"; +import { NotificationDecoration, type NotificationDecorationData } from "./NotificationDecoration"; +import { RoomListItemHoverMenu } from "./RoomListItemHoverMenu"; +import { RoomListItemContextMenu } from "./RoomListItemContextMenu"; +import { type RoomNotifState } from "./RoomNotifs"; +import styles from "./RoomListItem.module.css"; +import { useViewModel, type ViewModel } from "../../viewmodel"; +import { _t } from "../../utils/i18n"; + +/** + * Opaque type representing a Room object from the parent application + */ +export type Room = unknown; + +/** + * Generate an accessible label for a room based on its notification state. + */ +function getA11yLabel(roomName: string, notification: NotificationDecorationData): string { + if (notification.isUnsentMessage) { + return _t("room_list|a11y|unsent_message", { roomName }); + } else if (notification.invited) { + return _t("room_list|a11y|invitation", { roomName }); + } else if (notification.isMention && notification.count) { + return _t("room_list|a11y|mention", { roomName, count: notification.count }); + } else if (notification.hasUnreadCount && notification.count) { + return _t("room_list|a11y|unread", { roomName, count: notification.count }); + } else { + return _t("room_list|a11y|default", { roomName }); + } +} + +/** + * Snapshot for a room list item. + * Contains all the data needed to render a room in the list. + */ +export interface RoomListItemSnapshot { + /** Unique identifier for the room (used for list keying) */ + id: string; + /** The opaque Room object from the client (e.g., matrix-js-sdk Room) */ + room: Room; + /** The name of the room */ + name: string; + /** Whether the room name should be bolded (has unread/activity) */ + isBold: boolean; + /** Optional message preview text */ + messagePreview?: string; + /** Notification decoration data */ + notification: NotificationDecorationData; + /** Whether the more options menu should be shown */ + showMoreOptionsMenu: boolean; + /** Whether the notification menu should be shown */ + showNotificationMenu: boolean; + /** Whether the room is a favourite room */ + isFavourite: boolean; + /** Whether the room is a low priority room */ + isLowPriority: boolean; + /** Can invite other users in the room */ + canInvite: boolean; + /** Can copy the room link */ + canCopyRoomLink: boolean; + /** Can mark the room as read */ + canMarkAsRead: boolean; + /** Can mark the room as unread */ + canMarkAsUnread: boolean; + /** The room's notification state */ + roomNotifState: RoomNotifState; +} + +/** + * Actions interface for room list item operations. + * Implemented by the room item view model. + */ +export interface RoomListItemActions { + /** Called when the room should be opened */ + onOpenRoom: () => void; + /** Called when the room should be marked as read */ + onMarkAsRead: () => void; + /** Called when the room should be marked as unread */ + onMarkAsUnread: () => void; + /** Called when the room's favorite status should be toggled */ + onToggleFavorite: () => void; + /** Called when the room's low priority status should be toggled */ + onToggleLowPriority: () => void; + /** Called when inviting users to the room */ + onInvite: () => void; + /** Called when copying the room link */ + onCopyRoomLink: () => void; + /** Called when leaving the room */ + onLeaveRoom: () => void; + /** Called when setting the room notification state */ + onSetRoomNotifState: (state: RoomNotifState) => void; +} + +/** + * The view model type for a room list item + */ +export type RoomItemViewModel = ViewModel & RoomListItemActions; + +/** + * Props for RoomListItemView component + */ +export interface RoomListItemViewProps extends Omit, "onFocus"> { + /** The room item view model */ + vm: RoomItemViewModel; + /** Whether the room is selected */ + isSelected: boolean; + /** Whether the room should be focused */ + isFocused: boolean; + /** Callback when item receives focus */ + onFocus: (roomId: string, e: React.FocusEvent) => void; + /** Index of this room in the list (for accessibility) */ + roomIndex: number; + /** Total number of rooms in the list (for accessibility) */ + roomCount: number; + /** Function to render the room avatar */ + renderAvatar: (room: Room) => ReactNode; +} + +/** + * A presentational room list item component. + * Displays room name, avatar, message preview, and notifications. + */ +export const RoomListItemView = memo(function RoomListItemView({ + vm, + isSelected, + isFocused, + onFocus, + roomIndex, + roomCount, + renderAvatar, + ...props +}: RoomListItemViewProps): JSX.Element { + const ref = useRef(null); + const item = useViewModel(vm); + + useEffect(() => { + if (isFocused) { + ref.current?.focus({ preventScroll: true, focusVisible: true } as FocusOptions); + } + }, [isFocused]); + + // Generate a11y label from notification state and room name + const a11yLabel = getA11yLabel(item.name, item.notification); + + const content = ( + ) => onFocus(item.id, e)} + tabIndex={isFocused ? 0 : -1} + {...props} + > + {renderAvatar(item.room)} + + {/* We truncate the room name when too long. Title here is to show the full name on hover */} +
        +
        + {item.name} +
        + {item.messagePreview && ( +
        + {item.messagePreview} +
        + )} +
        + {(item.showMoreOptionsMenu || item.showNotificationMenu) && ( + + )} + + {/* aria-hidden because we summarise the unread count/notification status in a11yLabel */} +
        + +
        +
        +
        + ); + + return {content}; +}); diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemContextMenu.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemContextMenu.tsx new file mode 100644 index 0000000000..0d202474f8 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemContextMenu.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, type PropsWithChildren } from "react"; +import { ContextMenu } from "@vector-im/compound-web"; + +import { _t } from "../../utils/i18n"; +import { MoreOptionContent, type RoomItemViewModel } from "./RoomListItemMoreOptionsMenu"; + +/** + * Props for RoomListItemContextMenu component + */ +export interface RoomListItemContextMenuProps { + /** The room item view model */ + vm: RoomItemViewModel; +} + +/** + * The context menu for room list items. + * Wraps the trigger element with a right-click context menu displaying room options. + */ +export const RoomListItemContextMenu: React.FC> = ({ + vm, + children, +}): JSX.Element => { + return ( + + + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemHoverMenu.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemHoverMenu.tsx new file mode 100644 index 0000000000..9a453b2014 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemHoverMenu.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; + +import { Flex } from "../../utils/Flex"; +import { RoomListItemMoreOptionsMenu, type RoomItemViewModel } from "./RoomListItemMoreOptionsMenu"; +import { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu"; +import styles from "./RoomListItem.module.css"; + +/** + * Props for RoomListItemHoverMenu component + */ +export interface RoomListItemHoverMenuProps { + /** Whether the more options menu should be shown */ + showMoreOptionsMenu: boolean; + /** Whether the notification menu should be shown */ + showNotificationMenu: boolean; + /** The room item view model */ + vm: RoomItemViewModel; +} + +/** + * The hover menu for room list items. + * Displays more options and notification settings menus. + */ +export const RoomListItemHoverMenu: React.FC = ({ + showMoreOptionsMenu, + showNotificationMenu, + vm, +}): JSX.Element => { + return ( + + {showMoreOptionsMenu && } + {showNotificationMenu && } + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx new file mode 100644 index 0000000000..40b9917c5b --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.test.tsx @@ -0,0 +1,227 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi } from "vitest"; + +import { RoomListItemMoreOptionsMenu } from "./RoomListItemMoreOptionsMenu"; +import { useMockedViewModel } from "../../viewmodel"; +import type { RoomListItemSnapshot } from "./RoomListItem"; +import { defaultSnapshot } from "./default-snapshot"; + +describe("", () => { + const mockCallbacks = { + onOpenRoom: vi.fn(), + onMarkAsRead: vi.fn(), + onMarkAsUnread: vi.fn(), + onToggleFavorite: vi.fn(), + onToggleLowPriority: vi.fn(), + onInvite: vi.fn(), + onCopyRoomLink: vi.fn(), + onLeaveRoom: vi.fn(), + onSetRoomNotifState: vi.fn(), + }; + + const renderMenu = (overrides: Partial = {}): ReturnType => { + const TestComponent = (): JSX.Element => { + const vm = useMockedViewModel( + { + ...defaultSnapshot, + showMoreOptionsMenu: true, + showNotificationMenu: false, + ...overrides, + } as RoomListItemSnapshot, + mockCallbacks, + ); + return ; + }; + return render(); + }; + + it("should render the more options button", () => { + renderMenu(); + expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument(); + }); + + it("should open menu when clicked", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menu")).toBeInTheDocument(); + }); + + it("should show mark as read option when canMarkAsRead is true", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsRead: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Mark as read" })).toBeInTheDocument(); + }); + + it("should not show mark as read option when canMarkAsRead is false", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsRead: false }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.queryByRole("menuitem", { name: "Mark as read" })).not.toBeInTheDocument(); + }); + + it("should call onMarkAsRead when mark as read clicked", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsRead: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const markAsReadOption = screen.getByRole("menuitem", { name: "Mark as read" }); + await user.click(markAsReadOption); + + expect(mockCallbacks.onMarkAsRead).toHaveBeenCalled(); + }); + + it("should show mark as unread option when canMarkAsUnread is true", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsUnread: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Mark as unread" })).toBeInTheDocument(); + }); + + it("should call onMarkAsUnread when mark as unread clicked", async () => { + const user = userEvent.setup(); + renderMenu({ canMarkAsUnread: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const markAsUnreadOption = screen.getByRole("menuitem", { name: "Mark as unread" }); + await user.click(markAsUnreadOption); + + expect(mockCallbacks.onMarkAsUnread).toHaveBeenCalled(); + }); + + it("should show favorite option and call onToggleFavorite", async () => { + const user = userEvent.setup(); + renderMenu({ isFavourite: false }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const favoriteOption = screen.getByRole("menuitemcheckbox", { name: "Favourited" }); + expect(favoriteOption).toBeInTheDocument(); + expect(favoriteOption).toHaveAttribute("aria-checked", "false"); + + await user.click(favoriteOption); + expect(mockCallbacks.onToggleFavorite).toHaveBeenCalled(); + }); + + it("should show favorite as checked when isFavourite is true", async () => { + const user = userEvent.setup(); + renderMenu({ isFavourite: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const favoriteOption = screen.getByRole("menuitemcheckbox", { name: "Favourited" }); + expect(favoriteOption).toHaveAttribute("aria-checked", "true"); + }); + + it("should show low priority option and call onToggleLowPriority", async () => { + const user = userEvent.setup(); + renderMenu({ isLowPriority: false }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const lowPriorityOption = screen.getByRole("menuitemcheckbox", { name: "Low priority" }); + expect(lowPriorityOption).toBeInTheDocument(); + expect(lowPriorityOption).toHaveAttribute("aria-checked", "false"); + + await user.click(lowPriorityOption); + expect(mockCallbacks.onToggleLowPriority).toHaveBeenCalled(); + }); + + it("should show invite option when canInvite is true", async () => { + const user = userEvent.setup(); + renderMenu({ canInvite: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Invite" })).toBeInTheDocument(); + }); + + it("should call onInvite when invite clicked", async () => { + const user = userEvent.setup(); + renderMenu({ canInvite: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const inviteOption = screen.getByRole("menuitem", { name: "Invite" }); + await user.click(inviteOption); + + expect(mockCallbacks.onInvite).toHaveBeenCalled(); + }); + + it("should show copy link option when canCopyRoomLink is true", async () => { + const user = userEvent.setup(); + renderMenu({ canCopyRoomLink: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Copy room link" })).toBeInTheDocument(); + }); + + it("should call onCopyRoomLink when copy link clicked", async () => { + const user = userEvent.setup(); + renderMenu({ canCopyRoomLink: true }); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const copyLinkOption = screen.getByRole("menuitem", { name: "Copy room link" }); + await user.click(copyLinkOption); + + expect(mockCallbacks.onCopyRoomLink).toHaveBeenCalled(); + }); + + it("should show leave room option", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + expect(screen.getByRole("menuitem", { name: "Leave room" })).toBeInTheDocument(); + }); + + it("should call onLeaveRoom when leave room clicked", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "More Options" }); + await user.click(button); + + const leaveRoomOption = screen.getByRole("menuitem", { name: "Leave room" }); + await user.click(leaveRoomOption); + + expect(mockCallbacks.onLeaveRoom).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.tsx new file mode 100644 index 0000000000..d10b5c32ec --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemMoreOptionsMenu.tsx @@ -0,0 +1,137 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { useState, type JSX } from "react"; +import { IconButton, Menu, MenuItem, Separator, ToggleMenuItem } from "@vector-im/compound-web"; +import { + MarkAsReadIcon, + MarkAsUnreadIcon, + FavouriteIcon, + ArrowDownIcon, + UserAddIcon, + LinkIcon, + LeaveIcon, + OverflowHorizontalIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; + +import { _t } from "../../utils/i18n"; +import { useViewModel, type ViewModel } from "../../viewmodel"; +import type { RoomListItemSnapshot, RoomListItemActions } from "./RoomListItem"; + +/** + * View model type for room list item + */ +export type RoomItemViewModel = ViewModel & RoomListItemActions; + +/** + * Props for RoomListItemMoreOptionsMenu component + */ +export interface RoomListItemMoreOptionsMenuProps { + /** The room item view model */ + vm: RoomItemViewModel; +} + +/** + * The more options menu for room list items. + * Displays additional room actions like mark as read/unread, favorite, invite, etc. + */ +export function RoomListItemMoreOptionsMenu({ vm }: RoomListItemMoreOptionsMenuProps): JSX.Element { + const [open, setOpen] = useState(false); + + return ( + + + + } + > + + + ); +} + +interface MoreOptionContentProps { + vm: RoomItemViewModel; +} + +export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element { + const snapshot = useViewModel(vm); + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
        e.stopPropagation()}> + {snapshot.canMarkAsRead && ( + evt.stopPropagation()} + hideChevron={true} + /> + )} + {snapshot.canMarkAsUnread && ( + evt.stopPropagation()} + hideChevron={true} + /> + )} + evt.stopPropagation()} + /> + evt.stopPropagation()} + /> + {snapshot.canInvite && ( + evt.stopPropagation()} + hideChevron={true} + /> + )} + {snapshot.canCopyRoomLink && ( + evt.stopPropagation()} + hideChevron={true} + /> + )} + + evt.stopPropagation()} + hideChevron={true} + /> +
        + ); +} diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx new file mode 100644 index 0000000000..3f88e2f8a1 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.test.tsx @@ -0,0 +1,164 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi } from "vitest"; + +import { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu"; +import { RoomNotifState } from "./RoomNotifs"; +import { useMockedViewModel } from "../../viewmodel"; +import type { RoomListItemSnapshot } from "./RoomListItem"; +import { defaultSnapshot } from "./default-snapshot"; + +describe("", () => { + const mockCallbacks = { + onOpenRoom: vi.fn(), + onMarkAsRead: vi.fn(), + onMarkAsUnread: vi.fn(), + onToggleFavorite: vi.fn(), + onToggleLowPriority: vi.fn(), + onInvite: vi.fn(), + onCopyRoomLink: vi.fn(), + onLeaveRoom: vi.fn(), + onSetRoomNotifState: vi.fn(), + }; + + const renderMenu = (roomNotifState: RoomNotifState = RoomNotifState.AllMessages): ReturnType => { + const TestComponent = (): JSX.Element => { + const vm = useMockedViewModel( + { + ...defaultSnapshot, + showMoreOptionsMenu: false, + showNotificationMenu: true, + roomNotifState, + } as RoomListItemSnapshot, + mockCallbacks, + ); + return ; + }; + return render(); + }; + + it("should render the notification menu button", () => { + renderMenu(); + expect(screen.getByRole("button", { name: "Notification options" })).toBeInTheDocument(); + }); + + it("should show muted icon when notifications are muted", () => { + renderMenu(RoomNotifState.Mute); + const button = screen.getByRole("button", { name: "Notification options" }); + expect(button.querySelector("svg")).toBeInTheDocument(); + }); + + it("should open menu when clicked", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + expect(screen.getByRole("menu")).toBeInTheDocument(); + }); + + it("should call onSetRoomNotifState with AllMessages when default settings selected", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const defaultOption = screen.getByRole("menuitem", { name: "Match default settings" }); + await user.click(defaultOption); + + expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessages); + }); + + it("should call onSetRoomNotifState with AllMessagesLoud when all messages selected", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const allMessagesOption = screen.getByRole("menuitem", { name: "All messages" }); + await user.click(allMessagesOption); + + expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.AllMessagesLoud); + }); + + it("should call onSetRoomNotifState with MentionsOnly when mentions and keywords selected", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const mentionsOption = screen.getByRole("menuitem", { name: "Mentions and keywords" }); + await user.click(mentionsOption); + + expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.MentionsOnly); + }); + + it("should call onSetRoomNotifState with Mute when mute selected", async () => { + const user = userEvent.setup(); + renderMenu(); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const muteOption = screen.getByRole("menuitem", { name: "Mute room" }); + await user.click(muteOption); + + expect(mockCallbacks.onSetRoomNotifState).toHaveBeenCalledWith(RoomNotifState.Mute); + }); + + it("should show check mark next to selected option - AllMessage", async () => { + const user = userEvent.setup(); + renderMenu(RoomNotifState.AllMessages); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const defaultOption = screen.getByRole("menuitem", { name: "Match default settings" }); + expect(defaultOption).toHaveAttribute("aria-selected", "true"); + }); + + it("should show check mark next to selected option - AllMessagesLoud", async () => { + const user = userEvent.setup(); + renderMenu(RoomNotifState.AllMessagesLoud); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const allMessagesOption = screen.getByRole("menuitem", { name: "All messages" }); + expect(allMessagesOption).toHaveAttribute("aria-selected", "true"); + }); + + it("should show check mark next to selected option - MentionsOnly", async () => { + const user = userEvent.setup(); + renderMenu(RoomNotifState.MentionsOnly); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const mentionsOption = screen.getByRole("menuitem", { name: "Mentions and keywords" }); + expect(mentionsOption).toHaveAttribute("aria-selected", "true"); + }); + + it("should show check mark next to selected option - Mute", async () => { + const user = userEvent.setup(); + renderMenu(RoomNotifState.Mute); + + const button = screen.getByRole("button", { name: "Notification options" }); + await user.click(button); + + const muteOption = screen.getByRole("menuitem", { name: "Mute room" }); + expect(muteOption).toHaveAttribute("aria-selected", "true"); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.tsx b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.tsx new file mode 100644 index 0000000000..e4038fae6c --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomListItemNotificationMenu.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { useState, type JSX } from "react"; +import { IconButton, Menu, MenuItem } from "@vector-im/compound-web"; +import { + NotificationsSolidIcon, + NotificationsOffSolidIcon, + CheckIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; + +import { _t } from "../../utils/i18n"; +import { RoomNotifState } from "./RoomNotifs"; +import { useViewModel, type ViewModel } from "../../viewmodel"; +import type { RoomListItemSnapshot, RoomListItemActions } from "./RoomListItem"; + +/** + * View model type for room list item + */ +export type RoomItemViewModel = ViewModel & RoomListItemActions; + +/** + * Props for RoomListItemNotificationMenu component + */ +export interface RoomListItemNotificationMenuProps { + /** The room item view model */ + vm: RoomItemViewModel; +} + +/** + * The notification settings menu for room list items. + * Displays options to change notification settings. + */ +export function RoomListItemNotificationMenu({ vm }: RoomListItemNotificationMenuProps): JSX.Element { + const snapshot = useViewModel(vm); + const [open, setOpen] = useState(false); + const isMuted = snapshot.roomNotifState === RoomNotifState.Mute; + const checkComponent = ; + + return ( + + {isMuted ? : } + + } + > + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
        e.stopPropagation()} + > + vm.onSetRoomNotifState(RoomNotifState.AllMessages)} + onClick={(evt) => evt.stopPropagation()} + > + {snapshot.roomNotifState === RoomNotifState.AllMessages && checkComponent} + + vm.onSetRoomNotifState(RoomNotifState.AllMessagesLoud)} + onClick={(evt) => evt.stopPropagation()} + > + {snapshot.roomNotifState === RoomNotifState.AllMessagesLoud && checkComponent} + + vm.onSetRoomNotifState(RoomNotifState.MentionsOnly)} + onClick={(evt) => evt.stopPropagation()} + > + {snapshot.roomNotifState === RoomNotifState.MentionsOnly && checkComponent} + + vm.onSetRoomNotifState(RoomNotifState.Mute)} + onClick={(evt) => evt.stopPropagation()} + > + {snapshot.roomNotifState === RoomNotifState.Mute && checkComponent} + +
        +
        + ); +} diff --git a/packages/shared-components/src/room-list/RoomListItem/RoomNotifs.ts b/packages/shared-components/src/room-list/RoomListItem/RoomNotifs.ts new file mode 100644 index 0000000000..06fc0fc23d --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/RoomNotifs.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +/** + * Notification state for a room. + */ +export enum RoomNotifState { + /** All messages (default) */ + AllMessages = "all_messages", + /** All messages with sound */ + AllMessagesLoud = "all_messages_loud", + /** Only mentions and keywords */ + MentionsOnly = "mentions_only", + /** Muted */ + Mute = "mute", +} diff --git a/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItem.test.tsx.snap b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItem.test.tsx.snap new file mode 100644 index 0000000000..ff1ccad613 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/__snapshots__/RoomListItem.test.tsx.snap @@ -0,0 +1,1236 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders Bold story 1`] = ` +
        +
        +
        + + +
        + + +
        +
        +
        +`; + +exports[` > renders Default story 1`] = ` +
        +
        +
        + + +
        + + +
        +
        +
        +`; + +exports[` > renders Invitation story 1`] = ` +
        +
        +
        + + +
        + +
        + +
        +
        +
        +`; + +exports[` > renders NoMessagePreview story 1`] = ` +
        +
        +
        + + +
        + + +
        +
        +
        +`; + +exports[` > renders Selected story 1`] = ` +
        +
        +
        + + +
        + + +
        +
        +
        +`; + +exports[` > renders UnsentMessage story 1`] = ` +
        +
        +
        + + +
        + +
        + +
        +
        +
      +`; + +exports[` > renders WithHoverMenu story 1`] = ` +
      +
      +
      + + +
      + + +
      +
      +
    +`; + +exports[` > renders WithMention story 1`] = ` +
    +
    +
    + + +
    + +
    + +
    +
+
+`; + +exports[` > renders WithNotification story 1`] = ` +
+
+
+ + +
+ +
+ +
+
+
+`; diff --git a/packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts b/packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts new file mode 100644 index 0000000000..b5e263567f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/default-snapshot.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type RoomListItemSnapshot } from "./RoomListItem"; +import { RoomNotifState } from "./RoomNotifs"; + +export const mockRoom = { name: "General" }; + +export const defaultSnapshot: RoomListItemSnapshot = { + id: "!room:server", + room: mockRoom, + name: "General", + isBold: false, + messagePreview: "Alice: Hey everyone!", + notification: { + hasAnyNotificationOrActivity: false, + isUnsentMessage: false, + invited: false, + isMention: false, + isActivityNotification: false, + isNotification: false, + hasUnreadCount: false, + count: 0, + muted: false, + }, + showMoreOptionsMenu: true, + showNotificationMenu: true, + isFavourite: false, + isLowPriority: false, + canInvite: true, + canCopyRoomLink: true, + canMarkAsRead: false, + canMarkAsUnread: true, + roomNotifState: RoomNotifState.AllMessages, +}; diff --git a/packages/shared-components/src/room-list/RoomListItem/index.ts b/packages/shared-components/src/room-list/RoomListItem/index.ts new file mode 100644 index 0000000000..edf17066b8 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItem/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { RoomListItemView } from "./RoomListItem"; +export type { + Room, + RoomListItemSnapshot, + RoomItemViewModel, + RoomListItemActions, + RoomListItemViewProps, +} from "./RoomListItem"; +export { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu"; +export type { RoomListItemNotificationMenuProps } from "./RoomListItemNotificationMenu"; +export { RoomListItemMoreOptionsMenu, MoreOptionContent } from "./RoomListItemMoreOptionsMenu"; +export type { RoomListItemMoreOptionsMenuProps } from "./RoomListItemMoreOptionsMenu"; +export { RoomListItemHoverMenu } from "./RoomListItemHoverMenu"; +export type { RoomListItemHoverMenuProps } from "./RoomListItemHoverMenu"; +export { RoomListItemContextMenu } from "./RoomListItemContextMenu"; +export type { RoomListItemContextMenuProps } from "./RoomListItemContextMenu"; +export { NotificationDecoration } from "./NotificationDecoration"; +export type { NotificationDecorationProps, NotificationDecorationData } from "./NotificationDecoration"; +export { RoomNotifState } from "./RoomNotifs"; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css new file mode 100644 index 0000000000..29db6d1bd6 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.module.css @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.roomListPrimaryFilters { + padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x); +} + +/* Hide filters that are wrapping when collapsed */ +.roomListPrimaryFilters :global(.wrapping) { + display: none; +} + +.list { + /** + * The InteractionObserver needs the height to be set to work properly. + */ + height: 100%; + flex: 1; +} + +/* IconButton styles for chevron */ +.iconButton svg { + transition: transform 0.1s linear; +} + +.iconButton[aria-expanded="true"] svg { + transform: rotate(180deg); +} diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx new file mode 100644 index 0000000000..a1a80334b9 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.stories.tsx @@ -0,0 +1,91 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; +import type { FilterId } from "./useVisibleFilters"; + +const meta: Meta = { + title: "Room List/RoomListPrimaryFilters", + component: RoomListPrimaryFilters, + tags: ["autodocs"], + args: { + onToggleFilter: fn(), + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel-2025?node-id=98-1979&t=vafb4zoYMNLRuAbh-4", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +// All available filter IDs +const allFilterIds: FilterId[] = ["unread", "people", "rooms", "favourite", "mentions", "invites", "low_priority"]; + +// Subset of filters for narrow container tests +const fewFilterIds: FilterId[] = ["people", "rooms", "unread"]; + +export const Default: Story = { + args: { + filterIds: allFilterIds, + }, +}; + +export const PeopleSelected: Story = { + args: { + filterIds: allFilterIds, + activeFilterId: "people", + }, +}; + +export const NoFilters: Story = { + args: { + filterIds: [], + }, +}; + +/** + * Narrow container that causes filters to wrap. + * The chevron button should appear to expand/collapse the filter list. + */ +export const NarrowContainer: Story = { + args: { + filterIds: fewFilterIds, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +/** + * Narrow container with active filter that would wrap. + * When collapsed, the active filter should move to the front. + */ +export const NarrowWithActiveWrappingFilter: Story = { + args: { + filterIds: fewFilterIds, + activeFilterId: "unread", + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx new file mode 100644 index 0000000000..a86181da15 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.test.tsx @@ -0,0 +1,140 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { act } from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect, vi, beforeEach } from "vitest"; + +import * as stories from "./RoomListPrimaryFilters.stories"; + +const { Default, PeopleSelected, NoFilters, NarrowContainer, NarrowWithActiveWrappingFilter } = composeStories(stories); + +describe(" stories", () => { + describe("snapshots", () => { + it("renders Default story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders PeopleSelected story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NoFilters story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NarrowContainer story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders NarrowWithActiveWrappingFilter story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + }); + + describe("behavior", () => { + it("should call onToggleFilter when a filter is clicked", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("option", { name: "People" })); + + expect(Default.args.onToggleFilter).toHaveBeenCalled(); + }); + }); + + describe("resize behavior", () => { + let resizeCallback: ResizeObserverCallback; + + beforeEach(() => { + globalThis.ResizeObserver = class MockResizeObserver { + public constructor(callback: ResizeObserverCallback) { + resizeCallback = callback; + } + public observe = vi.fn(); + public unobserve = vi.fn(); + public disconnect = vi.fn(); + } as unknown as typeof ResizeObserver; + }); + + function mockFiltersNotWrapping(): void { + vi.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0); + vi.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30); + vi.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(60); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + act(() => resizeCallback([{ target: listbox } as any], {} as ResizeObserver)); + } + + function mockUnreadWrapping(): void { + vi.spyOn(screen.getByText("People"), "offsetLeft", "get").mockReturnValue(0); + vi.spyOn(screen.getByText("Rooms"), "offsetLeft", "get").mockReturnValue(30); + vi.spyOn(screen.getByText("Unreads"), "offsetLeft", "get").mockReturnValue(0); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + act(() => resizeCallback([{ target: listbox } as any], {} as ResizeObserver)); + } + + it("should hide wrapping filters and show chevron", () => { + render(); + mockUnreadWrapping(); + + expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); + expect(screen.getByRole("button", { name: "Expand filter list" })).toBeInTheDocument(); + }); + + it("should expand and collapse filter list with chevron button", async () => { + const user = userEvent.setup(); + render(); + mockUnreadWrapping(); + + expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); + + await user.click(screen.getByRole("button", { name: "Expand filter list" })); + expect(screen.getByRole("option", { name: "Unreads" })).toBeVisible(); + + await user.click(screen.getByRole("button", { name: "Collapse filter list" })); + expect(screen.queryByRole("option", { name: "Unreads" })).toBeNull(); + }); + + it("should move active filter to front when collapsed and wrapping", () => { + render(); + mockUnreadWrapping(); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + expect(listbox.children[0]).toBe(screen.getByRole("option", { name: "Unreads" })); + }); + + it("should restore original filter order when expanded", async () => { + const user = userEvent.setup(); + render(); + mockUnreadWrapping(); + + await user.click(screen.getByRole("button", { name: "Expand filter list" })); + + const listbox = screen.getByRole("listbox", { name: "Room list filters" }); + expect(listbox.children[0]).toBe(screen.getByRole("option", { name: "People" })); + }); + + it("should handle resize from non-wrapping to wrapping", () => { + render(); + mockFiltersNotWrapping(); + + expect(screen.queryByRole("button", { name: "Expand filter list" })).toBeNull(); + + mockUnreadWrapping(); + expect(screen.getByRole("button", { name: "Expand filter list" })).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx new file mode 100644 index 0000000000..561544a3a5 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, useId, useState } from "react"; +import { ChatFilter, IconButton } from "@vector-im/compound-web"; +import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; + +import { Flex } from "../../utils/Flex"; +import { _t } from "../../utils/i18n"; +import { useCollapseFilters } from "./useCollapseFilters"; +import { useVisibleFilters, type FilterId } from "./useVisibleFilters"; +import styles from "./RoomListPrimaryFilters.module.css"; + +/** + * Maps filter IDs to translated labels + */ +const filterIdToLabel = (filterId: FilterId): string => { + switch (filterId) { + case "unread": + return _t("room_list|filters|unread"); + case "people": + return _t("room_list|filters|people"); + case "rooms": + return _t("room_list|filters|rooms"); + case "favourite": + return _t("room_list|filters|favourite"); + case "mentions": + return _t("room_list|filters|mentions"); + case "invites": + return _t("room_list|filters|invites"); + case "low_priority": + return _t("room_list|filters|low_priority"); + } +}; + +/** + * Props for RoomListPrimaryFilters component + */ +export interface RoomListPrimaryFiltersProps { + /** Array of filter IDs to display */ + filterIds: FilterId[]; + /** Currently active filter ID (if any) */ + activeFilterId?: FilterId; + /** Callback when a filter is toggled */ + onToggleFilter: (filterId: FilterId) => void; +} + +/** + * The primary filters component for the room list. + * Displays a collapsible list of filters with expand/collapse functionality. + */ +export const RoomListPrimaryFilters: React.FC = ({ + filterIds, + activeFilterId, + onToggleFilter, +}): JSX.Element | null => { + const id = useId(); + const [isExpanded, setIsExpanded] = useState(false); + + const { + ref, + isWrapping: displayChevron, + wrappingIndex, + } = useCollapseFilters(isExpanded, "wrapping"); + const visibleFilterIds = useVisibleFilters(filterIds, activeFilterId, wrappingIndex); + + return ( + + {displayChevron && ( + setIsExpanded((expanded) => !expanded)} + > + + + )} + + {visibleFilterIds.map((filterId, index) => ( + onToggleFilter(filterId)} + > + {filterIdToLabel(filterId)} + + ))} + + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap b/packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap new file mode 100644 index 0000000000..74c281bde5 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/__snapshots__/RoomListPrimaryFilters.test.tsx.snap @@ -0,0 +1,388 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` stories > snapshots > renders Default story 1`] = ` +
+
+ +
+ + + + + + + +
+
+
+`; + +exports[` stories > snapshots > renders NarrowContainer story 1`] = ` +
+
+
+ +
+ + + +
+
+
+
+`; + +exports[` stories > snapshots > renders NarrowWithActiveWrappingFilter story 1`] = ` +
+
+
+ +
+ + + +
+
+
+
+`; + +exports[` stories > snapshots > renders NoFilters story 1`] = ` +
+
+
+
+
+`; + +exports[` stories > snapshots > renders PeopleSelected story 1`] = ` +
+
+ +
+ + + + + + + +
+
+
+`; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx new file mode 100644 index 0000000000..7697d4829c --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/index.tsx @@ -0,0 +1,12 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; +export type { RoomListPrimaryFiltersProps } from "./RoomListPrimaryFilters"; +export { useCollapseFilters } from "./useCollapseFilters"; +export { useVisibleFilters } from "./useVisibleFilters"; +export type { FilterId } from "./useVisibleFilters"; diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts new file mode 100644 index 0000000000..e3fbf74e54 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useCollapseFilters.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { useEffect, useRef, useState, type RefObject } from "react"; + +/** + * A hook to manage the wrapping of filters in the room list. + * It observes the filter list and hides filters that are wrapping when the list is not expanded. + * @param isExpanded + * @param wrappingClassName - the CSS class to apply to wrapping filters + * @returns an object containing: + * - `ref`: a ref to put on the filter list element + * - `isWrapping`: a boolean indicating if the filters are wrapping + * - `wrappingIndex`: the index of the first filter that is wrapping + */ +export function useCollapseFilters( + isExpanded: boolean, + wrappingClassName: string, +): { + ref: RefObject; + isWrapping: boolean; + wrappingIndex: number; +} { + const ref = useRef(null); + const [isWrapping, setIsWrapping] = useState(false); + const [wrappingIndex, setWrappingIndex] = useState(-1); + + useEffect(() => { + if (!ref.current) return; + + const hideFilters = (list: Element): void => { + let isWrapping = false; + Array.from(list.children).forEach((node, i): void => { + const child = node as HTMLElement; + child.setAttribute("aria-hidden", "false"); + child.classList.remove(wrappingClassName); + + // If the filter list is expanded, all filters are visible + if (isExpanded) return; + + // If the previous element is on the left element of the current one, it means that the filter is wrapping + const previousSibling = child.previousElementSibling as HTMLElement | null; + if (previousSibling && child.offsetLeft <= previousSibling.offsetLeft) { + if (!isWrapping) setWrappingIndex(i); + isWrapping = true; + } + + // If the filter is wrapping, we hide it + child.classList.toggle(wrappingClassName, isWrapping); + child.setAttribute("aria-hidden", isWrapping.toString()); + }); + + if (!isWrapping) setWrappingIndex(-1); + setIsWrapping(isExpanded || isWrapping); + }; + + hideFilters(ref.current); + const observer = new ResizeObserver((entries) => entries.forEach((entry) => hideFilters(entry.target))); + + observer.observe(ref.current); + return () => { + observer.disconnect(); + }; + }, [isExpanded, wrappingClassName]); + + return { ref, isWrapping, wrappingIndex }; +} diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts new file mode 100644 index 0000000000..73a580b4d9 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/useVisibleFilters.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { useEffect, useState } from "react"; + +/** + * Standard filter identifiers that can be used across implementations. + * These are stable keys - the view layer maps them to translated labels. + */ +export type FilterId = "unread" | "people" | "rooms" | "favourite" | "mentions" | "invites" | "low_priority"; + +/** + * A hook to sort the filter IDs by active state. + * The list is sorted if the active filter index is greater than or equal to the wrapping index. + * If the wrapping index is -1, the filters are not sorted. + * + * @param filterIds - the list of filter IDs to sort. + * @param activeFilterId - the currently active filter ID (if any). + * @param wrappingIndex - the index of the first filter that is wrapping. + */ +export function useVisibleFilters( + filterIds: FilterId[], + activeFilterId: FilterId | undefined, + wrappingIndex: number, +): FilterId[] { + // By default, the filters are not sorted + const [sortedFilterIds, setSortedFilterIds] = useState(filterIds); + + useEffect(() => { + const activeIndex = activeFilterId ? filterIds.indexOf(activeFilterId) : -1; + const isActiveFilterWrapping = activeIndex >= wrappingIndex; + // If the active filter is not wrapping, we don't need to sort the filters + if (!isActiveFilterWrapping || wrappingIndex === -1) { + setSortedFilterIds(filterIds); + return; + } + + // Sort the filters with the active filter at first position + setSortedFilterIds( + filterIds.slice().sort((filterA, filterB) => { + // If the filter is active, it should be at the top of the list + if (filterA === activeFilterId && filterB !== activeFilterId) return -1; + if (filterA !== activeFilterId && filterB === activeFilterId) return 1; + // If both filters are active or not, keep their original order + return 0; + }), + ); + }, [filterIds, activeFilterId, wrappingIndex]); + + return sortedFilterIds; +} diff --git a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx index 66f5af461c..d862cc4295 100644 --- a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx @@ -14,7 +14,7 @@ import { type RoomListSearchViewActions, type RoomListSearchViewSnapshot, } from "./RoomListSearchView"; -import { useMockedViewModel } from "../../useMockedViewModel"; +import { useMockedViewModel } from "../../viewmodel"; type RoomListSearchProps = RoomListSearchViewSnapshot & RoomListSearchViewActions; diff --git a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.test.tsx b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.test.tsx index 1a256d63b1..325d8516da 100644 --- a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.test.tsx +++ b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.test.tsx @@ -5,10 +5,11 @@ * Please see LICENSE files in the repository root for full details. */ -import { render, screen } from "jest-matrix-react"; +import { render, screen } from "@test-utils"; import { composeStories } from "@storybook/react-vite"; import React from "react"; import userEvent from "@testing-library/user-event"; +import { describe, it, vi, afterEach, expect } from "vitest"; import * as stories from "./RoomListSearchView.stories"; import { @@ -22,7 +23,7 @@ const { Default, WithDialPad, WithoutExplore, AllButtons } = composeStories(stor describe("RoomListSearchView", () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe("Storybook snapshots", () => { @@ -48,9 +49,9 @@ describe("RoomListSearchView", () => { }); describe("User interactions", () => { - const onSearchClick = jest.fn(); - const onDialPadClick = jest.fn(); - const onExploreClick = jest.fn(); + const onSearchClick = vi.fn(); + const onDialPadClick = vi.fn(); + const onExploreClick = vi.fn(); class TestViewModel extends MockViewModel implements RoomListSearchViewActions { public onSearchClick = onSearchClick; diff --git a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.tsx b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.tsx index 0c652328ec..3b4218e02e 100644 --- a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.tsx +++ b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.tsx @@ -12,8 +12,7 @@ import SearchIcon from "@vector-im/compound-design-tokens/assets/web/icons/searc import DialPadIcon from "@vector-im/compound-design-tokens/assets/web/icons/dial-pad"; import styles from "./RoomListSearchView.module.css"; -import { type ViewModel } from "../../viewmodel/ViewModel"; -import { useViewModel } from "../../useViewModel"; +import { type ViewModel, useViewModel } from "../../viewmodel"; import { Flex } from "../../utils/Flex"; import { useI18n } from "../../utils/i18nContext"; diff --git a/packages/shared-components/src/room-list/RoomListSearchView/__snapshots__/RoomListSearchView.test.tsx.snap b/packages/shared-components/src/room-list/RoomListSearchView/__snapshots__/RoomListSearchView.test.tsx.snap index 689ea2ad8d..6c6a8ba373 100644 --- a/packages/shared-components/src/room-list/RoomListSearchView/__snapshots__/RoomListSearchView.test.tsx.snap +++ b/packages/shared-components/src/room-list/RoomListSearchView/__snapshots__/RoomListSearchView.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`RoomListSearchView Storybook snapshots renders the default state 1`] = ` +exports[`RoomListSearchView > Storybook snapshots > renders the default state 1`] = `
+ {snapshot.canCreateRoom && ( + + )} + + + ); + } + + // Handle different filter cases based on filter ID + switch (snapshot.activeFilterId) { + case "favourite": + return ( + + ); + case "people": + return ( + + ); + case "rooms": + return ( + + ); + case "unread": + return ( + vm.onToggleFilter(snapshot.activeFilterId!)} + /> + ); + case "invites": + return ( + vm.onToggleFilter(snapshot.activeFilterId!)} + /> + ); + case "mentions": + return ( + vm.onToggleFilter(snapshot.activeFilterId!)} + /> + ); + case "low_priority": + return ( + vm.onToggleFilter(snapshot.activeFilterId!)} + /> + ); + default: + return ( + + ); + } +}; + +interface GenericPlaceholderProps { + /** The title of the placeholder */ + title: string; + /** The description of the placeholder */ + description?: string; + /** Optional children (e.g., action buttons) */ + children?: ReactNode; +} + +/** + * A generic placeholder for the room list + */ +function GenericPlaceholder({ title, description, children }: PropsWithChildren): JSX.Element { + return ( + + {title} + {description && {description}} + {children} + + ); +} + +interface ActionPlaceholderProps { + /** The title to display */ + title: string; + /** The action button text */ + action: string; + /** Callback when the action button is clicked */ + onAction?: () => void; +} + +/** + * A placeholder for the room list when a filter is active + * The user can take action to toggle the filter + */ +function ActionPlaceholder({ title, action, onAction }: ActionPlaceholderProps): JSX.Element { + return ( + + {onAction && ( + + )} + + ); +} diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.module.css b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.module.css new file mode 100644 index 0000000000..2f65f7969d --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.module.css @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +.skeleton { + position: relative; + margin-left: 4px; + height: 100%; + flex: 1; +} + +.skeleton::before { + background-color: var(--cpd-color-bg-subtle-secondary); + width: 100%; + height: 100%; + content: ""; + position: absolute; + mask-repeat: repeat-y; + mask-size: auto 96px; + mask-image: url("./assets/skeleton.svg"); +} diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.tsx new file mode 100644 index 0000000000..6ab8b80de3 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListLoadingSkeleton.tsx @@ -0,0 +1,18 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; + +import styles from "./RoomListLoadingSkeleton.module.css"; + +/** + * Loading skeleton component for the room list. + * Displays a repeating skeleton pattern while rooms are being fetched. + */ +export const RoomListLoadingSkeleton: React.FC = (): JSX.Element => { + return
; +}; diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx new file mode 100644 index 0000000000..206307262e --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx @@ -0,0 +1,221 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Room } from "../RoomListItem/RoomListItem"; +import type { FilterId } from "../RoomListPrimaryFilters"; +import { RoomListView, type RoomListSnapshot, type RoomListViewActions } from "./RoomListView"; +import { useMockedViewModel } from "../../viewmodel"; +import { + renderAvatar, + createGetRoomItemViewModel, + mockRoomIds, + smallListRoomIds, + largeListRoomIds, +} from "../story-mocks"; + +type RoomListViewProps = RoomListSnapshot & RoomListViewActions & { renderAvatar: (room: Room) => React.ReactElement }; + +const mockFilterIds: FilterId[] = ["unread", "people", "rooms", "favourite"]; + +// Wrapper component that creates a mocked ViewModel +const RoomListViewWrapper = ({ + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + renderAvatar: renderAvatarProp, + ...rest +}: RoomListViewProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + }); + return ; +}; + +const meta = { + title: "Room List/RoomListView", + component: RoomListViewWrapper, + tags: ["autodocs"], + decorators: [ + (Story) => ( +
+ +
+ ), + ], + args: { + // Snapshot properties (state) + isLoadingRooms: false, + isRoomListEmpty: false, + filterIds: mockFilterIds, + activeFilterId: undefined, + roomListState: { + activeRoomIndex: undefined, + spaceId: "!space:server", + filterKeys: undefined, + }, + roomIds: mockRoomIds, + canCreateRoom: true, + // Action properties (callbacks) + onToggleFilter: fn(), + createChatRoom: fn(), + createRoom: fn(), + getRoomItemViewModel: createGetRoomItemViewModel(mockRoomIds), + updateVisibleRooms: fn(), + renderAvatar, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=2925-19126", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Loading: Story = { + args: { + isLoadingRooms: true, + }, +}; + +export const Empty: Story = { + args: { + isRoomListEmpty: true, + }, +}; + +export const EmptyWithoutCreatePermission: Story = { + args: { + isRoomListEmpty: true, + canCreateRoom: false, + }, +}; + +export const WithActiveFilter: Story = { + args: { + filterIds: ["unread", "people", "rooms", "favourite"], + activeFilterId: "favourite", + roomListState: { + activeRoomIndex: undefined, + spaceId: "!space:server", + filterKeys: ["favourites"], + }, + }, +}; + +export const WithSelection: Story = { + args: { + roomListState: { + activeRoomIndex: 0, + spaceId: "!space:server", + filterKeys: undefined, + }, + }, +}; + +export const EmptyFavouriteFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["favourite", "people"], + activeFilterId: "favourite", + }, +}; + +export const EmptyPeopleFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["people", "rooms"], + activeFilterId: "people", + }, +}; + +export const EmptyRoomsFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["rooms", "people"], + activeFilterId: "rooms", + }, +}; + +export const EmptyUnreadFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["unread", "people"], + activeFilterId: "unread", + }, +}; + +export const EmptyInvitesFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["invites", "people"], + activeFilterId: "invites", + }, +}; + +export const EmptyMentionsFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["mentions", "people"], + activeFilterId: "mentions", + }, +}; + +export const EmptyLowPriorityFilter: Story = { + args: { + isRoomListEmpty: true, + roomIds: [], + filterIds: ["low_priority", "people"], + activeFilterId: "low_priority", + }, +}; + +export const SmallList: Story = { + args: { + roomIds: smallListRoomIds, + getRoomItemViewModel: createGetRoomItemViewModel(smallListRoomIds), + }, +}; + +export const LargeList: Story = { + args: { + roomIds: largeListRoomIds, + getRoomItemViewModel: createGetRoomItemViewModel(largeListRoomIds), + }, +}; diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx new file mode 100644 index 0000000000..15237eed7e --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx @@ -0,0 +1,177 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen } from "@test-utils"; +import userEvent from "@testing-library/user-event"; +import { VirtuosoMockContext } from "react-virtuoso"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; + +import * as stories from "./RoomListView.stories"; + +const { + Default, + Loading, + Empty, + EmptyWithoutCreatePermission, + WithActiveFilter, + SmallList, + LargeList, + EmptyFavouriteFilter, + EmptyPeopleFilter, + EmptyRoomsFilter, + EmptyUnreadFilter, + EmptyInvitesFilter, + EmptyMentionsFilter, + EmptyLowPriorityFilter, +} = composeStories(stories); + +const renderWithMockContext = (component: React.ReactElement): ReturnType => { + return render(component, { + wrapper: ({ children }) => ( + + {children} + + ), + }); +}; + +describe("", () => { + it("renders Default story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders Loading story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders Empty story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyWithoutCreatePermission story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders WithActiveFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders SmallList story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders LargeList story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyFavouriteFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyPeopleFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyRoomsFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyUnreadFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyInvitesFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyMentionsFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders EmptyLowPriorityFilter story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("should call onToggleFilter when filter is clicked", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("option", { name: "People" })); + + expect(Default.args.onToggleFilter).toHaveBeenCalled(); + }); + + it("should call createRoom when New room button is clicked", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "New room" })); + + expect(Empty.args.createRoom).toHaveBeenCalled(); + }); + + it("should call createChatRoom when Start chat button is clicked", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "Start chat" })); + + expect(Empty.args.createChatRoom).toHaveBeenCalled(); + }); + + it("should call onToggleFilter when Show all chats is clicked in unread empty state", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "Show all chats" })); + + expect(EmptyUnreadFilter.args.onToggleFilter).toHaveBeenCalled(); + }); + + it("should call onToggleFilter when See all activity is clicked in invites empty state", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "See all activity" })); + + expect(EmptyInvitesFilter.args.onToggleFilter).toHaveBeenCalled(); + }); + + it("should call onToggleFilter when See all activity is clicked in mentions empty state", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "See all activity" })); + + expect(EmptyMentionsFilter.args.onToggleFilter).toHaveBeenCalled(); + }); + + it("should call onToggleFilter when See all activity is clicked in low priority empty state", async () => { + const user = userEvent.setup(); + renderWithMockContext(); + + await user.click(screen.getByRole("button", { name: "See all activity" })); + + expect(EmptyLowPriorityFilter.args.onToggleFilter).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx new file mode 100644 index 0000000000..491c28d7d1 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx @@ -0,0 +1,101 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX, type ReactNode } from "react"; + +import { useViewModel, type ViewModel } from "../../viewmodel"; +import { RoomListPrimaryFilters, type FilterId } from "../RoomListPrimaryFilters"; +import { RoomListLoadingSkeleton } from "./RoomListLoadingSkeleton"; +import { RoomListEmptyStateView } from "./RoomListEmptyStateView"; +import { VirtualizedRoomListView, type RoomListViewState } from "../VirtualizedRoomListView"; +import { type Room } from "../RoomListItem"; + +/** + * Snapshot for the room list view + */ +export type RoomListSnapshot = { + /** Whether the rooms are currently loading */ + isLoadingRooms: boolean; + /** Whether the room list is empty */ + isRoomListEmpty: boolean; + /** Array of filter IDs */ + filterIds: FilterId[]; + /** Currently active filter ID (if any) */ + activeFilterId?: FilterId; + /** Room list state */ + roomListState: RoomListViewState; + /** Array of room IDs for virtualization */ + roomIds: string[]; + /** Optional description for the empty state */ + emptyStateDescription?: string; + /** Optional action element for the empty state */ + emptyStateAction?: ReactNode; + /** Whether the user can create rooms */ + canCreateRoom?: boolean; +}; + +/** + * Actions interface for room list operations + */ +export interface RoomListViewActions { + /** Called when a filter is toggled */ + onToggleFilter: (filterId: FilterId) => void; + /** Called to create a new chat room */ + createChatRoom: () => void; + /** Called to create a new room */ + createRoom: () => void; + /** Get view model for a specific room (virtualization API) */ + getRoomItemViewModel: (roomId: string) => any; + /** Called when the visible range changes (virtualization API) */ + updateVisibleRooms: (startIndex: number, endIndex: number) => void; +} + +/** + * The view model type for the room list view + */ +export type RoomListViewModel = ViewModel & RoomListViewActions; + +/** + * Props for RoomListView component + */ +export interface RoomListViewProps { + /** The view model containing all data and callbacks */ + vm: RoomListViewModel; + /** Render function for room avatar */ + renderAvatar: (room: Room) => ReactNode; + /** Optional callback for keyboard events on the room list */ + onKeyDown?: (e: React.KeyboardEvent) => void; +} + +/** + * Room list view component that manages filters, loading states, empty states, and the room list. + */ +export const RoomListView: React.FC = ({ vm, renderAvatar, onKeyDown }): JSX.Element => { + const snapshot = useViewModel(vm); + let listBody: ReactNode; + + if (snapshot.isLoadingRooms) { + listBody = ; + } else if (snapshot.isRoomListEmpty) { + listBody = ; + } else { + listBody = ; + } + + return ( + <> +
+ +
+ {listBody} + + ); +}; diff --git a/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap b/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap new file mode 100644 index 0000000000..c518632039 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap @@ -0,0 +1,11387 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders Default story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + + + + +`; + +exports[` > renders Empty story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+ + No chats yet + + + Get started by messaging someone or by creating a room + +
+ + +
+
+
+
+`; + +exports[` > renders EmptyFavouriteFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don't have favourite chats yet + + + You can add a chat to your favourites in the chat settings + +
+
+
+`; + +exports[` > renders EmptyInvitesFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don't have any unread invites + + +
+
+
+`; + +exports[` > renders EmptyLowPriorityFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don't have any low priority rooms + + +
+
+
+`; + +exports[` > renders EmptyMentionsFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don't have any unread mentions + + +
+
+
+`; + +exports[` > renders EmptyPeopleFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You don’t have direct chats with anyone yet + + + You can deselect filters in order to see your other chats + +
+
+
+`; + +exports[` > renders EmptyRoomsFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + You’re not in any room yet + + + You can deselect filters in order to see your other chats + +
+
+
+`; + +exports[` > renders EmptyUnreadFilter story 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + Congrats! You don’t have any unread messages + + +
+
+
+`; + +exports[` > renders EmptyWithoutCreatePermission story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+ + No chats yet + + + Get started by messaging someone + +
+ +
+
+
+
+`; + +exports[` > renders LargeList story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + + + + + + +`; + +exports[` > renders Loading story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+`; + +exports[` > renders SmallList story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + + + +`; + +exports[` > renders WithActiveFilter story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + + + + +`; diff --git a/packages/shared-components/src/room-list/RoomListView/assets/skeleton.svg b/packages/shared-components/src/room-list/RoomListView/assets/skeleton.svg new file mode 100644 index 0000000000..adf56e4ed8 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/assets/skeleton.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/shared-components/src/room-list/RoomListView/index.tsx b/packages/shared-components/src/room-list/RoomListView/index.tsx new file mode 100644 index 0000000000..405b94bf2f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListView/index.tsx @@ -0,0 +1,12 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { RoomListView } from "./RoomListView"; +export type { RoomListViewProps, RoomListViewModel, RoomListSnapshot, RoomListViewActions } from "./RoomListView"; +export { RoomListLoadingSkeleton } from "./RoomListLoadingSkeleton"; +export { RoomListEmptyStateView } from "./RoomListEmptyStateView"; +export type { RoomListEmptyStateViewProps } from "./RoomListEmptyStateView"; diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.module.css b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.module.css new file mode 100644 index 0000000000..c444c8c1cd --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.module.css @@ -0,0 +1,14 @@ +/* + * Copyright 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +/** + * Room list container styles + */ +.roomList { + height: 100%; + width: 100%; +} diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx new file mode 100644 index 0000000000..3ea908cb55 --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Room } from "../RoomListItem/RoomListItem"; +import { VirtualizedRoomListView, type RoomListViewState } from "./VirtualizedRoomListView"; +import type { RoomListSnapshot, RoomListViewActions } from "../RoomListView"; +import { useMockedViewModel } from "../../viewmodel"; +import type { FilterId } from "../RoomListPrimaryFilters"; +import { renderAvatar, createGetRoomItemViewModel, mockRoomIds } from "../story-mocks"; + +type RoomListStoryProps = RoomListSnapshot & RoomListViewActions & { renderAvatar: (room: Room) => React.ReactElement }; + +// Use first 10 room IDs for this story +const storyRoomIds = mockRoomIds.slice(0, 10); + +// Wrapper component that creates a mocked ViewModel +const RoomListWrapper = ({ + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + renderAvatar: renderAvatarProp, + ...rest +}: RoomListStoryProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + }); + + return ( +
+ +
+ ); +}; + +const mockFilterIds: FilterId[] = ["unread", "people"]; + +const defaultRoomListState: RoomListViewState = { + activeRoomIndex: 0, + spaceId: "!space:server", + filterKeys: undefined, +}; + +const meta: Meta = { + title: "Room List/VirtualizedRoomListView", + component: RoomListWrapper, + tags: ["autodocs"], + args: { + isLoadingRooms: false, + isRoomListEmpty: false, + filterIds: mockFilterIds, + activeFilterId: undefined, + roomIds: storyRoomIds, + roomListState: defaultRoomListState, + canCreateRoom: true, + onToggleFilter: fn(), + createChatRoom: fn(), + createRoom: fn(), + getRoomItemViewModel: createGetRoomItemViewModel(storyRoomIds), + updateVisibleRooms: fn(), + renderAvatar, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel-2025?node-id=98-1979&t=vafb4zoYMNLRuAbh-4", + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.test.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.test.tsx new file mode 100644 index 0000000000..23f384554d --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render, screen, fireEvent } from "@test-utils"; +import { VirtuosoMockContext } from "react-virtuoso"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; + +import * as stories from "./VirtualizedRoomListView.stories"; + +const { Default } = composeStories(stories); + +const renderWithMockContext = (component: React.ReactElement): ReturnType => { + return render(component, { + wrapper: ({ children }) => ( + + {children} + + ), + }); +}; + +describe("", () => { + it("renders Default story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("should render the room list listbox", () => { + renderWithMockContext(); + expect(screen.getByRole("listbox", { name: "Room list" })).toBeInTheDocument(); + }); + + it("should render room items", () => { + renderWithMockContext(); + const items = screen.getAllByRole("option"); + expect(items.length).toBeGreaterThan(0); + }); + + it("should mark selected room with aria-selected true", () => { + renderWithMockContext(); + const items = screen.getAllByRole("option"); + // The first item (index 0) should be selected based on Default story (activeRoomIndex: 0) + expect(items[0]).toHaveAttribute("aria-selected", "true"); + }); + + it("should handle focus state correctly", () => { + renderWithMockContext(); + + const listbox = screen.getByRole("listbox", { name: "Room list" }); + fireEvent.focus(listbox); + + const items = screen.getAllByRole("option"); + // First item should have tabIndex 0 (focusable) when list is focused + expect(items[0]).toHaveAttribute("tabIndex", "0"); + }); + + it("should call updateVisibleRooms on render", () => { + renderWithMockContext(); + expect(Default.args.updateVisibleRooms).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx new file mode 100644 index 0000000000..7b27df0f28 --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx @@ -0,0 +1,198 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { useCallback, useMemo, useRef, type JSX, type ReactNode } from "react"; +import { type ScrollIntoViewLocation } from "react-virtuoso"; +import { isEqual } from "lodash"; + +import type { Room } from "../RoomListItem/RoomListItem"; +import { useViewModel } from "../../viewmodel"; +import { _t } from "../../utils/i18n"; +import { VirtualizedList, type VirtualizedListContext } from "../../utils/VirtualizedList"; +import { RoomListItemView } from "../RoomListItem"; +import type { RoomListViewModel } from "../RoomListView"; + +/** + * Filter key type - opaque string type for filter identifiers + */ +export type FilterKey = string; + +/** + * State for the room list data (nested within RoomListSnapshot) + */ +export interface RoomListViewState { + /** Optional active room index for keyboard navigation */ + activeRoomIndex?: number; + /** Space ID for context tracking */ + spaceId?: string; + /** Active filter keys for context tracking */ + filterKeys?: FilterKey[]; +} + +/** + * Props for the VirtualizedRoomListView component + */ +export interface VirtualizedRoomListViewProps { + /** + * The view model containing all room list data and callbacks + */ + vm: RoomListViewModel; + + /** + * Render function for room avatar + * @param room - The opaque Room object from the client + */ + renderAvatar: (room: Room) => ReactNode; + + /** + * Optional callback for keyboard key down events + */ + onKeyDown?: (e: React.KeyboardEvent) => void; +} + +/** Height of a single room list item in pixels */ +const ROOM_LIST_ITEM_HEIGHT = 48; + +/** + * Type for context used in ListView + */ +type Context = { spaceId: string; filterKeys: FilterKey[] | undefined }; + +/** + * Amount to extend the top and bottom of the viewport by. + * From manual testing and user feedback 25 items is reported to be enough to avoid blank space + * when using the mouse wheel, and the trackpad scrolling at a slow to moderate speed where you + * can still see/read the content. Using the trackpad to sling through a large percentage of the + * list quickly will still show blank space. We would likely need to simplify the item content to + * improve this case. + */ +const EXTENDED_VIEWPORT_HEIGHT = 25 * ROOM_LIST_ITEM_HEIGHT; + +/** + * A virtualized list of rooms. + * This component provides efficient rendering of large room lists using virtualization, + * and renders RoomListItemView components for each room. + * + * @example + * ```tsx + * } /> + * ``` + */ +export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: VirtualizedRoomListViewProps): JSX.Element { + const snapshot = useViewModel(vm); + const { roomListState, roomIds } = snapshot; + const activeRoomIndex = roomListState.activeRoomIndex; + const lastSpaceId = useRef(undefined); + const lastFilterKeys = useRef(undefined); + const roomCount = roomIds.length; + + /** + * Callback when the visible range changes + * Notifies the view model which rooms are visible + */ + const rangeChanged = useCallback( + (range: { startIndex: number; endIndex: number }) => { + vm.updateVisibleRooms(range.startIndex, range.endIndex); + }, + [vm], + ); + + /** + * Get the item component for a specific index + * Gets the room's view model and passes it to RoomListItemView + */ + const getItemComponent = useCallback( + ( + index: number, + roomId: string, + context: VirtualizedListContext, + onFocus: (item: string, e: React.FocusEvent) => void, + ): JSX.Element => { + const isSelected = activeRoomIndex === index; + const roomItemVM = vm.getRoomItemViewModel(roomId); + + // Item is focused when the list has focus AND this item's key matches tabIndexKey + // This matches the old RoomList implementation's roving tabindex pattern + const isFocused = context.focused && context.tabIndexKey === roomId; + + return ( + + ); + }, + [activeRoomIndex, roomCount, renderAvatar, vm], + ); + + /** + * Get the key for a room item + * Since we're using virtualization, items are always room ID strings + */ + const getItemKey = useCallback((item: string): string => { + return item; + }, []); + + const context = useMemo( + () => ({ spaceId: roomListState.spaceId || "", filterKeys: roomListState.filterKeys }), + [roomListState.spaceId, roomListState.filterKeys], + ); + + /** + * Determine if we should scroll the active index into view + * This happens when the space or filters change + */ + const scrollIntoViewOnChange = useCallback( + (params: { + context: VirtualizedListContext<{ spaceId: string; filterKeys: FilterKey[] | undefined }>; + }): ScrollIntoViewLocation | null | undefined | false => { + const { spaceId, filterKeys } = params.context.context; + const shouldScrollIndexIntoView = + lastSpaceId.current !== spaceId || !isEqual(lastFilterKeys.current, filterKeys); + lastFilterKeys.current = filterKeys; + lastSpaceId.current = spaceId; + + if (shouldScrollIndexIntoView) { + return { + align: "start", + index: activeRoomIndex || 0, + behavior: "auto", + }; + } + return false; + }, + [activeRoomIndex], + ); + + return ( + true} + rangeChanged={rangeChanged} + onKeyDown={onKeyDown} + increaseViewportBy={{ + bottom: EXTENDED_VIEWPORT_HEIGHT, + top: EXTENDED_VIEWPORT_HEIGHT, + }} + /> + ); +} diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/__snapshots__/VirtualizedRoomListView.test.tsx.snap b/packages/shared-components/src/room-list/VirtualizedRoomListView/__snapshots__/VirtualizedRoomListView.test.tsx.snap new file mode 100644 index 0000000000..85cd5f6d08 --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/__snapshots__/VirtualizedRoomListView.test.tsx.snap @@ -0,0 +1,1277 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders Default story 1`] = ` +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + + + + + +`; diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/index.ts b/packages/shared-components/src/room-list/VirtualizedRoomListView/index.ts new file mode 100644 index 0000000000..da5840ada5 --- /dev/null +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { VirtualizedRoomListView } from "./VirtualizedRoomListView"; +export type { VirtualizedRoomListViewProps, RoomListViewState, FilterKey } from "./VirtualizedRoomListView"; diff --git a/packages/shared-components/src/room-list/story-mocks.tsx b/packages/shared-components/src/room-list/story-mocks.tsx new file mode 100644 index 0000000000..83a8eb1b94 --- /dev/null +++ b/packages/shared-components/src/room-list/story-mocks.tsx @@ -0,0 +1,138 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { fn } from "storybook/test"; + +import type { Room } from "./RoomListItem/RoomListItem"; +import type { RoomListItemSnapshot } from "./RoomListItem"; +import { RoomNotifState } from "./RoomListItem/RoomNotifs"; + +/** + * Mock avatar component for stories + */ +export const mockAvatar = (name: string): React.ReactElement => ( +
+ {name.substring(0, 2).toUpperCase()} +
+); + +/** + * Render avatar function for stories + */ +export const renderAvatar = (room: Room): React.ReactElement => { + // Cast to any to access properties - in real usage, the room object from the SDK will have these + return mockAvatar((room as any)?.name || "Room"); +}; + +/** + * Room names used for mock data + */ +const roomNames = [ + "General", + "Random", + "Engineering", + "Design", + "Product", + "Marketing", + "Sales", + "Support", + "Announcements", + "Off-topic", + "Team Alpha", + "Team Beta", + "Project X", + "Project Y", + "Water Cooler", + "Feedback", + "Ideas", + "Bugs", + "Features", + "Releases", +]; + +/** + * Create a mock room item snapshot for stories + */ +export const createMockRoomSnapshot = (id: string, name: string, index: number): RoomListItemSnapshot => ({ + id, + room: { name }, + name, + isBold: index % 3 === 0, + messagePreview: index % 2 === 0 ? `Last message in ${name}` : undefined, + notification: { + hasAnyNotificationOrActivity: index % 5 === 0, + isUnsentMessage: false, + invited: false, + isMention: index % 5 === 0, + isActivityNotification: false, + isNotification: index % 5 === 0, + hasUnreadCount: index % 5 === 0, + count: index % 5 === 0 ? index : 0, + muted: false, + }, + showMoreOptionsMenu: true, + showNotificationMenu: true, + isFavourite: false, + isLowPriority: false, + canInvite: true, + canCopyRoomLink: true, + canMarkAsRead: false, + canMarkAsUnread: true, + roomNotifState: RoomNotifState.AllMessages, +}); + +/** + * Create a mock getRoomItemViewModel function for stories + */ +export const createGetRoomItemViewModel = (roomIds: string[]): ((roomId: string) => any) => { + const viewModels = new Map(); + roomIds.forEach((roomId, index) => { + const name = roomNames[index % roomNames.length]; + const snapshot = createMockRoomSnapshot(roomId, name, index); + + const mockViewModel = { + getSnapshot: () => snapshot, + subscribe: fn(), + unsubscribe: fn(), + onOpenRoom: fn(), + onMarkAsRead: fn(), + onMarkAsUnread: fn(), + onToggleFavorite: fn(), + onToggleLowPriority: fn(), + onInvite: fn(), + onCopyRoomLink: fn(), + onLeaveRoom: fn(), + onSetRoomNotifState: fn(), + }; + viewModels.set(roomId, mockViewModel); + }); + + return (roomId: string) => viewModels.get(roomId); +}; + +/** + * Mock room IDs for different list sizes + */ +export const mockRoomIds = Array.from({ length: 20 }, (_, i) => `!room${i}:server`); +export const smallListRoomIds = mockRoomIds.slice(0, 5); +export const largeListRoomIds = Array.from({ length: 100 }, (_, i) => `!room${i}:server`); diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx new file mode 100644 index 0000000000..33983bd473 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.stories.tsx @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { HistoryVisibilityBadge } from "./HistoryVisibilityBadge"; + +const meta = { + title: "Room/HistoryVisibilityBadge", + component: HistoryVisibilityBadge, + tags: ["autodocs"], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/IXcnmuaIwtm3F3vBuFCPUp/Room-History-Sharing?node-id=39-10758&t=MKC8KCGCpykDbrcX-1", + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; +export const InvitedHistoryVisibility: Story = { args: { historyVisibility: "invited" } }; +export const JoinedHistoryVisibility: Story = { args: { historyVisibility: "joined" } }; +export const SharedHistoryVisibility: Story = { args: { historyVisibility: "shared" } }; +export const WorldReadableHistoryVisibility: Story = { args: { historyVisibility: "world_readable" } }; diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.test.tsx b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.test.tsx new file mode 100644 index 0000000000..6b8ea1c398 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { describe, expect, it } from "vitest"; +import { render } from "@testing-library/react"; + +import { HistoryVisibilityBadge } from "./HistoryVisibilityBadge.tsx"; + +describe("HistoryVisibilityBadge", () => { + for (const visibility of ["invited", "joined", "shared", "world_readable"]) { + it(`renders the badge for ${visibility}`, () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + } +}); diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.tsx b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.tsx new file mode 100644 index 0000000000..f03e69dd00 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/HistoryVisibilityBadge.tsx @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; +import { Badge } from "@vector-im/compound-web"; +import { + HistoryIcon, + UserProfileSolidIcon, + VisibilityOffIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; + +import { _t } from "../../utils/i18n"; + +interface Props { + /** The history visibility of the room, according to the room state. */ + historyVisibility: "invited" | "joined" | "shared" | "world_readable"; +} + +/** A badge showing the history visibility of a room. */ +export function HistoryVisibilityBadge({ historyVisibility }: Props): JSX.Element | null { + const iconProps = { + color: "var(--cpd-color-icon-info-primary)", + width: "1rem", // 16px at the default font size, per the design + height: "1rem", + }; + switch (historyVisibility) { + case "invited": + case "joined": + return ( + + + {_t("room|history_visibility_badge|private")} + + ); + case "shared": + return ( + + + {_t("room|history_visibility_badge|shared")} + + ); + case "world_readable": + return ( + + + {_t("room|history_visibility_badge|world_readable")} + + ); + default: + return null; + } +} diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/__snapshots__/HistoryVisibilityBadge.test.tsx.snap b/packages/shared-components/src/room/HistoryVisibilityBadge/__snapshots__/HistoryVisibilityBadge.test.tsx.snap new file mode 100644 index 0000000000..e284eb7a51 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/__snapshots__/HistoryVisibilityBadge.test.tsx.snap @@ -0,0 +1,99 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`HistoryVisibilityBadge > renders the badge for invited 1`] = ` +
+ + + + + New members don't see history + +
+`; + +exports[`HistoryVisibilityBadge > renders the badge for joined 1`] = ` +
+ + + + + New members don't see history + +
+`; + +exports[`HistoryVisibilityBadge > renders the badge for shared 1`] = ` +
+ + + + + + New members see history + +
+`; + +exports[`HistoryVisibilityBadge > renders the badge for world_readable 1`] = ` +
+ + + + + + Anyone can see history + +
+`; diff --git a/packages/shared-components/src/room/HistoryVisibilityBadge/index.ts b/packages/shared-components/src/room/HistoryVisibilityBadge/index.ts new file mode 100644 index 0000000000..e8958a9913 --- /dev/null +++ b/packages/shared-components/src/room/HistoryVisibilityBadge/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export * from "./HistoryVisibilityBadge"; diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.module.css b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.module.css new file mode 100644 index 0000000000..b0e85609b6 --- /dev/null +++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.module.css @@ -0,0 +1,11 @@ +.container { + color: var(--cpd-color-text-primary); + svg { + /* Ensure button icons are primary too */ + color: var(--cpd-color-text-primary) !important; + } +} + +.description { + color: var(--cpd-color-text-secondary); +} diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx new file mode 100644 index 0000000000..07b890139e --- /dev/null +++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ +import { type Meta, type StoryFn } from "@storybook/react-vite"; +import React, { type JSX } from "react"; +import { fn } from "storybook/test"; + +import { useMockedViewModel } from "../../viewmodel"; +import { + RoomStatusBarState, + RoomStatusBarView, + type RoomStatusBarViewActions, + type RoomStatusBarViewSnapshot, +} from "./RoomStatusBarView"; + +type RoomStatusBarProps = RoomStatusBarViewSnapshot & RoomStatusBarViewActions; + +const RoomStatusBarViewWrapper = ({ + onResendAllClick, + onDeleteAllClick, + onRetryRoomCreationClick, + onTermsAndConditionsClicked, + ...rest +}: RoomStatusBarProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + onResendAllClick, + onDeleteAllClick, + onRetryRoomCreationClick, + onTermsAndConditionsClicked, + }); + return ; +}; + +export default { + title: "room/RoomStatusBarView", + component: RoomStatusBarViewWrapper, + tags: ["autodocs"], + argTypes: {}, + args: { + onResendAllClick: fn(), + onDeleteAllClick: fn(), + onRetryRoomCreationClick: fn(), + onTermsAndConditionsClicked: fn(), + }, +} as Meta; + +const Template: StoryFn = (args) => ; + +/** + * Rendered when the client has lost connection with the server. + */ +export const WithConnectionLost = Template.bind({}); +WithConnectionLost.args = { + state: RoomStatusBarState.ConnectionLost, +}; + +/** + * Rendered when the client needs the user to consent to some terms and conditions before + * they can perform any room actions. + */ +export const WithConsentLink = Template.bind({}); +WithConsentLink.args = { + state: RoomStatusBarState.NeedsConsent, + consentUri: "#example", +}; + +/** + * Rendered when the server has hit a usage limit and is forbidding the user from performing + * any actions in the room. There is an optional parameter to link to an admin to contact. + */ +export const WithResourceLimit = Template.bind({}); +WithResourceLimit.args = { + state: RoomStatusBarState.ResourceLimited, + resourceLimit: "hs_disabled", + adminContactHref: "#example", +}; + +/** + * Rendered when the client has some unsent messages in the room, stored locally. + */ +export const WithUnsentMessages = Template.bind({}); +WithUnsentMessages.args = { + state: RoomStatusBarState.UnsentMessages, + isResending: false, +}; + +/** + * Rendered when the client has some unsent messages in the room, stored locally and is + * trying to send them. + */ +export const WithUnsentMessagesSending = Template.bind({}); +WithUnsentMessagesSending.args = { + state: RoomStatusBarState.UnsentMessages, + isResending: true, +}; +/** + * Rendered when a local room has failed to be created. + */ +export const WithLocalRoomRetry = Template.bind({}); +WithLocalRoomRetry.args = { + state: RoomStatusBarState.LocalRoomFailed, +}; diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.test.tsx b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.test.tsx new file mode 100644 index 0000000000..58cc6e6e9f --- /dev/null +++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { render } from "@test-utils"; +import { composeStories } from "@storybook/react-vite"; +import userEvent from "@testing-library/user-event"; +import { describe, it, vi, expect } from "vitest"; + +import * as stories from "./RoomStatusBarView.stories.tsx"; + +const { WithConnectionLost, WithConsentLink, WithResourceLimit, WithUnsentMessages, WithLocalRoomRetry } = + composeStories(stories); + +describe("RoomStatusBarView", () => { + it("renders connection lost", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it("renders resource limit error", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it("renders consent link", () => { + const { container, getByRole } = render(); + expect(container).toMatchSnapshot(); + + const button = getByRole("link"); + expect(button.getAttribute("href")).toEqual("#example"); + }); + it("renders unsent messages", async () => { + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + it("renders unsent messages and deletes all", async () => { + const onDeleteAllClick = vi.fn(); + const { container, getByRole } = render(); + expect(container).toMatchSnapshot(); + + const button = getByRole("button", { name: "Delete all" }); + await userEvent.click(button); + expect(onDeleteAllClick).toHaveBeenCalled(); + }); + it("renders unsent messages and resends all", async () => { + const onResendAllClick = vi.fn(); + const { container, getByRole } = render(); + expect(container).toMatchSnapshot(); + + const button = getByRole("button", { name: "Retry all" }); + await userEvent.click(button); + expect(onResendAllClick).toHaveBeenCalled(); + }); + it("renders local room error", async () => { + const onRetryRoomCreationClick = vi.fn(); + const { container, getByRole } = render( + , + ); + expect(container).toMatchSnapshot(); + + const button = getByRole("button", { name: "Retry" }); + await userEvent.click(button); + expect(onRetryRoomCreationClick).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx new file mode 100644 index 0000000000..9746321616 --- /dev/null +++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { useCallback, useId, type JSX } from "react"; +import { RestartIcon, DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Button, InlineSpinner, Text } from "@vector-im/compound-web"; + +import styles from "./RoomStatusBarView.module.css"; +import { type ViewModel, useViewModel } from "../../viewmodel"; +import { useI18n } from "../../utils/i18nContext"; +import { Banner } from "../../composer/Banner"; +export interface RoomStatusBarViewActions { + /** + * Called when the user clicks on the 'resend all' button in the 'unsent messages' bar. + */ + onResendAllClick?: () => Promise; + + /** + * Called when the user clicks on the 'cancel all' button in the 'unsent messages' bar. + */ + onDeleteAllClick?: () => void; + + /** + * Called when the user clicks on the 'Retry' button in the 'failed to start chat' bar. + */ + onRetryRoomCreationClick?: () => void; + + /** + * Called when the user clicks on the 'Review Terms and Conditions' button. + */ + onTermsAndConditionsClicked?: () => void; +} + +export const RoomStatusBarState = { + /** + * Connectivity to the homeserver has been lost. The user can not take any actions + * until the connection is restored. + */ + ConnectionLost: "ConnectionLost", + /** + * The homeserver has indiciated the user needs to consent to the Terms and Conditions + * before they can send a message. + */ + NeedsConsent: "NeedsConsent", + /** + * The homeserver has indiciated that messages can not be sent due to a resource limit + * being reached. The user may use the given admin contact details. + */ + ResourceLimited: "ResourceLimited", + /** + * There are messages stored locally that previously failed to send that the user + * may now retry or delete. + */ + UnsentMessages: "UnsentMessages", + /** + * There was an error creating a room. The user may retry creation. + */ + LocalRoomFailed: "LocalRoomFailed", +} as const; + +export interface RoomStatusBarNotVisible { + state: null; +} + +export interface RoomStatusBarNoConnection { + state: "ConnectionLost"; +} + +export interface RoomStatusBarConsentState { + state: "NeedsConsent"; + consentUri: string; +} + +export interface RoomStatusBarResourceLimitedState { + state: "ResourceLimited"; + resourceLimit: "monthly_active_user" | "hs_disabled" | string; + adminContactHref?: string; +} + +export interface RoomStatusBarUnsentMessagesState { + state: "UnsentMessages"; + isResending: boolean; +} +export interface RoomStatusBarLocalRoomError { + state: "LocalRoomFailed"; +} + +export type RoomStatusBarViewSnapshot = + | RoomStatusBarNoConnection + | RoomStatusBarConsentState + | RoomStatusBarResourceLimitedState + | RoomStatusBarUnsentMessagesState + | RoomStatusBarLocalRoomError + | RoomStatusBarNotVisible; + +/** + * The view model for RoomStatusBarView. + */ +export type RoomStatusBarViewModel = ViewModel & RoomStatusBarViewActions; + +interface RoomStatusBarViewProps { + /** + * The view model for the banner. + */ + vm: RoomStatusBarViewModel; +} + +/** + * A component to alert to a failure in the context of a room. + * + * @example + * ```tsx + * + * ``` + */ +export function RoomStatusBarView({ vm }: Readonly): JSX.Element | null { + const { translate: _t } = useI18n(); + const snapshot = useViewModel(vm); + const bannerTitleId = useId(); + + const deleteAllClick = useCallback>( + (ev) => { + ev.preventDefault(); + vm.onDeleteAllClick?.(); + }, + [vm], + ); + + const resendClick = useCallback>( + (ev) => { + ev.preventDefault(); + void vm.onResendAllClick?.(); + }, + [vm], + ); + + const retryRoomCreationClick = useCallback>( + (ev) => { + ev.preventDefault(); + vm.onRetryRoomCreationClick?.(); + }, + [vm], + ); + + const termsAndConditionsClicked = useCallback>(() => { + // Allow the link to go through. + vm.onTermsAndConditionsClicked?.(); + }, [vm]); + + if (snapshot.state === null) { + // Nothing to show! + return null; + } + + switch (snapshot.state) { + case RoomStatusBarState.ConnectionLost: + return ( + +
+ + {_t("room|status_bar|server_connectivity_lost_title")} + + + {_t("room|status_bar|server_connectivity_lost_description")} + +
+
+ ); + case RoomStatusBarState.NeedsConsent: + return ( + + {_t("terms|tac_button")} + + } + > +
+ + {_t("room|status_bar|requires_consent_agreement_title")} + +
+
+ ); + case RoomStatusBarState.ResourceLimited: + return ( + + Contact admin + + ) + } + > +
+ + {{ + monthly_active_user: _t("room|status_bar|monthly_user_limit_reached_title"), + hs_disabled: _t("room|status_bar|homeserver_blocked_title"), + }[snapshot.resourceLimit] || _t("room|status_bar|exceeded_resource_limit_title")} + + + {_t("room|status_bar|exceeded_resource_limit_description")} + +
+
+ ); + case RoomStatusBarState.LocalRoomFailed: + return ( + + {_t("action|retry")} + + } + > + + {_t("room|status_bar|failed_to_create_room_title")} + + + ); + case RoomStatusBarState.UnsentMessages: + return ( + + ) : ( + <> + {vm.onDeleteAllClick && ( + + )} + {vm.onResendAllClick && ( + + )} + + ) + } + aria-labelledby={bannerTitleId} + > +
+ + {_t("room|status_bar|some_messages_not_sent")} + + + {_t("room|status_bar|select_messages_to_retry")} + +
+
+ ); + default: + // We should never get into this state. + return null; + } +} diff --git a/packages/shared-components/src/room/RoomStatusBar/__snapshots__/RoomStatusBarView.test.tsx.snap b/packages/shared-components/src/room/RoomStatusBar/__snapshots__/RoomStatusBarView.test.tsx.snap new file mode 100644 index 0000000000..71bf744b3b --- /dev/null +++ b/packages/shared-components/src/room/RoomStatusBar/__snapshots__/RoomStatusBarView.test.tsx.snap @@ -0,0 +1,520 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`RoomStatusBarView > renders connection lost 1`] = ` +
+ +`; + +exports[`RoomStatusBarView > renders consent link 1`] = ` +
+ +
+`; + +exports[`RoomStatusBarView > renders local room error 1`] = ` +
+ +
+`; + +exports[`RoomStatusBarView > renders resource limit error 1`] = ` +
+ +
+`; + +exports[`RoomStatusBarView > renders unsent messages 1`] = ` +
+ +
+`; + +exports[`RoomStatusBarView > renders unsent messages and deletes all 1`] = ` +
+ +
+`; + +exports[`RoomStatusBarView > renders unsent messages and resends all 1`] = ` +
+ +
+`; diff --git a/packages/shared-components/src/composer/HistoryVisibleBannerView/index.ts b/packages/shared-components/src/room/RoomStatusBar/index.ts similarity index 83% rename from packages/shared-components/src/composer/HistoryVisibleBannerView/index.ts rename to packages/shared-components/src/room/RoomStatusBar/index.ts index 96bf208bea..43f1bfebc2 100644 --- a/packages/shared-components/src/composer/HistoryVisibleBannerView/index.ts +++ b/packages/shared-components/src/room/RoomStatusBar/index.ts @@ -5,4 +5,4 @@ * Please see LICENSE files in the repository root for full details. */ -export * from "./HistoryVisibleBannerView"; +export * from "./RoomStatusBarView"; diff --git a/packages/shared-components/src/test/setupTests.ts b/packages/shared-components/src/test/setupTests.ts index 43ffc0c071..9b048d272b 100644 --- a/packages/shared-components/src/test/setupTests.ts +++ b/packages/shared-components/src/test/setupTests.ts @@ -5,18 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import fetchMock from "fetch-mock-jest"; +import fetchMock from "@fetch-mock/vitest"; +import { cleanup } from "@test-utils"; +import { afterEach } from "vitest"; import { setLanguage } from "../../src/utils/i18n"; -import en from "../../../../src/i18n/strings/en_EN.json"; +import en from "../i18n/strings/en_EN.json"; -export function setupLanguageMock(): void { +function setupLanguageMock(): void { fetchMock - .get("/i18n/languages.json", { + .get("end:/i18n/languages.json", { en: "en_EN.json", }) .get("end:en_EN.json", en); } setupLanguageMock(); +fetchMock.mockGlobal(); setLanguage("en"); + +afterEach(() => { + cleanup(); +}); diff --git a/packages/shared-components/src/test/utils/jest-matrix-react.tsx b/packages/shared-components/src/test/utils/index.tsx similarity index 96% rename from packages/shared-components/src/test/utils/jest-matrix-react.tsx rename to packages/shared-components/src/test/utils/index.tsx index d610d87211..bcbd0820da 100644 --- a/packages/shared-components/src/test/utils/jest-matrix-react.tsx +++ b/packages/shared-components/src/test/utils/index.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -// Copied from element-web/test/test-utils because, seemingly, if we +// Copied from element-web/test/@test-utils because, seemingly, if we // set that as the modules directory to use it directly, it fails to // actually put the right thing in the context somehow. diff --git a/packages/shared-components/src/utils/FormattingUtils.stories.tsx b/packages/shared-components/src/utils/FormattingUtils.stories.tsx new file mode 100644 index 0000000000..e3bd0040e3 --- /dev/null +++ b/packages/shared-components/src/utils/FormattingUtils.stories.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { Markdown } from "@storybook/addon-docs/blocks"; + +import type { Meta } from "@storybook/react-vite"; +import formatBytesDoc from "../../typedoc/functions/formatBytes.md?raw"; +import formatSecondsDoc from "../../typedoc/functions/formatSeconds.md?raw"; + +const meta = { + title: "utils/FormattingUtils", + parameters: { + docs: { + page: () => ( + <> +

Formatting Utilities

+

A collection of utility functions for formatting data into human-readable strings.

+ +
+

formatBytes

+ {formatBytesDoc} + +
+

formatSeconds

+ {formatSecondsDoc} + + ), + }, + }, + tags: ["autodocs", "skip-test"], +} satisfies Meta; + +export default meta; + +// Docs-only story - renders nothing but triggers autodocs +export const Docs = { + render: () => null, +}; diff --git a/packages/shared-components/src/utils/I18nApi.test.ts b/packages/shared-components/src/utils/I18nApi.test.ts index 2b3431f07c..a9fd287c4f 100644 --- a/packages/shared-components/src/utils/I18nApi.test.ts +++ b/packages/shared-components/src/utils/I18nApi.test.ts @@ -5,14 +5,15 @@ * Please see LICENSE files in the repository root for full details. */ -import { type TranslationKey } from "../i18nKeys"; +import { describe, it, expect } from "vitest"; + import { I18nApi } from "./I18nApi"; describe("I18nApi", () => { it("can register a translation and use it", () => { const i18n = new I18nApi(); i18n.register({ - "hello.world": { + ["hello.world" as TranslationKey]: { en: "Hello, World!", }, }); diff --git a/packages/shared-components/src/utils/I18nApi.ts b/packages/shared-components/src/utils/I18nApi.ts index 20d641f5ce..df474e2775 100644 --- a/packages/shared-components/src/utils/I18nApi.ts +++ b/packages/shared-components/src/utils/I18nApi.ts @@ -9,7 +9,6 @@ import { type I18nApi as II18nApi, type Variables, type Translations } from "@el import { humanizeTime } from "./humanize"; import { _t, getLocale, registerTranslations } from "./i18n"; -import { type TranslationKey } from "../i18nKeys"; export class I18nApi implements II18nApi { /** @@ -24,10 +23,11 @@ export class I18nApi implements II18nApi { */ public register(translations: Partial): void { const langs: Record> = {}; + for (const key in translations) { - for (const lang in translations[key]) { + for (const lang in translations[key as keyof Translations]) { langs[lang] = langs[lang] || {}; - langs[lang][key] = translations[key][lang]; + langs[lang][key] = translations[key as keyof Translations]![lang]; } } diff --git a/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.stories.tsx b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.stories.tsx new file mode 100644 index 0000000000..a20fe188a3 --- /dev/null +++ b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.stories.tsx @@ -0,0 +1,52 @@ +/* +Copyright 2026 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { VirtualizedList, type IVirtualizedListProps, type VirtualizedListContext } from "./VirtualizedList"; + +interface SimpleItem { + id: string; + label: string; +} + +const items: SimpleItem[] = Array.from({ length: 50 }, (_, i) => ({ + id: `item-${i}`, + label: `Item ${i + 1}`, +})); + +const meta = { + title: "Utils/VirtualizedList", + component: VirtualizedList, + args: { + items, + getItemComponent: ( + _index: number, + item: SimpleItem, + context: VirtualizedListContext, + onFocus: (item: SimpleItem, e: React.FocusEvent) => void, + ) => ( +
onFocus(item, e)} + > + {item.label} +
+ ), + isItemFocusable: () => true, + getItemKey: (item) => item.id, + style: { height: "400px" }, + }, +} satisfies Meta>; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/test/unit-tests/components/views/utils/ListView-test.tsx b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.test.tsx similarity index 72% rename from test/unit-tests/components/views/utils/ListView-test.tsx rename to packages/shared-components/src/utils/VirtualizedList/VirtualizedList.test.tsx index 0cae681f73..4dd5125b6f 100644 --- a/test/unit-tests/components/views/utils/ListView-test.tsx +++ b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.test.tsx @@ -1,15 +1,24 @@ /* -Copyright 2024 New Vector Ltd. + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, screen, fireEvent } from "jest-matrix-react"; +import React, { type PropsWithChildren } from "react"; +import { render, screen, fireEvent } from "@test-utils"; import { VirtuosoMockContext } from "react-virtuoso"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { ListView, type IListViewProps } from "../../../../../src/components/utils/ListView"; +import { VirtualizedList, type IVirtualizedListProps } from "./VirtualizedList"; + +const expectTabIndex = (element: Element, expected: string): void => { + expect(element.getAttribute("tabindex")).toBe(expected); +}; + +const expectAttribute = (element: Element, attr: string, expected: string): void => { + expect(element.getAttribute(attr)).toBe(expected); +}; interface TestItem { id: string; @@ -20,9 +29,9 @@ interface TestItem { const SEPARATOR_ITEM = "SEPARATOR" as const; type TestItemWithSeparator = TestItem | typeof SEPARATOR_ITEM; -describe("ListView", () => { - const mockGetItemComponent = jest.fn(); - const mockIsItemFocusable = jest.fn(); +describe("VirtualizedList", () => { + const mockGetItemComponent = vi.fn(); + const mockIsItemFocusable = vi.fn(); const defaultItems: TestItemWithSeparator[] = [ { id: "1", name: "Item 1" }, @@ -31,22 +40,26 @@ describe("ListView", () => { { id: "3", name: "Item 3" }, ]; - const defaultProps: IListViewProps = { + const defaultProps: IVirtualizedListProps = { items: defaultItems, getItemComponent: mockGetItemComponent, isItemFocusable: mockIsItemFocusable, getItemKey: (item) => (typeof item === "string" ? item : item.id), }; - const getListViewComponent = (props: Partial> = {}) => { + const getListComponent = ( + props: Partial> = {}, + ): React.JSX.Element => { const mergedProps = { ...defaultProps, ...props }; - return ; + return ; }; - const renderListViewWithHeight = (props: Partial> = {}) => { + const renderListWithHeight = ( + props: Partial> = {}, + ): ReturnType => { const mergedProps = { ...defaultProps, ...props }; - return render(getListViewComponent(mergedProps), { - wrapper: ({ children }) => ( + return render(getListComponent(mergedProps), { + wrapper: ({ children }: PropsWithChildren) => ( <>{children} @@ -55,12 +68,12 @@ describe("ListView", () => { }; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); mockGetItemComponent.mockImplementation((index: number, item: TestItemWithSeparator, context: any) => { const itemKey = typeof item === "string" ? item : item.id; const isFocused = context.tabIndexKey === itemKey; return ( -
+
{item === SEPARATOR_ITEM ? "---" : (item as TestItem).name}
); @@ -69,24 +82,24 @@ describe("ListView", () => { }); afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); describe("Rendering", () => { - it("should render the ListView component", () => { - renderListViewWithHeight(); - expect(screen.getByRole("grid")).toBeInTheDocument(); + it("should render the VirtualizedList component", () => { + renderListWithHeight(); + expect(screen.getByRole("grid")).toBeDefined(); }); it("should render with empty items array", () => { - renderListViewWithHeight({ items: [] }); - expect(screen.getByRole("grid")).toBeInTheDocument(); + renderListWithHeight({ items: [] }); + expect(screen.getByRole("grid")).toBeDefined(); }); }); describe("Keyboard Navigation", () => { it("should handle ArrowDown key navigation", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); fireEvent.focus(container); @@ -94,13 +107,13 @@ describe("ListView", () => { // ArrowDown should skip the non-focusable item at index 1 and go to index 2 const items = container.querySelectorAll(".mx_item"); - expect(items[2]).toHaveAttribute("tabindex", "0"); - expect(items[0]).toHaveAttribute("tabindex", "-1"); - expect(items[1]).toHaveAttribute("tabindex", "-1"); + expectTabIndex(items[2], "0"); + expectTabIndex(items[0], "-1"); + expectTabIndex(items[1], "-1"); }); it("should handle ArrowUp key navigation", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); // First focus and navigate down to second item @@ -112,12 +125,12 @@ describe("ListView", () => { // Verify focus moved back to first item const items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "0"); - expect(items[1]).toHaveAttribute("tabindex", "-1"); + expectTabIndex(items[0], "0"); + expectTabIndex(items[1], "-1"); }); it("should handle Home key navigation", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); // First focus and navigate to a later item @@ -130,15 +143,15 @@ describe("ListView", () => { // Verify focus moved to first item const items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "0"); + expectTabIndex(items[0], "0"); // Check that other items are not focused for (let i = 1; i < items.length; i++) { - expect(items[i]).toHaveAttribute("tabindex", "-1"); + expectTabIndex(items[i], "-1"); } }); it("should handle End key navigation", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); // First focus on the list (starts at first item) @@ -151,15 +164,15 @@ describe("ListView", () => { const items = container.querySelectorAll(".mx_item"); // Should focus on the last visible item const lastIndex = items.length - 1; - expect(items[lastIndex]).toHaveAttribute("tabindex", "0"); + expectTabIndex(items[lastIndex], "0"); // Check that other items are not focused for (let i = 0; i < lastIndex; i++) { - expect(items[i]).toHaveAttribute("tabindex", "-1"); + expectTabIndex(items[i], "-1"); } }); it("should handle PageDown key navigation", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); // First focus on the list (starts at first item) @@ -172,12 +185,12 @@ describe("ListView", () => { const items = container.querySelectorAll(".mx_item"); // PageDown should move to the last visible item since we only have 4 items const lastIndex = items.length - 1; - expect(items[lastIndex]).toHaveAttribute("tabindex", "0"); - expect(items[0]).toHaveAttribute("tabindex", "-1"); + expectTabIndex(items[lastIndex], "0"); + expectTabIndex(items[0], "-1"); }); it("should handle PageUp key navigation", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); // First focus and navigate to last item to have something to page up from @@ -190,56 +203,56 @@ describe("ListView", () => { // Verify focus moved up const items = container.querySelectorAll(".mx_item"); // PageUp should move back to the first item since we only have 4 items - expect(items[0]).toHaveAttribute("tabindex", "0"); + expectTabIndex(items[0], "0"); const lastIndex = items.length - 1; - expect(items[lastIndex]).toHaveAttribute("tabindex", "-1"); + expectTabIndex(items[lastIndex], "-1"); }); it("should not handle keyboard navigation when modifier keys are pressed", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); fireEvent.focus(container); // Store initial state - first item should be focused const initialItems = container.querySelectorAll(".mx_item"); - expect(initialItems[0]).toHaveAttribute("tabindex", "0"); - expect(initialItems[2]).toHaveAttribute("tabindex", "-1"); + expectTabIndex(initialItems[0], "0"); + expectTabIndex(initialItems[2], "-1"); // Test ArrowDown with Ctrl modifier - should NOT navigate fireEvent.keyDown(container, { code: "ArrowDown", ctrlKey: true }); let items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "0"); // Should still be on first item - expect(items[2]).toHaveAttribute("tabindex", "-1"); // Should not have moved to third item + expectTabIndex(items[0], "0"); // Should still be on first item + expectTabIndex(items[2], "-1"); // Should not have moved to third item // Test ArrowDown with Alt modifier - should NOT navigate fireEvent.keyDown(container, { code: "ArrowDown", altKey: true }); items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "0"); // Should still be on first item - expect(items[2]).toHaveAttribute("tabindex", "-1"); // Should not have moved to third item + expectTabIndex(items[0], "0"); // Should still be on first item + expectTabIndex(items[2], "-1"); // Should not have moved to third item // Test ArrowDown with Shift modifier - should NOT navigate fireEvent.keyDown(container, { code: "ArrowDown", shiftKey: true }); items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "0"); // Should still be on first item - expect(items[2]).toHaveAttribute("tabindex", "-1"); // Should not have moved to third item + expectTabIndex(items[0], "0"); // Should still be on first item + expectTabIndex(items[2], "-1"); // Should not have moved to third item // Test ArrowDown with Meta/Cmd modifier - should NOT navigate fireEvent.keyDown(container, { code: "ArrowDown", metaKey: true }); items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "0"); // Should still be on first item - expect(items[2]).toHaveAttribute("tabindex", "-1"); // Should not have moved to third item + expectTabIndex(items[0], "0"); // Should still be on first item + expectTabIndex(items[2], "-1"); // Should not have moved to third item // Test normal ArrowDown without modifiers - SHOULD navigate fireEvent.keyDown(container, { code: "ArrowDown" }); items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "-1"); // Should have moved from first item - expect(items[2]).toHaveAttribute("tabindex", "0"); // Should have moved to third item (skipping separator) + expectTabIndex(items[0], "-1"); // Should have moved from first item + expectTabIndex(items[2], "0"); // Should have moved to third item (skipping separator) }); it("should skip non-focusable items when navigating down", async () => { @@ -257,7 +270,7 @@ describe("ListView", () => { return (item as TestItem).isFocusable !== false; }); - renderListViewWithHeight({ items: mixedItems }); + renderListWithHeight({ items: mixedItems }); const container = screen.getByRole("grid"); fireEvent.focus(container); @@ -266,9 +279,9 @@ describe("ListView", () => { // Verify it skipped the non-focusable item at index 1 // and went directly to the focusable item at index 2 const items = container.querySelectorAll(".mx_item"); - expect(items[2]).toHaveAttribute("tabindex", "0"); // Item 3 is focused - expect(items[0]).toHaveAttribute("tabindex", "-1"); // Item 1 is not focused - expect(items[1]).toHaveAttribute("tabindex", "-1"); // Item 2 (non-focusable) is not focused + expectTabIndex(items[2], "0"); // Item 3 is focused + expectTabIndex(items[0], "-1"); // Item 1 is not focused + expectTabIndex(items[1], "-1"); // Item 2 (non-focusable) is not focused }); it("should skip non-focusable items when navigating up", () => { @@ -284,7 +297,7 @@ describe("ListView", () => { return (item as TestItem).isFocusable !== false; }); - renderListViewWithHeight({ items: mixedItems }); + renderListWithHeight({ items: mixedItems }); const container = screen.getByRole("grid"); // Focus and go to last item first, then navigate up @@ -295,14 +308,14 @@ describe("ListView", () => { // Verify it skipped non-focusable items // and went to the first focusable item const items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "0"); // Item 1 is focused - expect(items[3]).toHaveAttribute("tabindex", "-1"); // Item 3 is not focused anymore + expectTabIndex(items[0], "0"); // Item 1 is focused + expectTabIndex(items[3], "-1"); // Item 3 is not focused anymore }); }); describe("Focus Management", () => { it("should focus first item when list gains focus for the first time", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); // Initial focus should go to first item @@ -310,15 +323,15 @@ describe("ListView", () => { // Verify first item gets focus const items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "0"); + expectTabIndex(items[0], "0"); // Other items should not be focused for (let i = 1; i < items.length; i++) { - expect(items[i]).toHaveAttribute("tabindex", "-1"); + expectTabIndex(items[i], "-1"); } }); it("should restore last focused item when regaining focus", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); // Focus and navigate to simulate previous usage @@ -327,7 +340,7 @@ describe("ListView", () => { // Verify item 2 is focused let items = container.querySelectorAll(".mx_item"); - expect(items[2]).toHaveAttribute("tabindex", "0"); // ArrowDown skips to item 2 + expectTabIndex(items[2], "0"); // ArrowDown skips to item 2 // Simulate blur by focusing elsewhere fireEvent.blur(container); @@ -337,11 +350,11 @@ describe("ListView", () => { // Verify focus is restored to the previously focused item items = container.querySelectorAll(".mx_item"); - expect(items[2]).toHaveAttribute("tabindex", "0"); // Should still be item 2 + expectTabIndex(items[2], "0"); // Should still be item 2 }); it("should not interfere with focus if item is already focused", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); // Focus once @@ -350,7 +363,7 @@ describe("ListView", () => { // Focus again when already focused fireEvent.focus(container); - expect(container).toBeInTheDocument(); + expect(container).toBeDefined(); }); it("should not scroll to top when clicking an item after manual scroll", () => { @@ -360,7 +373,7 @@ describe("ListView", () => { name: `Item ${i}`, })); - const mockOnClick = jest.fn(); + const mockOnClick = vi.fn(); mockGetItemComponent.mockImplementation( ( @@ -376,7 +389,13 @@ describe("ListView", () => { className="mx_item" data-testid={`row-${index}`} tabIndex={isFocused ? 0 : -1} + role="button" onClick={() => mockOnClick(item)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + mockOnClick(item); + } + }} onFocus={(e) => onFocus(item, e)} > {item === SEPARATOR_ITEM ? "---" : (item as TestItem).name} @@ -385,7 +404,7 @@ describe("ListView", () => { }, ); - const { container } = renderListViewWithHeight({ items: largerItems }); + const { container } = renderListWithHeight({ items: largerItems }); const listContainer = screen.getByRole("grid"); // Step 1: Focus the list initially (this sets tabIndexKey to first item: "item-0") @@ -393,8 +412,8 @@ describe("ListView", () => { // Verify first item is focused initially and tabIndexKey is set to first item let items = container.querySelectorAll(".mx_item"); - expect(items[0]).toHaveAttribute("tabindex", "0"); - expect(items[0]).toHaveAttribute("data-testid", "row-0"); + expectTabIndex(items[0], "0"); + expectAttribute(items[0], "data-testid", "row-0"); // Step 2: Simulate manual scrolling (mouse wheel, scroll bar drag, etc.) // This changes which items are visible but DOES NOT change tabIndexKey @@ -426,7 +445,7 @@ describe("ListView", () => { // With the fix applied: the clicked item should become focused (tabindex="0") // This validates that the fix prevents unwanted scrolling back to the top - expect(clickTargetItem).toHaveAttribute("tabindex", "0"); + expectTabIndex(clickTargetItem, "0"); // The key validation: ensure we haven't scrolled back to the top // item-0 should still not be visible (if the fix is working) @@ -437,18 +456,18 @@ describe("ListView", () => { describe("Accessibility", () => { it("should set correct ARIA attributes", () => { - renderListViewWithHeight(); + renderListWithHeight(); const container = screen.getByRole("grid"); - expect(container).toHaveAttribute("role", "grid"); - expect(container).toHaveAttribute("aria-rowcount", "4"); - expect(container).toHaveAttribute("aria-colcount", "1"); + expectAttribute(container, "role", "grid"); + expectAttribute(container, "aria-rowcount", "4"); + expectAttribute(container, "aria-colcount", "1"); }); it("should update aria-rowcount when items change", () => { - const { rerender } = renderListViewWithHeight(); + const { rerender } = renderListWithHeight(); let container = screen.getByRole("grid"); - expect(container).toHaveAttribute("aria-rowcount", "4"); + expectAttribute(container, "aria-rowcount", "4"); // Update with fewer items const fewerItems = [ @@ -456,21 +475,21 @@ describe("ListView", () => { { id: "2", name: "Item 2" }, ]; rerender( - getListViewComponent({ + getListComponent({ ...defaultProps, items: fewerItems, }), ); container = screen.getByRole("grid"); - expect(container).toHaveAttribute("aria-rowcount", "2"); + expectAttribute(container, "aria-rowcount", "2"); }); it("should handle custom ARIA label", () => { - renderListViewWithHeight({ "aria-label": "Custom list label" }); + renderListWithHeight({ "aria-label": "Custom list label" }); const container = screen.getByRole("grid"); - expect(container).toHaveAttribute("aria-label", "Custom list label"); + expectAttribute(container, "aria-label", "Custom list label"); }); }); }); diff --git a/src/components/utils/ListView.tsx b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx similarity index 79% rename from src/components/utils/ListView.tsx rename to packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx index e35f7e2e47..adea593d07 100644 --- a/src/components/utils/ListView.tsx +++ b/packages/shared-components/src/utils/VirtualizedList/VirtualizedList.tsx @@ -1,19 +1,39 @@ /* -Copyright 2025 New Vector Ltd. + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useRef, type JSX, useCallback, useEffect, useState } from "react"; +import React, { useRef, type JSX, useCallback, useEffect, useState, useMemo } from "react"; import { type VirtuosoHandle, type ListRange, Virtuoso, type VirtuosoProps } from "react-virtuoso"; -import { isModifiedKeyEvent, Key } from "../../Keyboard"; +/** + * Keyboard key codes + */ +export const Key = { + ARROW_UP: "ArrowUp", + ARROW_DOWN: "ArrowDown", + HOME: "Home", + END: "End", + PAGE_UP: "PageUp", + PAGE_DOWN: "PageDown", + ENTER: "Enter", + SPACE: "Space", +} as const; + +/** + * Check if a keyboard event includes modifier keys + */ +export function isModifiedKeyEvent(event: React.KeyboardEvent): boolean { + return event.ctrlKey || event.metaKey || event.shiftKey || event.altKey; +} + /** * Context object passed to each list item containing the currently focused key * and any additional context data from the parent component. */ -export type ListContext = { +export type VirtualizedListContext = { /** The key of item that should have tabIndex == 0 */ tabIndexKey?: string; /** Whether an item in the list is currently focused */ @@ -22,8 +42,8 @@ export type ListContext = { context: Context; }; -export interface IListViewProps extends Omit< - VirtuosoProps>, +export interface IVirtualizedListProps extends Omit< + VirtuosoProps>, "data" | "itemContent" | "context" > { /** @@ -43,13 +63,13 @@ export interface IListViewProps extends Omit< getItemComponent: ( index: number, item: Item, - context: ListContext, + context: VirtualizedListContext, onFocus: (item: Item, e: React.FocusEvent) => void, ) => JSX.Element; /** * Optional additional context data to pass to each rendered item. - * This will be available in the ListContext passed to getItemComponent. + * This will be available in the VirtualizedListContext passed to getItemComponent. */ context?: Context; @@ -66,16 +86,37 @@ export interface IListViewProps extends Omit< * @return The key to use for focusing the item */ getItemKey: (item: Item) => string; + /** * Callback function to handle key down events on the list container. - * ListView handles keyboard navigation for focus(up, down, home, end, pageUp, pageDown) + * List handles keyboard navigation for focus(up, down, home, end, pageUp, pageDown) * and stops propagation otherwise the event bubbles and this callback is called for the use of the parent. * @param e - The keyboard event * @returns */ onKeyDown?: (e: React.KeyboardEvent) => void; + + /** + * Optional total count of items (for virtualization with partial data loading). + * If provided, this will be used instead of items.length for the total count. + */ + totalCount?: number; + + /** + * Optional callback when the visible range of items changes. + * Useful for loading data on-demand as the user scrolls. + * @param range - The new visible range with startIndex and endIndex + */ + rangeChanged?: (range: ListRange) => void; } +/** + * Utility type for the prop scrollIntoViewOnChange allowing it to be memoised by a caller without repeating types + */ +export type ScrollIntoViewOnChange = NonNullable< + VirtuosoProps>["scrollIntoViewOnChange"] +>; + /** * A generic virtualized list component built on top of react-virtuoso. * Provides keyboard navigation and virtualized rendering for performance with large lists. @@ -83,9 +124,19 @@ export interface IListViewProps extends Omit< * @template Item - The type of data items in the list * @template Context - The type of additional context data passed to items */ -export function ListView(props: IListViewProps): React.ReactElement { +export function VirtualizedList(props: IVirtualizedListProps): React.ReactElement { // Extract our custom props to avoid conflicts with Virtuoso props - const { items, getItemComponent, isItemFocusable, getItemKey, context, onKeyDown, ...virtuosoProps } = props; + const { + items, + getItemComponent, + isItemFocusable, + getItemKey, + context, + onKeyDown, + totalCount, + rangeChanged, + ...virtuosoProps + } = props; /** Reference to the Virtuoso component for programmatic scrolling */ const virtuosoHandleRef = useRef(null); /** Reference to the DOM element containing the virtualized list */ @@ -206,11 +257,11 @@ export function ListView(props: IListViewProps(props: IListViewProps { // If one of the item components has been focused directly, set the focused and tabIndex state - // and stop propagation so the ListViews onFocus doesn't also handle it. + // and stop propagation so the List's onFocus doesn't also handle it. const key = getItemKey(item); setIsFocused(true); setTabIndexKey(key); @@ -249,10 +300,11 @@ export function ListView(props: IListViewProps): JSX.Element => + (index: number, item: Item, context: VirtualizedListContext): JSX.Element => getItemComponent(index, item, context, onFocusForGetItemComponent), [getItemComponent, onFocusForGetItemComponent], ); + /** * Handles focus events on the list. * Sets the focused state and scrolls to the focused item if it is not currently visible. @@ -286,11 +338,23 @@ export function ListView(props: IListViewProps = { - tabIndexKey: tabIndexKey, - focused: isFocused, - context: props.context || ({} as Context), - }; + const listContext: VirtualizedListContext = useMemo( + () => ({ + tabIndexKey: tabIndexKey, + focused: isFocused, + context: props.context || ({} as Context), + }), + [tabIndexKey, isFocused, props.context], + ); + + // Combine internal range tracking with optional external callback + const handleRangeChanged = useCallback( + (range: ListRange) => { + setVisibleRange(range); + rangeChanged?.(range); + }, + [rangeChanged], + ); return ( (props: IListViewProps ( + <> +

humanize

+ {humanizeTimeDoc} + + ), + }, + }, + tags: ["autodocs", "skip-test"], +} satisfies Meta; + +export default meta; + +// Docs-only story - renders nothing but triggers autodocs +export const Docs = { + render: () => null, +}; diff --git a/packages/shared-components/src/utils/humanize.test.ts b/packages/shared-components/src/utils/humanize.test.ts index 1c07dd3d04..9cef35bab3 100644 --- a/packages/shared-components/src/utils/humanize.test.ts +++ b/packages/shared-components/src/utils/humanize.test.ts @@ -5,13 +5,15 @@ * Please see LICENSE files in the repository root for full details. */ +import { describe, it, beforeAll, vi, expect } from "vitest"; + import { humanizeTime } from "./humanize"; describe("humanizeTime", () => { const now = new Date("2025-08-01T12:00:00Z").getTime(); beforeAll(() => { - jest.useFakeTimers().setSystemTime(now); + vi.useFakeTimers().setSystemTime(now); }); it.each([ diff --git a/packages/shared-components/src/utils/i18n.test.ts b/packages/shared-components/src/utils/i18n.test.ts index a578f7bcfe..94aebd7c2f 100644 --- a/packages/shared-components/src/utils/i18n.test.ts +++ b/packages/shared-components/src/utils/i18n.test.ts @@ -6,39 +6,40 @@ */ import counterpart from "counterpart"; +import { vi, describe, it, beforeEach, expect } from "vitest"; import { registerTranslations, setMissingEntryGenerator, getLocale, setLocale } from "./i18n"; describe("i18n utils", () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it("should wrap registerTranslations", () => { - jest.spyOn(counterpart, "registerTranslations"); + vi.spyOn(counterpart, "registerTranslations"); registerTranslations("en", { test: "This is a test" }); expect(counterpart.registerTranslations).toHaveBeenCalledWith("en", { test: "This is a test" }); }); it("should wrap setMissingEntryGenerator", () => { - jest.spyOn(counterpart, "setMissingEntryGenerator"); + vi.spyOn(counterpart, "setMissingEntryGenerator"); - const dummyFn = jest.fn(); + const dummyFn = vi.fn(); setMissingEntryGenerator(dummyFn); expect(counterpart.setMissingEntryGenerator).toHaveBeenCalledWith(dummyFn); }); it("should wrap getLocale", () => { - jest.spyOn(counterpart, "getLocale"); + vi.spyOn(counterpart, "getLocale"); getLocale(); expect(counterpart.getLocale).toHaveBeenCalled(); }); it("should wrap setLocale", () => { - jest.spyOn(counterpart, "setLocale"); + vi.spyOn(counterpart, "setLocale"); setLocale("en"); expect(counterpart.setLocale).toHaveBeenCalledWith("en"); diff --git a/packages/shared-components/src/utils/i18n.tsx b/packages/shared-components/src/utils/i18n.tsx index 2ce1f78005..d3cf396087 100644 --- a/packages/shared-components/src/utils/i18n.tsx +++ b/packages/shared-components/src/utils/i18n.tsx @@ -25,13 +25,11 @@ import React from "react"; import { KEY_SEPARATOR } from "matrix-web-i18n"; import counterpart from "counterpart"; -import type { TranslationKey } from "../index"; - -// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config -import webpackLangJsonUrl from "$webapp/i18n/languages.json"; - export { KEY_SEPARATOR, normalizeLanguageKey, getNormalizedLanguageKeys } from "matrix-web-i18n"; +// Path where we load language files from (the index plus translations for each language) +// The filename is appended to this, so a relative path here will result in a fetch for +// a relative URL. const i18nFolder = "i18n/"; // Control whether to also return original, untranslated strings @@ -421,13 +419,7 @@ async function getLanguage(langPath: string): Promise { } export async function getLangsJson(): Promise { - let url: string; - if (typeof webpackLangJsonUrl === "string") { - // in Jest this 'url' isn't a URL, so just fall through - url = webpackLangJsonUrl; - } else { - url = i18nFolder + "languages.json"; - } + const url = i18nFolder + "languages.json"; const res = await fetch(url, { method: "GET" }); diff --git a/packages/shared-components/src/utils/numbers.stories.tsx b/packages/shared-components/src/utils/numbers.stories.tsx new file mode 100644 index 0000000000..638f8fe4ed --- /dev/null +++ b/packages/shared-components/src/utils/numbers.stories.tsx @@ -0,0 +1,61 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React from "react"; +import { Markdown } from "@storybook/addon-docs/blocks"; + +import type { Meta } from "@storybook/react-vite"; +import clampDoc from "../../typedoc/functions/clamp.md?raw"; +import defaultNumberDoc from "../../typedoc/functions/defaultNumber.md?raw"; +import percentageOfDoc from "../../typedoc/functions/percentageOf.md?raw"; +import percentageWithinDoc from "../../typedoc/functions/percentageWithin.md?raw"; +import sumDoc from "../../typedoc/functions/sum.md?raw"; + +const meta = { + title: "utils/numbers", + parameters: { + docs: { + page: () => ( + <> +

Number Utilities

+

+ A collection of utility functions for working with numbers, including validation, clamping, and + percentage calculations. +

+ +
+

defaultNumber

+ {defaultNumberDoc} + +
+

clamp

+ {clampDoc} + +
+

sum

+ {sumDoc} + +
+

percentageWithin

+ {percentageWithinDoc} + +
+

percentageOf

+ {percentageOfDoc} + + ), + }, + }, + tags: ["autodocs", "skip-test"], +} satisfies Meta; + +export default meta; + +// Docs-only story - renders nothing but triggers autodocs +export const Docs = { + render: () => null, +}; diff --git a/packages/shared-components/src/utils/numbers.test.ts b/packages/shared-components/src/utils/numbers.test.ts index 928fd67ae0..7fa75cb81f 100644 --- a/packages/shared-components/src/utils/numbers.test.ts +++ b/packages/shared-components/src/utils/numbers.test.ts @@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { describe, it, expect } from "vitest"; + import { clamp, defaultNumber, percentageOf, percentageWithin, sum } from "./numbers"; describe("numbers", () => { diff --git a/packages/shared-components/src/viewmodel/Disposables.ts b/packages/shared-components/src/viewmodel/Disposables.ts index 77df53d097..34934a55b7 100644 --- a/packages/shared-components/src/viewmodel/Disposables.ts +++ b/packages/shared-components/src/viewmodel/Disposables.ts @@ -49,7 +49,7 @@ export class Disposables { /** * Add an event listener that will be removed on dispose */ - public trackListener(emitter: EventEmitter, event: string, callback: (...args: unknown[]) => void): void { + public trackListener(emitter: EventEmitter, event: string | symbol, callback: (...args: unknown[]) => void): void { this.throwIfDisposed(); emitter.on(event, callback); this.track(() => { diff --git a/packages/shared-components/src/viewmodel/index.ts b/packages/shared-components/src/viewmodel/index.ts index 0267f7934d..25e5ec60b8 100644 --- a/packages/shared-components/src/viewmodel/index.ts +++ b/packages/shared-components/src/viewmodel/index.ts @@ -12,3 +12,5 @@ export * from "./ViewModelSubscriptions"; export type * from "./ViewModel"; export * from "./MockViewModel"; export * from "./useCreateAutoDisposedViewModel"; +export * from "./useMockedViewModel"; +export * from "./useViewModel"; diff --git a/packages/shared-components/src/viewmodel/tests/Disposables.test.ts b/packages/shared-components/src/viewmodel/tests/Disposables.test.ts index 5b71f1871d..4b0cdedc11 100644 --- a/packages/shared-components/src/viewmodel/tests/Disposables.test.ts +++ b/packages/shared-components/src/viewmodel/tests/Disposables.test.ts @@ -5,6 +5,7 @@ Please see LICENSE files in the repository root for full details. */ import { EventEmitter } from "events"; +import { describe, it, vi, expect } from "vitest"; import { Disposables } from ".."; @@ -21,11 +22,11 @@ describe("Disposable", () => { const item1 = { foo: 5, - dispose: jest.fn(), + dispose: vi.fn(), }; disposables.track(item1); - const item2 = jest.fn(); + const item2 = vi.fn(); disposables.track(item2); disposables.dispose(); @@ -38,7 +39,7 @@ describe("Disposable", () => { const disposables = new Disposables(); disposables.dispose(); expect(() => { - disposables.track(jest.fn); + disposables.track(vi.fn); }).toThrow(); }); @@ -46,7 +47,7 @@ describe("Disposable", () => { const disposables = new Disposables(); const emitter = new EventEmitter(); - const fn = jest.fn(); + const fn = vi.fn(); disposables.trackListener(emitter, "FooEvent", fn); emitter.emit("FooEvent"); expect(fn).toHaveBeenCalled(); diff --git a/packages/shared-components/src/viewmodel/tests/Snapshot.test.ts b/packages/shared-components/src/viewmodel/tests/Snapshot.test.ts index 82cacfc02e..d731898fc0 100644 --- a/packages/shared-components/src/viewmodel/tests/Snapshot.test.ts +++ b/packages/shared-components/src/viewmodel/tests/Snapshot.test.ts @@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { describe, it, vi, expect } from "vitest"; + import { Snapshot } from ".."; interface TestSnapshot { @@ -15,26 +17,26 @@ interface TestSnapshot { describe("Snapshot", () => { it("should accept an initial value", () => { - const snapshot = new Snapshot({ key1: "foo", key2: 5, key3: false }, jest.fn()); + const snapshot = new Snapshot({ key1: "foo", key2: 5, key3: false }, vi.fn()); expect(snapshot.current).toStrictEqual({ key1: "foo", key2: 5, key3: false }); }); it("should call emit callback when state changes", () => { - const emit = jest.fn(); + const emit = vi.fn(); const snapshot = new Snapshot({ key1: "foo", key2: 5, key3: false }, emit); snapshot.merge({ key3: true }); expect(emit).toHaveBeenCalledTimes(1); }); it("should swap out entire snapshot on set call", () => { - const snapshot = new Snapshot({ key1: "foo", key2: 5, key3: false }, jest.fn()); + const snapshot = new Snapshot({ key1: "foo", key2: 5, key3: false }, vi.fn()); const newValue = { key1: "bar", key2: 8, key3: true }; snapshot.set(newValue); expect(snapshot.current).toStrictEqual(newValue); }); it("should merge partial snapshot on merge call", () => { - const snapshot = new Snapshot({ key1: "foo", key2: 5, key3: false }, jest.fn()); + const snapshot = new Snapshot({ key1: "foo", key2: 5, key3: false }, vi.fn()); snapshot.merge({ key2: 10 }); expect(snapshot.current).toStrictEqual({ key1: "foo", key2: 10, key3: false }); }); diff --git a/packages/shared-components/src/viewmodel/tests/useCreateAutoDisposedViewModel.test.ts b/packages/shared-components/src/viewmodel/tests/useCreateAutoDisposedViewModel.test.ts index 867ee56825..0ff4f59d1d 100644 --- a/packages/shared-components/src/viewmodel/tests/useCreateAutoDisposedViewModel.test.ts +++ b/packages/shared-components/src/viewmodel/tests/useCreateAutoDisposedViewModel.test.ts @@ -4,7 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { renderHook } from "jest-matrix-react"; +import { renderHook } from "@test-utils"; +import { describe, it, expect } from "vitest"; import { BaseViewModel } from "../BaseViewModel"; import { useCreateAutoDisposedViewModel } from "../useCreateAutoDisposedViewModel"; diff --git a/packages/shared-components/src/useMockedViewModel.ts b/packages/shared-components/src/viewmodel/useMockedViewModel.ts similarity index 89% rename from packages/shared-components/src/useMockedViewModel.ts rename to packages/shared-components/src/viewmodel/useMockedViewModel.ts index 6f13075351..dad041f1d7 100644 --- a/packages/shared-components/src/useMockedViewModel.ts +++ b/packages/shared-components/src/viewmodel/useMockedViewModel.ts @@ -7,7 +7,8 @@ import { useMemo } from "react"; -import { MockViewModel, type ViewModel } from "./viewmodel"; +import { MockViewModel } from "./MockViewModel"; +import { type ViewModel } from "./ViewModel"; /** * Hook helper to return a mocked view model created with the given snapshot and actions. diff --git a/packages/shared-components/src/useViewModel.ts b/packages/shared-components/src/viewmodel/useViewModel.ts similarity index 92% rename from packages/shared-components/src/useViewModel.ts rename to packages/shared-components/src/viewmodel/useViewModel.ts index 20c7070bff..ef7b8ec0da 100644 --- a/packages/shared-components/src/useViewModel.ts +++ b/packages/shared-components/src/viewmodel/useViewModel.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { useSyncExternalStore } from "react"; -import { type ViewModel } from "./viewmodel/ViewModel"; +import { type ViewModel } from "./ViewModel"; /** * A small wrapper around useSyncExternalStore to use a view model in a shared component view diff --git a/packages/shared-components/tsconfig.json b/packages/shared-components/tsconfig.json index 025901c97d..479670c124 100644 --- a/packages/shared-components/tsconfig.json +++ b/packages/shared-components/tsconfig.json @@ -11,15 +11,15 @@ "target": "es2022", "noUnusedLocals": true, "sourceMap": false, - "outDir": "./lib", "declaration": true, "jsx": "react", "lib": ["es2022", "es2024.promise", "dom", "dom.iterable"], + "types": [], "strict": true, "paths": { - "jest-matrix-react": ["./src/test/utils/jest-matrix-react"], - "rollup/parseAst": ["./node_modules/rollup/dist/parseAst"] + "@test-utils": ["./src/test/utils/index"] } }, - "include": ["./src/**/*.ts", "./src/**/*.tsx"] + "include": ["./src/**/*.ts", "./src/**/*.tsx", ".storybook/*.ts", ".storybook/*.tsx"], + "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/packages/shared-components/tsconfig.node.json b/packages/shared-components/tsconfig.node.json new file mode 100644 index 0000000000..7d4eb224bc --- /dev/null +++ b/packages/shared-components/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "target": "ESNext", + "module": "esnext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "types": [], + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "vitest.config.ts"] +} diff --git a/packages/shared-components/typedoc.json b/packages/shared-components/typedoc.json new file mode 100644 index 0000000000..2f4402584a --- /dev/null +++ b/packages/shared-components/typedoc.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/index.ts"], + "plugin": ["typedoc-plugin-markdown", "typedoc-plugin-missing-exports"], + "out": "typedoc", + "hidePageHeader": true, + "hidePageTitle": true, + "hideBreadcrumbs": true, + "useCodeBlocks": true, + "parametersFormat": "table", + "propertiesFormat": "table", + "enumMembersFormat": "table", + "typeDeclarationFormat": "table", + "indexFormat": "table", + "publicPath": "https://github.com/element-hq/element-web/blob/develop/packages/shared-components/", + "sourceLinkTemplate": "https://github.com/element-hq/element-web/blob/develop/{path}#L{line}", + "name": "@element-hq/web-shared-components", + "categorizeByGroup": true, + "externalSymbolLinkMappings": { + "@types/react": { + "*": "https://react.dev/" + }, + "react-virtuoso": { + "*": "https://virtuoso.dev/" + } + } +} diff --git a/packages/shared-components/vite.config.js b/packages/shared-components/vite.config.ts similarity index 83% rename from packages/shared-components/vite.config.js rename to packages/shared-components/vite.config.ts index 83c999d87f..7bb922b0f2 100644 --- a/packages/shared-components/vite.config.js +++ b/packages/shared-components/vite.config.ts @@ -1,10 +1,9 @@ /* - * * Copyright 2025 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. - * / + * */ import { dirname, resolve } from "node:path"; @@ -26,7 +25,13 @@ export default defineConfig({ rollupOptions: { // make sure to externalize deps that shouldn't be bundled // into your library - external: ["react", "react-dom", "@vector-im/compound-design-tokens", "@vector-im/compound-web"], + external: [ + "react", + "react-dom", + "@vector-im/compound-design-tokens", + "@vector-im/compound-web", + "react-virtuoso", + ], output: { // Provide global variables to use in the UMD build // for externalized deps @@ -37,12 +42,6 @@ export default defineConfig({ }, }, }, - resolve: { - alias: { - // Alias used by i18n.tsx - $webapp: resolve(__dirname, "..", "..", "webapp"), - }, - }, plugins: [ dts({ rollupTypes: true, diff --git a/packages/shared-components/vitest.config.ts b/packages/shared-components/vitest.config.ts new file mode 100644 index 0000000000..23cc026304 --- /dev/null +++ b/packages/shared-components/vitest.config.ts @@ -0,0 +1,155 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +/// + +import { defineConfig } from "vitest/config"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { storybookTest } from "@storybook/addon-vitest/vitest-plugin"; +import { storybookVis } from "storybook-addon-vis/vitest-plugin"; +import { playwright } from "@vitest/browser-playwright"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; +import { InlineConfig } from "vite"; +import { Reporter } from "vitest/reporters"; +import { env } from "process"; +import { BrowserContextOptions } from "playwright-core"; + +const dirname = typeof __dirname !== "undefined" ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + +const reporters: NonNullable["reporters"] = [["default"]]; +const slowTestReporter: Reporter = { + onTestRunEnd(testModules, unhandledErrors, reason) { + const tests = testModules + .flatMap((m) => Array.from(m.children.allTests())) + .filter((test) => test.diagnostic()?.slow); + tests.sort((x, y) => x.diagnostic()?.duration! - y.diagnostic()?.duration!); + tests.reverse(); + if (tests.length > 0) { + console.warn("Slowest 10 tests:"); + } + for (const t of tests.slice(0, 10)) { + console.warn(`${t.module.moduleId} > ${t.fullName}: ${t.diagnostic()?.duration.toFixed(0)}ms`); + } + }, +}; + +// if we're running under GHA, enable the GHA & Sonar reporters +if (env["GITHUB_ACTIONS"] !== undefined) { + reporters.push([ + "github-actions", + { + silent: false, + }, + ]); + + reporters.push([ + "vitest-sonar-reporter", + { + outputFile: "coverage/sonar-report.xml", + onWritePath: (path) => `packages/shared-components/${path}`, + }, + ]); + + // if we're running against the develop branch, also enable the slow test reporter + if (env["GITHUB_REF"] == "refs/heads/develop") { + reporters.push(slowTestReporter); + } +} + +const commonContextOptions: Omit = { + reducedMotion: "reduce", + // Force consistent font rendering + colorScheme: "light", + // Disable font smoothing for consistent rendering + deviceScaleFactor: 1, +}; + +const commonLaunchOptions = { + // Options to try to make font rendering more consistent + args: ["--font-render-hinting=none", "--disable-font-subpixel-positioning", "--disable-lcd-text"], +}; + +export default defineConfig({ + test: { + coverage: { + provider: "v8", + include: ["src/**/*.{ts,tsx}"], + exclude: ["src/**/*.stories.tsx"], + reporter: [["lcov", { projectRoot: "../../" }]], + }, + reporters, + globals: false, + pool: "threads", + projects: [ + { + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + storybookTest({ + configDir: path.join(dirname, ".storybook"), + storybookScript: "storybook --ci", + tags: { + exclude: ["skip-test"], + }, + }), + storybookVis({ + // 3px of difference allowed before marking as failed + failureThreshold: 3, + }), + ], + test: { + name: "storybook", + browser: { + enabled: true, + headless: true, + provider: playwright({ + contextOptions: commonContextOptions, + launchOptions: commonLaunchOptions, + }), + instances: [{ browser: "chromium" }], + }, + setupFiles: [".storybook/vitest.setup.ts"], + }, + }, + { + extends: true, + plugins: [nodePolyfills({ include: ["util"], globals: { global: false } })], + test: { + name: "unit", + browser: { + enabled: true, + headless: true, + provider: playwright({ + // These tests don't actually take screenshots (at least at time of writing) + // but let's pass these options everywhere for consistency + contextOptions: commonContextOptions, + launchOptions: commonLaunchOptions, + }), + instances: [{ browser: "chromium" }], + }, + setupFiles: ["src/test/setupTests.ts"], + }, + css: { + modules: { + // Stabilise snapshots by stripping the hash component of the CSS module class name + generateScopedName: (name) => name, + }, + }, + }, + ], + }, + optimizeDeps: { + include: ["vite-plugin-node-polyfills/shims/buffer", "vite-plugin-node-polyfills/shims/process"], + }, + resolve: { + alias: { + "@test-utils": path.resolve(__dirname, "./src/test/utils/index.tsx"), + }, + }, +}); diff --git a/packages/shared-components/yarn.lock b/packages/shared-components/yarn.lock index ceeb5ab89e..f6100b462d 100644 --- a/packages/shared-components/yarn.lock +++ b/packages/shared-components/yarn.lock @@ -14,7 +14,7 @@ dependencies: axe-core "~4.11.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.27.1": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== @@ -23,25 +23,34 @@ js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/compat-data@^7.27.2": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f" - integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== - -"@babel/core@^7.22.5", "@babel/core@^7.23.9", "@babel/core@^7.27.4", "@babel/core@^7.28.0", "@babel/core@^7.7.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" - integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== +"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.5" - "@babel/helper-compilation-targets" "^7.27.2" - "@babel/helper-module-transforms" "^7.28.3" - "@babel/helpers" "^7.28.4" - "@babel/parser" "^7.28.5" - "@babel/template" "^7.27.2" - "@babel/traverse" "^7.28.5" - "@babel/types" "^7.28.5" + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.28.6": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== + +"@babel/core@^7.24.4", "@babel/core@^7.28.5": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.6.tgz#531bf883a1126e53501ba46eb3bb414047af507f" + integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/generator" "^7.28.6" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" @@ -49,61 +58,139 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.22.5", "@babel/generator@^7.27.5", "@babel/generator@^7.28.3", "@babel/generator@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" - integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== +"@babel/core@^7.28.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== dependencies: - "@babel/parser" "^7.28.5" - "@babel/types" "^7.28.5" + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.28.6", "@babel/generator@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.0.tgz#4cba5a76b3c71d8be31761b03329d5dc7768447f" + integrity sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ== + dependencies: + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" "@jridgewell/gen-mapping" "^0.3.12" "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" -"@babel/helper-compilation-targets@^7.27.2": - version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" - integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== +"@babel/helper-annotate-as-pure@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== dependencies: - "@babel/compat-data" "^7.27.2" + "@babel/types" "^7.27.3" + +"@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== + dependencies: + "@babel/compat-data" "^7.28.6" "@babel/helper-validator-option" "^7.27.1" browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.18.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz#611ff5482da9ef0db6291bcd24303400bca170fb" + integrity sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.6" + semver "^6.3.1" + "@babel/helper-globals@^7.28.0": version "7.28.0" resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== -"@babel/helper-module-imports@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" - integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== +"@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== + dependencies: + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + +"@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== dependencies: - "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" -"@babel/helper-module-transforms@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" - integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== - dependencies: - "@babel/helper-module-imports" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" - "@babel/traverse" "^7.28.3" +"@babel/helper-plugin-utils@^7.18.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": +"@babel/helper-plugin-utils@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== +"@babel/helper-replace-supers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz#94aa9a1d7423a00aead3f204f78834ce7d53fe44" + integrity sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.28.6" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-string-parser@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5": +"@babel/helper-validator-identifier@^7.24.7", "@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== @@ -113,144 +200,54 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== -"@babel/helpers@^7.28.4": - version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" - integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== +"@babel/helpers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" + integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== dependencies: - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.4" + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" -"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.4", "@babel/parser@^7.28.5": +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.28.5", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== + dependencies: + "@babel/types" "^7.29.0" + +"@babel/parser@^7.24.4": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" + integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== + dependencies: + "@babel/types" "^7.28.6" + +"@babel/parser@^7.28.4": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== dependencies: "@babel/types" "^7.28.5" -"@babel/parser@^7.18.5": - version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8" - integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg== +"@babel/plugin-proposal-private-methods@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== dependencies: - "@babel/types" "^7.28.4" + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-bigint@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" - integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-import-attributes@^7.24.7": +"@babel/plugin-transform-react-jsx-self@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" - integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.27.1": +"@babel/plugin-transform-react-jsx-source@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" - integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== - dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" - integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -259,42 +256,63 @@ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== -"@babel/template@^7.22.5", "@babel/template@^7.27.2": - version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" - integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/parser" "^7.27.2" - "@babel/types" "^7.27.1" +"@babel/runtime@^7.17.9": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== -"@babel/traverse@^7.18.5": - version "7.28.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.4.tgz#8d456101b96ab175d487249f60680221692b958b" - integrity sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ== +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.3" + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.5": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e" + integrity sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/generator" "^7.28.6" "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.4" - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.4" + "@babel/parser" "^7.28.6" + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" debug "^4.3.1" -"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" - integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== +"@babel/traverse@^7.28.0", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.5" + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.5" - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.5" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" debug "^4.3.1" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5": +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@babel/types@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" + integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@babel/types@^7.27.3": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== @@ -307,319 +325,179 @@ resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== -"@bcoe/v8-coverage@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" - integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bcoe/v8-coverage@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa" + integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== "@element-hq/element-web-module-api@^1.8.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.9.0.tgz#2e4fcc8809418c8670d4f0576bc4a9a235bc6c50" integrity sha512-Ao/V9w+wysZK4bh61LlKlznF10n2ZbD6KcUI46/zUMttXbmJn3ahvbzhEpwYcD+Cjy3ag5ycxLIIGkKV/fncXg== -"@element-hq/element-web-playwright-common@^2.0.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@element-hq/element-web-playwright-common/-/element-web-playwright-common-2.1.0.tgz#86e8a5632f8cc8bb393a1ec1b793a6278cd5b2c7" - integrity sha512-Ah9aioownR5OxAX7IDzys7wqyFmojruqgiRr2oUTLbPA5Y6jUSKWdAu5AqGvi+PYr0kG6zQfdsruhe5FXW9YuQ== +"@element-hq/element-web-playwright-common@2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@element-hq/element-web-playwright-common/-/element-web-playwright-common-2.2.5.tgz#69b626ae35a297b341aa06c201168046b251edbe" + integrity sha512-M2a/vPqmqL0GigKeROYGMUjSxwGNSnDResK5sFjD6FJNlxVmJNSD5NdCe9SQ46o2VI0j2dJKD+e3n3gcQ/CNCg== dependencies: "@axe-core/playwright" "^4.10.1" "@testcontainers/postgresql" "^11.0.0" - glob "^11.1.0" - lodash-es "^4.17.21" + glob "^13.0.0" + lodash-es "^4.17.23" mailpit-api "^1.2.0" strip-ansi "^7.1.0" testcontainers "^11.0.0" yaml "^2.7.0" -"@emnapi/core@^1.4.3": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.1.tgz#3a79a02dbc84f45884a1806ebb98e5746bdfaac4" - integrity sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg== - dependencies: - "@emnapi/wasi-threads" "1.1.0" - tslib "^2.4.0" - -"@emnapi/runtime@^1.4.3": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791" - integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA== - dependencies: - tslib "^2.4.0" - -"@emnapi/wasi-threads@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" - integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== - dependencies: - tslib "^2.4.0" - -"@esbuild/aix-ppc64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c" - integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA== - "@esbuild/aix-ppc64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz#521cbd968dcf362094034947f76fa1b18d2d403c" integrity sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw== -"@esbuild/android-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752" - integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg== - "@esbuild/android-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz#61ea550962d8aa12a9b33194394e007657a6df57" integrity sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA== -"@esbuild/android-arm@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a" - integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg== - "@esbuild/android-arm@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz#554887821e009dd6d853f972fde6c5143f1de142" integrity sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA== -"@esbuild/android-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16" - integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg== - "@esbuild/android-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz#a7ce9d0721825fc578f9292a76d9e53334480ba2" integrity sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A== -"@esbuild/darwin-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd" - integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg== - "@esbuild/darwin-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz#2cb7659bd5d109803c593cfc414450d5430c8256" integrity sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg== -"@esbuild/darwin-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e" - integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA== - "@esbuild/darwin-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz#e741fa6b1abb0cd0364126ba34ca17fd5e7bf509" integrity sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA== -"@esbuild/freebsd-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe" - integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg== - "@esbuild/freebsd-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz#2b64e7116865ca172d4ce034114c21f3c93e397c" integrity sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g== -"@esbuild/freebsd-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3" - integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ== - "@esbuild/freebsd-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz#e5252551e66f499e4934efb611812f3820e990bb" integrity sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA== -"@esbuild/linux-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977" - integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ== - "@esbuild/linux-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz#dc4acf235531cd6984f5d6c3b13dbfb7ddb303cb" integrity sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw== -"@esbuild/linux-arm@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9" - integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw== - "@esbuild/linux-arm@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz#56a900e39240d7d5d1d273bc053daa295c92e322" integrity sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw== -"@esbuild/linux-ia32@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0" - integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA== - "@esbuild/linux-ia32@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz#d4a36d473360f6870efcd19d52bbfff59a2ed1cc" integrity sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w== -"@esbuild/linux-loong64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0" - integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng== - "@esbuild/linux-loong64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz#fcf0ab8c3eaaf45891d0195d4961cb18b579716a" integrity sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg== -"@esbuild/linux-mips64el@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd" - integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw== - "@esbuild/linux-mips64el@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz#598b67d34048bb7ee1901cb12e2a0a434c381c10" integrity sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw== -"@esbuild/linux-ppc64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869" - integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA== - "@esbuild/linux-ppc64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz#3846c5df6b2016dab9bc95dde26c40f11e43b4c0" integrity sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ== -"@esbuild/linux-riscv64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6" - integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w== - "@esbuild/linux-riscv64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz#173d4475b37c8d2c3e1707e068c174bb3f53d07d" integrity sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA== -"@esbuild/linux-s390x@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663" - integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg== - "@esbuild/linux-s390x@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz#f7a4790105edcab8a5a31df26fbfac1aa3dacfab" integrity sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w== -"@esbuild/linux-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306" - integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw== - "@esbuild/linux-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz#2ecc1284b1904aeb41e54c9ddc7fcd349b18f650" integrity sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA== -"@esbuild/netbsd-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4" - integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg== - "@esbuild/netbsd-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz#e2863c2cd1501845995cb11adf26f7fe4be527b0" integrity sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw== -"@esbuild/netbsd-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076" - integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ== - "@esbuild/netbsd-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz#93f7609e2885d1c0b5a1417885fba8d1fcc41272" integrity sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA== -"@esbuild/openbsd-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd" - integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A== - "@esbuild/openbsd-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz#a1985604a203cdc325fd47542e106fafd698f02e" integrity sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA== -"@esbuild/openbsd-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679" - integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw== - "@esbuild/openbsd-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz#8209e46c42f1ffbe6e4ef77a32e1f47d404ad42a" integrity sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg== -"@esbuild/openharmony-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d" - integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg== - "@esbuild/openharmony-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz#8fade4441893d9cc44cbd7dcf3776f508ab6fb2f" integrity sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag== -"@esbuild/sunos-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6" - integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w== - "@esbuild/sunos-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz#980d4b9703a16f0f07016632424fc6d9a789dfc2" integrity sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg== -"@esbuild/win32-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323" - integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg== - "@esbuild/win32-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz#1c09a3633c949ead3d808ba37276883e71f6111a" integrity sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg== -"@esbuild/win32-ia32@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267" - integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ== - "@esbuild/win32-ia32@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz#1b1e3a63ad4bef82200fef4e369e0fff7009eee5" integrity sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ== -"@esbuild/win32-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5" - integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA== - "@esbuild/win32-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b" integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": +"@eslint-community/eslint-utils@^4.2.0": version "4.9.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== dependencies: eslint-visitor-keys "^3.4.3" +"@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.2": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + "@eslint-community/regexpp@^4.6.1": version "4.12.1" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" @@ -645,6 +523,13 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@fetch-mock/vitest@^0.2.18": + version "0.2.18" + resolved "https://registry.yarnpkg.com/@fetch-mock/vitest/-/vitest-0.2.18.tgz#2514de2a054563892122d07cc7d00b76190f2892" + integrity sha512-s2bG7/MSwVFun5gTzrkZzJSmcdSurTmxt5B+JA/4ALyx0Pfo1al0/MlZPBtZ358Kkjv9CpRlhpyLf6bt4OrtLQ== + dependencies: + fetch-mock "^12.6.0" + "@figspec/components@^2.0.1": version "2.0.5" resolved "https://registry.yarnpkg.com/@figspec/components/-/components-2.0.5.tgz#0c953e367b2b9d7661cb3507fef61d371313d7a4" @@ -658,10 +543,57 @@ "@figspec/components" "^2.0.1" "@lit-labs/react" "^2.0.0" +"@floating-ui/core@^1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7" + integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w== + dependencies: + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/dom@^1.7.4": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77" + integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA== + dependencies: + "@floating-ui/core" "^1.7.3" + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz#189f681043c1400561f62972f461b93f01bf2231" + integrity sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw== + dependencies: + "@floating-ui/dom" "^1.7.4" + +"@floating-ui/react@^0.27.0": + version "0.27.16" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.16.tgz#6e485b5270b7a3296fdc4d0faf2ac9abf955a2f7" + integrity sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g== + dependencies: + "@floating-ui/react-dom" "^2.1.6" + "@floating-ui/utils" "^0.2.10" + tabbable "^6.0.0" + +"@floating-ui/utils@^0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" + integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== + +"@gerrit0/mini-shiki@^3.17.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-3.21.0.tgz#377938e63f29f9f698b00c35dcdebc0c104c1a15" + integrity sha512-9PrsT5DjZA+w3lur/aOIx3FlDeHdyCEFlv9U+fmsVyjPZh61G5SYURQ/1ebe2U63KbDmI2V8IhIUegWb8hjOyg== + dependencies: + "@shikijs/engine-oniguruma" "^3.21.0" + "@shikijs/langs" "^3.21.0" + "@shikijs/themes" "^3.21.0" + "@shikijs/types" "^3.21.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@grpc/grpc-js@^1.11.1": - version "1.14.2" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.2.tgz#d245069181a1a8057abd35522d6052482730cf19" - integrity sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA== + version "1.14.3" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.3.tgz#4c9b817a900ae4020ddc28515ae4b52c78cfb8da" + integrity sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA== dependencies: "@grpc/proto-loader" "^0.8.0" "@js-sdsl/ordered-map" "^4.4.2" @@ -686,18 +618,6 @@ protobufjs "^7.5.3" yargs "^17.7.2" -"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== - dependencies: - "@hapi/hoek" "^9.0.0" - "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -741,90 +661,11 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" -"@istanbuljs/load-nyc-config@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" - integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== - dependencies: - camelcase "^5.3.1" - find-up "^4.1.0" - get-package-type "^0.1.0" - js-yaml "^3.13.1" - resolve-from "^5.0.0" - -"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jest/console@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-30.2.0.tgz#c52fcd5b58fdd2e8eb66b2fd8ae56f2f64d05b28" - integrity sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ== - dependencies: - "@jest/types" "30.2.0" - "@types/node" "*" - chalk "^4.1.2" - jest-message-util "30.2.0" - jest-util "30.2.0" - slash "^3.0.0" - -"@jest/core@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-30.2.0.tgz#813d59faa5abd5510964a8b3a7b17cc77b775275" - integrity sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ== - dependencies: - "@jest/console" "30.2.0" - "@jest/pattern" "30.0.1" - "@jest/reporters" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" - "@types/node" "*" - ansi-escapes "^4.3.2" - chalk "^4.1.2" - ci-info "^4.2.0" - exit-x "^0.2.2" - graceful-fs "^4.2.11" - jest-changed-files "30.2.0" - jest-config "30.2.0" - jest-haste-map "30.2.0" - jest-message-util "30.2.0" - jest-regex-util "30.0.1" - jest-resolve "30.2.0" - jest-resolve-dependencies "30.2.0" - jest-runner "30.2.0" - jest-runtime "30.2.0" - jest-snapshot "30.2.0" - jest-util "30.2.0" - jest-validate "30.2.0" - jest-watcher "30.2.0" - micromatch "^4.0.8" - pretty-format "30.2.0" - slash "^3.0.0" - -"@jest/create-cache-key-function@^30.0.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-30.2.0.tgz#86dbaf8cce43e8a0266180a5236b6f0b3be9d09b" - integrity sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg== - dependencies: - "@jest/types" "30.2.0" - "@jest/diff-sequences@30.0.1": version "30.0.1" resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz#0ededeae4d071f5c8ffe3678d15f3a1be09156be" integrity sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw== -"@jest/environment@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-30.2.0.tgz#1e673cdb8b93ded707cf6631b8353011460831fa" - integrity sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g== - dependencies: - "@jest/fake-timers" "30.2.0" - "@jest/types" "30.2.0" - "@types/node" "*" - jest-mock "30.2.0" - "@jest/expect-utils@30.2.0": version "30.2.0" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.2.0.tgz#4f95413d4748454fdb17404bf1141827d15e6011" @@ -832,41 +673,11 @@ dependencies: "@jest/get-type" "30.1.0" -"@jest/expect@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-30.2.0.tgz#9a5968499bb8add2bbb09136f69f7df5ddbf3185" - integrity sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA== - dependencies: - expect "30.2.0" - jest-snapshot "30.2.0" - -"@jest/fake-timers@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-30.2.0.tgz#0941ddc28a339b9819542495b5408622dc9e94ec" - integrity sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw== - dependencies: - "@jest/types" "30.2.0" - "@sinonjs/fake-timers" "^13.0.0" - "@types/node" "*" - jest-message-util "30.2.0" - jest-mock "30.2.0" - jest-util "30.2.0" - "@jest/get-type@30.1.0": version "30.1.0" resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.1.0.tgz#4fcb4dc2ebcf0811be1c04fd1cb79c2dba431cbc" integrity sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA== -"@jest/globals@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-30.2.0.tgz#2f4b696d5862664b89c4ee2e49ae24d2bb7e0988" - integrity sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw== - dependencies: - "@jest/environment" "30.2.0" - "@jest/expect" "30.2.0" - "@jest/types" "30.2.0" - jest-mock "30.2.0" - "@jest/pattern@30.0.1": version "30.0.1" resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.0.1.tgz#d5304147f49a052900b4b853dedb111d080e199f" @@ -875,35 +686,6 @@ "@types/node" "*" jest-regex-util "30.0.1" -"@jest/reporters@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-30.2.0.tgz#a36b28fcbaf0c4595250b108e6f20e363348fd91" - integrity sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" - "@jridgewell/trace-mapping" "^0.3.25" - "@types/node" "*" - chalk "^4.1.2" - collect-v8-coverage "^1.0.2" - exit-x "^0.2.2" - glob "^10.3.10" - graceful-fs "^4.2.11" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^6.0.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^5.0.0" - istanbul-reports "^3.1.3" - jest-message-util "30.2.0" - jest-util "30.2.0" - jest-worker "30.2.0" - slash "^3.0.0" - string-length "^4.0.2" - v8-to-istanbul "^9.0.1" - "@jest/schemas@30.0.5": version "30.0.5" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.0.5.tgz#7bdf69fc5a368a5abdb49fd91036c55225846473" @@ -911,67 +693,7 @@ dependencies: "@sinclair/typebox" "^0.34.0" -"@jest/snapshot-utils@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz#387858eb90c2f98f67bff327435a532ac5309fbe" - integrity sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug== - dependencies: - "@jest/types" "30.2.0" - chalk "^4.1.2" - graceful-fs "^4.2.11" - natural-compare "^1.4.0" - -"@jest/source-map@30.0.1": - version "30.0.1" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-30.0.1.tgz#305ebec50468f13e658b3d5c26f85107a5620aaa" - integrity sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg== - dependencies: - "@jridgewell/trace-mapping" "^0.3.25" - callsites "^3.1.0" - graceful-fs "^4.2.11" - -"@jest/test-result@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-30.2.0.tgz#9c0124377fb7996cdffb86eda3dbc56eacab363d" - integrity sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg== - dependencies: - "@jest/console" "30.2.0" - "@jest/types" "30.2.0" - "@types/istanbul-lib-coverage" "^2.0.6" - collect-v8-coverage "^1.0.2" - -"@jest/test-sequencer@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz#bf0066bc72e176d58f5dfa7f212b6e7eee44f221" - integrity sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q== - dependencies: - "@jest/test-result" "30.2.0" - graceful-fs "^4.2.11" - jest-haste-map "30.2.0" - slash "^3.0.0" - -"@jest/transform@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-30.2.0.tgz#54bef1a4510dcbd58d5d4de4fe2980a63077ef2a" - integrity sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA== - dependencies: - "@babel/core" "^7.27.4" - "@jest/types" "30.2.0" - "@jridgewell/trace-mapping" "^0.3.25" - babel-plugin-istanbul "^7.0.1" - chalk "^4.1.2" - convert-source-map "^2.0.0" - fast-json-stable-stringify "^2.1.0" - graceful-fs "^4.2.11" - jest-haste-map "30.2.0" - jest-regex-util "30.0.1" - jest-util "30.2.0" - micromatch "^4.0.8" - pirates "^4.0.7" - slash "^3.0.0" - write-file-atomic "^5.0.1" - -"@jest/types@30.2.0", "@jest/types@^30.0.1": +"@jest/types@30.2.0": version "30.2.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.2.0.tgz#1c678a7924b8f59eafd4c77d56b6d0ba976d62b8" integrity sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg== @@ -984,13 +706,12 @@ "@types/yargs" "^17.0.33" chalk "^4.1.2" -"@joshwooding/vite-plugin-react-docgen-typescript@0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.1.tgz#f630b93ed13d5d07483c0ead42db793053b364a9" - integrity sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw== +"@joshwooding/vite-plugin-react-docgen-typescript@^0.6.3": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.3.tgz#cc371b00b0c4f5a74e20da5c125a3529d379983b" + integrity sha512-9TGZuAX+liGkNKkwuo3FYJu7gHWT0vkBcf7GkOe7s7fmC19XwH/4u5u7sDIFrMooe558ORcmuBvBz7Ur5PlbHw== dependencies: - glob "^10.0.0" - magic-string "^0.30.0" + glob "^11.1.0" react-docgen-typescript "^2.2.2" "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": @@ -1019,7 +740,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31": version "0.3.31" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== @@ -1044,6 +765,18 @@ resolved "https://registry.yarnpkg.com/@lit/react/-/react-1.0.8.tgz#b3e229173b7b57d550909bf95d8f3da1a9510557" integrity sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw== +"@matrix-org/react-sdk-module-api@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@matrix-org/react-sdk-module-api/-/react-sdk-module-api-2.5.0.tgz#df774d0ae0c327fbd40f8994bbb13ed35e26c337" + integrity sha512-l/SmiO47gPIRd6YJJGj+B6qbxyypJF6SEsfYr7j9rSW6E85ZYCqf+TpMM2LmfwZRADyKfCVkaJbbBZYpoD02VA== + dependencies: + "@babel/runtime" "^7.17.9" + +"@matrix-org/spec@^1.7.0": + version "1.16.0" + resolved "https://registry.yarnpkg.com/@matrix-org/spec/-/spec-1.16.0.tgz#c88f4ed521e4c0bd3a4c108bcaf13f25173a0fdc" + integrity sha512-xUKHkwGXXISMCfTrx6JW6uGEK5O8IeZVOjBm7FX1h/ihpK6l50nlSIMRYdtz4V6q3pvOVBOCft4hPYTJVeTZDA== + "@mdx-js/react@^3.0.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.1.1.tgz#24bda7fffceb2fe256f954482123cda1be5f5fef" @@ -1094,15 +827,6 @@ resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz#d4f6937353bc4568292654efb0a0e0532adbcba2" integrity sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw== -"@napi-rs/wasm-runtime@^0.2.11": - version "0.2.12" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" - integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== - dependencies: - "@emnapi/core" "^1.4.3" - "@emnapi/runtime" "^1.4.3" - "@tybys/wasm-util" "^0.10.0" - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1129,17 +853,17 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@pkgr/core@^0.2.9": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" - integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== - -"@playwright/test@1.57.0": - version "1.57.0" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.57.0.tgz#a14720ffa9ed7ef7edbc1f60784fc6134acbb003" - integrity sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA== +"@playwright/test@1.58.1": + version "1.58.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.58.1.tgz#891dcd1da815cb1042490531f6d8778988509d22" + integrity sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w== dependencies: - playwright "1.57.0" + playwright "1.58.1" + +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.29" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1" + integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -1194,6 +918,314 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@radix-ui/primitive@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.3.tgz#e2dbc13bdc5e4168f4334f75832d7bdd3e2de5ba" + integrity sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg== + +"@radix-ui/react-arrow@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz#e14a2657c81d961598c5e72b73dd6098acc04f09" + integrity sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w== + dependencies: + "@radix-ui/react-primitive" "2.1.3" + +"@radix-ui/react-collection@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz#d05c25ca9ac4695cc19ba91f42f686e3ea2d9aec" + integrity sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + +"@radix-ui/react-compose-refs@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" + integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== + +"@radix-ui/react-context-menu@^2.2.16": + version "2.2.16" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz#e7bf94a457b68af08f24ad696949144530faab50" + integrity sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww== + dependencies: + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-menu" "2.1.16" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.2.2" + +"@radix-ui/react-context@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" + integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== + +"@radix-ui/react-context@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.3.tgz#81286f643b310d040eaac13b18e223130861d839" + integrity sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw== + +"@radix-ui/react-dialog@^1.1.1": + version "1.1.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz#1de3d7a7e9a17a9874d29c07f5940a18a119b632" + integrity sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw== + dependencies: + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.11" + "@radix-ui/react-focus-guards" "1.1.3" + "@radix-ui/react-focus-scope" "1.1.7" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.5" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-controllable-state" "1.2.2" + aria-hidden "^1.2.4" + react-remove-scroll "^2.6.3" + +"@radix-ui/react-direction@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14" + integrity sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw== + +"@radix-ui/react-dismissable-layer@1.1.11": + version "1.1.11" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz#e33ab6f6bdaa00f8f7327c408d9f631376b88b37" + integrity sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg== + dependencies: + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-escape-keydown" "1.1.1" + +"@radix-ui/react-dropdown-menu@^2.1.1": + version "2.1.16" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz#5ee045c62bad8122347981c479d92b1ff24c7254" + integrity sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw== + dependencies: + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-menu" "2.1.16" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-controllable-state" "1.2.2" + +"@radix-ui/react-focus-guards@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz#2a5669e464ad5fde9f86d22f7fdc17781a4dfa7f" + integrity sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw== + +"@radix-ui/react-focus-scope@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d" + integrity sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + +"@radix-ui/react-form@^0.1.0": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@radix-ui/react-form/-/react-form-0.1.8.tgz#daec0fde305a70edf1a97b932b5e02a4cbf5b68e" + integrity sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ== + dependencies: + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-label" "2.1.7" + "@radix-ui/react-primitive" "2.1.3" + +"@radix-ui/react-id@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" + integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-label@2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.7.tgz#ad959ff9c6e4968d533329eb95696e1ba8ad72ab" + integrity sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ== + dependencies: + "@radix-ui/react-primitive" "2.1.3" + +"@radix-ui/react-menu@2.1.16": + version "2.1.16" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz#528a5a973c3a7413d3d49eb9ccd229aa52402911" + integrity sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg== + dependencies: + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-collection" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.11" + "@radix-ui/react-focus-guards" "1.1.3" + "@radix-ui/react-focus-scope" "1.1.7" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-popper" "1.2.8" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.5" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-roving-focus" "1.1.11" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + aria-hidden "^1.2.4" + react-remove-scroll "^2.6.3" + +"@radix-ui/react-popper@1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz#a79f39cdd2b09ab9fb50bf95250918422c4d9602" + integrity sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-rect" "1.1.1" + "@radix-ui/react-use-size" "1.1.1" + "@radix-ui/rect" "1.1.1" + +"@radix-ui/react-portal@1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472" + integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ== + dependencies: + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-presence@1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz#5d8f28ac316c32f078afce2996839250c10693db" + integrity sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-primitive@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc" + integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ== + dependencies: + "@radix-ui/react-slot" "1.2.3" + +"@radix-ui/react-primitive@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz#2626ea309ebd63bf5767d3e7fc4081f81b993df0" + integrity sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg== + dependencies: + "@radix-ui/react-slot" "1.2.4" + +"@radix-ui/react-progress@^1.1.0": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.8.tgz#9f9a68d75bf763f9c64808724a83d2de804bd061" + integrity sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA== + dependencies: + "@radix-ui/react-context" "1.1.3" + "@radix-ui/react-primitive" "2.1.4" + +"@radix-ui/react-roving-focus@1.1.11": + version "1.1.11" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz#ef54384b7361afc6480dcf9907ef2fedb5080fd9" + integrity sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA== + dependencies: + "@radix-ui/primitive" "1.1.3" + "@radix-ui/react-collection" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.2.2" + +"@radix-ui/react-separator@^1.1.0": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.8.tgz#24f871fbf9630af316d0c14cbc7519a6e33aa11e" + integrity sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g== + dependencies: + "@radix-ui/react-primitive" "2.1.4" + +"@radix-ui/react-slot@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" + integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + +"@radix-ui/react-slot@1.2.4", "@radix-ui/react-slot@^1.1.0": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz#63c0ba05fdf90cc49076b94029c852d7bac1fb83" + integrity sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + +"@radix-ui/react-use-callback-ref@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40" + integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg== + +"@radix-ui/react-use-controllable-state@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190" + integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg== + dependencies: + "@radix-ui/react-use-effect-event" "0.0.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-use-effect-event@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907" + integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-use-escape-keydown@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" + integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.1" + +"@radix-ui/react-use-layout-effect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" + integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== + +"@radix-ui/react-use-rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz#01443ca8ed071d33023c1113e5173b5ed8769152" + integrity sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w== + dependencies: + "@radix-ui/rect" "1.1.1" + +"@radix-ui/react-use-size@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz#6de276ffbc389a537ffe4316f5b0f24129405b37" + integrity sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" + integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== + +"@rolldown/pluginutils@1.0.0-beta.53": + version "1.0.0-beta.53" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz#c57a5234ae122671aff6fe72e673a7ed90f03f87" + integrity sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ== + "@rollup/plugin-inject@^5.0.5": version "5.0.5" resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz#616f3a73fe075765f91c5bec90176608bed277a3" @@ -1212,115 +1244,135 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-android-arm-eabi@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb" - integrity sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w== +"@rollup/rollup-android-arm-eabi@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz#60a7889627edae1e6fade79fe188db8ead2c6829" + integrity sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA== -"@rollup/rollup-android-arm64@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz#2b025510c53a5e3962d3edade91fba9368c9d71c" - integrity sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w== +"@rollup/rollup-android-arm64@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz#c2d15e2c1b720ea6bbcbdc6bd22fbc663840b82b" + integrity sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA== -"@rollup/rollup-darwin-arm64@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz#3577c38af68ccf34c03e84f476bfd526abca10a0" - integrity sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA== +"@rollup/rollup-darwin-arm64@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz#6f30bf301c6b4155f753231d220d47efe78ab04f" + integrity sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g== -"@rollup/rollup-darwin-x64@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz#2bf5f2520a1f3b551723d274b9669ba5b75ed69c" - integrity sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ== +"@rollup/rollup-darwin-x64@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz#4c9f37c97f93af9187c3cd0223b05d4f3f1eddc7" + integrity sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ== -"@rollup/rollup-freebsd-arm64@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz#4bb9cc80252564c158efc0710153c71633f1927c" - integrity sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w== +"@rollup/rollup-freebsd-arm64@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz#811bf4aeb619dc834837a10bc55fb2d23622bdb2" + integrity sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow== -"@rollup/rollup-freebsd-x64@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz#2301289094d49415a380cf942219ae9d8b127440" - integrity sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q== +"@rollup/rollup-freebsd-x64@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz#b2f3ee43ee13aa98abf30cce8a8e1f5cfc712317" + integrity sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ== -"@rollup/rollup-linux-arm-gnueabihf@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz#1d03d776f2065e09fc141df7d143476e94acca88" - integrity sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw== +"@rollup/rollup-linux-arm-gnueabihf@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz#0f7a59cef492b9d9dc225bb3d65d9638d371bc39" + integrity sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw== -"@rollup/rollup-linux-arm-musleabihf@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz#8623de0e040b2fd52a541c602688228f51f96701" - integrity sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg== +"@rollup/rollup-linux-arm-musleabihf@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz#3b97d6d4b64d328da78a0d7d29b2783c83315dc5" + integrity sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q== -"@rollup/rollup-linux-arm64-gnu@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz#ce2d1999bc166277935dde0301cde3dd0417fb6e" - integrity sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w== +"@rollup/rollup-linux-arm64-gnu@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz#62a49932e0210b25c85408076243d717e3efabf0" + integrity sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA== -"@rollup/rollup-linux-arm64-musl@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz#88c2523778444da952651a2219026416564a4899" - integrity sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A== +"@rollup/rollup-linux-arm64-musl@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz#69cb1d164c9cde8ceae026b333bf227ea4a7ea34" + integrity sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA== -"@rollup/rollup-linux-loong64-gnu@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz#578ca2220a200ac4226c536c10c8cc6e4f276714" - integrity sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g== +"@rollup/rollup-linux-loong64-gnu@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz#40d8ab4dae850555fed866bd2e7218aff7fe3ccf" + integrity sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA== -"@rollup/rollup-linux-ppc64-gnu@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz#aa338d3effd4168a20a5023834a74ba2c3081293" - integrity sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw== +"@rollup/rollup-linux-loong64-musl@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz#a1a7a06dbcbf9d3038df1603d7e7d2eb9bf20e6b" + integrity sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA== -"@rollup/rollup-linux-riscv64-gnu@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz#16ba582f9f6cff58119aa242782209b1557a1508" - integrity sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g== +"@rollup/rollup-linux-ppc64-gnu@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz#9495db6fe330cdcc2aea781434406fd08a180442" + integrity sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw== -"@rollup/rollup-linux-riscv64-musl@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz#e404a77ebd6378483888b8064c703adb011340ab" - integrity sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A== +"@rollup/rollup-linux-ppc64-musl@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz#6ea3814aacddd8c811542e1a5cbd5772b9f19cce" + integrity sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ== -"@rollup/rollup-linux-s390x-gnu@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz#92ad52d306227c56bec43d96ad2164495437ffe6" - integrity sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg== +"@rollup/rollup-linux-riscv64-gnu@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz#b5ea2c1599140d3cca2490c668ade233bc6c6a78" + integrity sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg== -"@rollup/rollup-linux-x64-gnu@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz#fd0dea3bb9aa07e7083579f25e1c2285a46cb9fa" - integrity sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w== +"@rollup/rollup-linux-riscv64-musl@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz#3b2694f588c4eeaeab30f697ba35e17347536c53" + integrity sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q== -"@rollup/rollup-linux-x64-musl@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz#37a3efb09f18d555f8afc490e1f0444885de8951" - integrity sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q== +"@rollup/rollup-linux-s390x-gnu@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz#c37296f3b4642fe834c5390efeb9b85c166ac1a8" + integrity sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ== -"@rollup/rollup-openharmony-arm64@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz#c489bec9f4f8320d42c9b324cca220c90091c1f7" - integrity sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw== +"@rollup/rollup-linux-x64-gnu@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz#95d926276df80cd738f4a1a7fc5b897534fc81bb" + integrity sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw== -"@rollup/rollup-win32-arm64-msvc@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz#152832b5f79dc22d1606fac3db946283601b7080" - integrity sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw== +"@rollup/rollup-linux-x64-musl@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz#9116cd2892f79c843f28c5045a2fc3c77204a20d" + integrity sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA== -"@rollup/rollup-win32-ia32-msvc@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz#54d91b2bb3bf3e9f30d32b72065a4e52b3a172a5" - integrity sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA== +"@rollup/rollup-openbsd-x64@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz#d7e0517290503243d1856d27d48abadcdbc301b6" + integrity sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw== -"@rollup/rollup-win32-x64-gnu@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz#df9df03e61a003873efec8decd2034e7f135c71e" - integrity sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg== +"@rollup/rollup-openharmony-arm64@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz#661320edb00150f9ec9810d776225d48f0b97a33" + integrity sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ== -"@rollup/rollup-win32-x64-msvc@4.53.3": - version "4.53.3" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe" - integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ== +"@rollup/rollup-win32-arm64-msvc@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz#38bb3e21bae763166da6992e22e413c6e5fdf957" + integrity sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ== + +"@rollup/rollup-win32-ia32-msvc@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz#fc59f6fa03cf1e87b3a60a9f1f60f8e7f676f96f" + integrity sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ== + +"@rollup/rollup-win32-x64-gnu@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz#ed3f1546fce1a6918ed950aba4d1fd524c24a09c" + integrity sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw== + +"@rollup/rollup-win32-x64-msvc@4.55.2": + version "4.55.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz#af3ff15decd9050692c989f9328f7808c5ec72eb" + integrity sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q== + +"@rtsao/scc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== "@rushstack/node-core-library@5.17.0": version "5.17.0" @@ -1368,83 +1420,99 @@ argparse "~1.0.9" string-argv "~0.3.1" -"@sideway/address@^4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" - integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== +"@shikijs/engine-oniguruma@^3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz#0e666454a03fd85d6c634d9dbe70a63f007a6323" + integrity sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ== dependencies: - "@hapi/hoek" "^9.0.0" + "@shikijs/types" "3.21.0" + "@shikijs/vscode-textmate" "^10.0.2" -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== +"@shikijs/langs@^3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.21.0.tgz#da33400a85c7cba75fc9f4a6b9feb69a6c39c800" + integrity sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA== + dependencies: + "@shikijs/types" "3.21.0" -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@shikijs/themes@^3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.21.0.tgz#1955d642ea37d70d1137e6cf47da7dc9c34ff4c0" + integrity sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw== + dependencies: + "@shikijs/types" "3.21.0" + +"@shikijs/types@3.21.0", "@shikijs/types@^3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.21.0.tgz#510d6ddbea65add27980a6ca36cc7bdabc7afe90" + integrity sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA== + dependencies: + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224" + integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== "@sinclair/typebox@^0.34.0": - version "0.34.41" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.41.tgz#aa51a6c1946df2c5a11494a2cdb9318e026db16c" - integrity sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g== + version "0.34.48" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.48.tgz#75b0ead87e59e1adbd6dccdc42bad4fddee73b59" + integrity sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA== -"@sinonjs/commons@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" - integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== - dependencies: - type-detect "4.0.8" - -"@sinonjs/fake-timers@^13.0.0": - version "13.0.5" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz#36b9dbc21ad5546486ea9173d6bea063eb1717d5" - integrity sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw== - dependencies: - "@sinonjs/commons" "^3.0.1" +"@standard-schema/spec@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== "@storybook/addon-a11y@^10.0.7": - version "10.1.5" - resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-10.1.5.tgz#9fc95df597243d0617a4d6f36d4534f949474988" - integrity sha512-dMUrkuQyvDfD6SdvV7F7cbjRrhHN0kqCNhRfg1i1IJuLuck6kiALpx8176KhWBcAkN/0J/1V75n7+F9YU/JlPA== + version "10.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-10.2.4.tgz#63fcef8454a1bc42ddf09f89d9a0e2aa48fef47c" + integrity sha512-VGhdZ+iP2l/CSulIKV2kt3SMWVHntOigqWqGkNYf6YNYofynUYEKdsNqBvHx4ySuNEl/eXJ8LRO8FKYnU7LxZQ== dependencies: "@storybook/global" "^5.0.0" axe-core "^4.2.0" "@storybook/addon-designs@^11.0.1": - version "11.1.0" - resolved "https://registry.yarnpkg.com/@storybook/addon-designs/-/addon-designs-11.1.0.tgz#c5d363d43b386a4fd6d9c1745b13e95a89a08761" - integrity sha512-i9lnUJ9x+UwThUpIjgg7QWvadhwmQ1ZuqcrTFe12giqyyYJKM6hdrUEuxGgSOrz3pkmDV/Bypq3G5ehwIDdKiw== + version "11.1.1" + resolved "https://registry.yarnpkg.com/@storybook/addon-designs/-/addon-designs-11.1.1.tgz#54ab51526959f7f8c4cb832ee6e5fa8e418dedce" + integrity sha512-1KAmTzoW/qw4RfR8uft3pBgsdWHoQiMp9rt+nzhFLEBPd1Ru3YiTnVL/JBEiJkGXsQfQxMnAYRRwYgf+HTr4yw== dependencies: "@figspec/react" "^2.0.0" "@storybook/addon-docs@^10.0.7": - version "10.1.5" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.1.5.tgz#52bce4eed90240fc2b133caec1131962697c865a" - integrity sha512-2FfqFrfEeaKv8OerZCWt1b+dm7N/nizv1G2CnTZfWJ0TKxbPDH6kffAqC9lMnT3xAZjDWiBLdnVx2oouKdmSvw== + version "10.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.2.4.tgz#4fb70439b14e18035c6e862234aa8a12a89d0ae9" + integrity sha512-FzscAmdBiOGnGrxiEM+8eTg43kjqgjLfObg+lbJVRR/a0DmZ3xfAPNB0+VKYQbN0FacNcWLM9LZ/7U0hRBPBnQ== dependencies: "@mdx-js/react" "^3.0.0" - "@storybook/csf-plugin" "10.1.5" - "@storybook/icons" "^2.0.0" - "@storybook/react-dom-shim" "10.1.5" + "@storybook/csf-plugin" "10.2.4" + "@storybook/icons" "^2.0.1" + "@storybook/react-dom-shim" "10.2.4" react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" ts-dedent "^2.0.0" -"@storybook/builder-vite@10.1.5": - version "10.1.5" - resolved "https://registry.yarnpkg.com/@storybook/builder-vite/-/builder-vite-10.1.5.tgz#27b11a0fb583e342cd041ea1eded7d012a0c278a" - integrity sha512-5alpNa+TQXK1zp9MeovUK/yIUkZqpIFUScUer6cYgidI96Boovn7OXt5oXQ8CqqpzuEtgCvz44TzCmgZoGv41g== +"@storybook/addon-vitest@^10.1.11": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-vitest/-/addon-vitest-10.2.4.tgz#94b77163a736824eda3730ee9c76253668314f7c" + integrity sha512-BT1iP89U4wcbpzTURU8WYTAeUcdNh4WIt0BqsnATmMwR/jKNJW6QgXCVqGQTSpRjWj40hX5e2JkQYCNXdjKsPw== dependencies: - "@storybook/csf-plugin" "10.1.5" - "@vitest/mocker" "3.2.4" + "@storybook/global" "^5.0.0" + "@storybook/icons" "^2.0.1" + +"@storybook/builder-vite@10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@storybook/builder-vite/-/builder-vite-10.2.4.tgz#d092e43937ad1e33d62e18ecf3045f3343a7a7ad" + integrity sha512-/hcT1xj3CL5GkJ5v5/EguZdttDwNE6weNXK7vKzp034tnGcLycOossDsTiUQkBowSL+Ylc8aKj+ZgvddPNfOig== + dependencies: + "@storybook/csf-plugin" "10.2.4" ts-dedent "^2.0.0" -"@storybook/csf-plugin@10.1.5": - version "10.1.5" - resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.1.5.tgz#8d147fa82c19e348991bbad88c1af8ef56a9d2a3" - integrity sha512-v+D7PVRkNUHznfoQg8yqpLWZIIbPddqHDSi1oBGdegF0Kv/lVsGqTZGRLroApsMu7BLwLhpcMID6ofxlfftWKg== +"@storybook/csf-plugin@10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.2.4.tgz#d39734349475eb158d3c384f15230f424145e696" + integrity sha512-kupPQEV+4N9mzsZHYaokvhO/KHBjYdWda9PNmPQwy0TR7r2mzthgaNH72TjmgN1L6DIbsuyOG1wtczcPJn4+Jg== dependencies: unplugin "^2.3.5" @@ -1453,163 +1521,58 @@ resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed" integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ== -"@storybook/icons@^2.0.0": +"@storybook/icons@^2.0.0", "@storybook/icons@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.0.1.tgz#1bd351db1d33bfccbbafa7b64fb413168f1a6616" integrity sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg== -"@storybook/react-dom-shim@10.1.5": - version "10.1.5" - resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.1.5.tgz#e39c169b093594de88dcb0c59b797ec23028ef0c" - integrity sha512-CsXcq26wINUgYP8KnfSuS60B10/Ag34YdcnWIEl9hM5UtTQ65WYJ9fVFqpzfnQrkpgRMd7iQjtmUhCe+4umnHg== +"@storybook/react-dom-shim@10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.2.4.tgz#a6611240690a6548d576ffd87def4854a25bc52e" + integrity sha512-i22OtrZ7GeZPt/odLf0vqyDhRSKyaLsHkkKSBcANQfzRRnBZmiz2FchOtWm9uvoDWybQsTruZq7kTdtpEhwyGw== "@storybook/react-vite@^10.0.7": - version "10.1.5" - resolved "https://registry.yarnpkg.com/@storybook/react-vite/-/react-vite-10.1.5.tgz#9da7bbb0b860526da51fa3d50993934b593588b3" - integrity sha512-27RiCVw5QZ/f9fXS8sGaPHuWkbHSoS66ifeakxHgbkbIXjVI4M6pWB7NUj49MwU1YUMOpB0T8KasvyMZzv/UPA== + version "10.2.4" + resolved "https://registry.yarnpkg.com/@storybook/react-vite/-/react-vite-10.2.4.tgz#336a89774e9c1fc861509468b104475ce42e671d" + integrity sha512-ztPAyfpF/uTyz1j9E7i0tnaHb7QsqdCwb4Obmj+SMw0mthahtYiUt+DAhRofB3/74x6/0cQoMKkbXFr8Urv1xA== dependencies: - "@joshwooding/vite-plugin-react-docgen-typescript" "0.6.1" + "@joshwooding/vite-plugin-react-docgen-typescript" "^0.6.3" "@rollup/pluginutils" "^5.0.2" - "@storybook/builder-vite" "10.1.5" - "@storybook/react" "10.1.5" + "@storybook/builder-vite" "10.2.4" + "@storybook/react" "10.2.4" empathic "^2.0.0" magic-string "^0.30.0" react-docgen "^8.0.0" resolve "^1.22.8" tsconfig-paths "^4.2.0" -"@storybook/react@10.1.5": - version "10.1.5" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-10.1.5.tgz#0f6a1ec5c4338d3604d6e957d0f53f7ddf9a2e6a" - integrity sha512-M8fR7WVs79fPJHwRZxkz4XzIfzs/bN0heWdZX0D4iRjeIcY4nLM/tyalCcQDrGgrSJbgAAf4xd7KXaZzaZSAqA== +"@storybook/react@10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-10.2.4.tgz#d4bad2da9943ce5cc031e81419ff09731f530e0e" + integrity sha512-PiBQIF1WZ09yNiQUSqxqZ0wyrSdQRK/eQcoA9f8JJ1BpW7qTy4pnoqKc0s32SI4I3IcaTBm9JwCyMFR0MrIosA== dependencies: "@storybook/global" "^5.0.0" - "@storybook/react-dom-shim" "10.1.5" + "@storybook/react-dom-shim" "10.2.4" react-docgen "^8.0.2" -"@storybook/test-runner@^0.24.1": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@storybook/test-runner/-/test-runner-0.24.2.tgz#59deba0619f7b896f92e186ac108244014859716" - integrity sha512-76DbflDTGAKq8Af6uHbWTGnKzKHhjLbJaZXRFhVnKqFocoXcej58C9DpM0BJ3addu7fSDJmPwfR97OINg16XFQ== +"@stylistic/eslint-plugin@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-5.7.1.tgz#bb108186a0133071b38be5fa705cd262260be8a8" + integrity sha512-zjTUwIsEfT+k9BmXwq1QEFYsb4afBlsI1AXFyWQBgggMzwBFOuu92pGrE5OFx90IOjNl+lUbQoTG7f8S0PkOdg== dependencies: - "@babel/core" "^7.22.5" - "@babel/generator" "^7.22.5" - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.5" - "@jest/types" "^30.0.1" - "@swc/core" "^1.5.22" - "@swc/jest" "^0.2.38" - expect-playwright "^0.8.0" - jest "^30.0.4" - jest-circus "^30.0.4" - jest-environment-node "^30.0.4" - jest-junit "^16.0.0" - jest-process-manager "^0.4.0" - jest-runner "^30.0.4" - jest-serializer-html "^7.1.0" - jest-watch-typeahead "^3.0.1" - nyc "^15.1.0" - playwright "^1.14.0" - playwright-core ">=1.2.0" - rimraf "^3.0.2" - uuid "^8.3.2" - -"@swc/core-darwin-arm64@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz#bd0bd3ab7730e3ffa64cf200c0ed7c572cbaba97" - integrity sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ== - -"@swc/core-darwin-x64@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.15.3.tgz#502b1e1c680df6b962265ca81a0c1a23e6ff070f" - integrity sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A== - -"@swc/core-linux-arm-gnueabihf@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.3.tgz#e32cc6a2e06a75060d6f598ba2ca6f96c5c0cc43" - integrity sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg== - -"@swc/core-linux-arm64-gnu@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.3.tgz#9b9861bc44059e393d4baf98b3cd3d6c4ea6f521" - integrity sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw== - -"@swc/core-linux-arm64-musl@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.3.tgz#f6388743e5a159018bd468e8f710940b2614384b" - integrity sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g== - -"@swc/core-linux-x64-gnu@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz#15fea551c7a3aeb1bdc3ad5c652d73c9321ddba8" - integrity sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A== - -"@swc/core-linux-x64-musl@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz#d3f17bab4ffcadbb47f135e6a14d6f3e401af289" - integrity sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug== - -"@swc/core-win32-arm64-msvc@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.3.tgz#9da386df7fed00b3473bcf4281ff3fcd14726d2c" - integrity sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA== - -"@swc/core-win32-ia32-msvc@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.3.tgz#c398d4f0f10ffec2151a79733ee1ce86a945a1ea" - integrity sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw== - -"@swc/core-win32-x64-msvc@1.15.3": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.3.tgz#715596b034a654c82b03ef734a9b44c29bcd3a68" - integrity sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog== - -"@swc/core@^1.5.22": - version "1.15.3" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.15.3.tgz#2d0a5c4ac4c180c3dbf2f6d5d958b9fcbaa9755f" - integrity sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q== - dependencies: - "@swc/counter" "^0.1.3" - "@swc/types" "^0.1.25" - optionalDependencies: - "@swc/core-darwin-arm64" "1.15.3" - "@swc/core-darwin-x64" "1.15.3" - "@swc/core-linux-arm-gnueabihf" "1.15.3" - "@swc/core-linux-arm64-gnu" "1.15.3" - "@swc/core-linux-arm64-musl" "1.15.3" - "@swc/core-linux-x64-gnu" "1.15.3" - "@swc/core-linux-x64-musl" "1.15.3" - "@swc/core-win32-arm64-msvc" "1.15.3" - "@swc/core-win32-ia32-msvc" "1.15.3" - "@swc/core-win32-x64-msvc" "1.15.3" - -"@swc/counter@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" - integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== - -"@swc/jest@^0.2.38": - version "0.2.39" - resolved "https://registry.yarnpkg.com/@swc/jest/-/jest-0.2.39.tgz#482bee0adb0726fab1487a4f902a278ec563a6b7" - integrity sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA== - dependencies: - "@jest/create-cache-key-function" "^30.0.0" - "@swc/counter" "^0.1.3" - jsonc-parser "^3.2.0" - -"@swc/types@^0.1.25": - version "0.1.25" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.25.tgz#b517b2a60feb37dd933e542d93093719e4cf1078" - integrity sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g== - dependencies: - "@swc/counter" "^0.1.3" + "@eslint-community/eslint-utils" "^4.9.1" + "@typescript-eslint/types" "^8.53.1" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" + estraverse "^5.3.0" + picomatch "^4.0.3" "@testcontainers/postgresql@^11.0.0": - version "11.9.0" - resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-11.9.0.tgz#9009e28208c4134c4cd6614827fa98614ea1d494" - integrity sha512-beLyLdLygFllktviM132Xd6tQ4i5FnuyZP+4BQEjUb5sJYHYnIrV/ZBzRRflIlF8gugt1GXgudkmr/HxM9vtKw== + version "11.11.0" + resolved "https://registry.yarnpkg.com/@testcontainers/postgresql/-/postgresql-11.11.0.tgz#c3244b397902c89c13f0c6c914e9e54f51b3799f" + integrity sha512-Og64I/h5LKLVvUTkAcLeTXfFcMhh3dCHCypN3Uzd+tQMd70SpCfQ0LCP9v/U+MS7JBRzU9EmqhUFkTOm4hyZWw== dependencies: - testcontainers "^11.9.0" + testcontainers "^11.11.0" "@testing-library/dom@^10.4.1": version "10.4.1" @@ -1625,7 +1588,7 @@ picocolors "1.1.1" pretty-format "^27.0.2" -"@testing-library/jest-dom@^6.6.3": +"@testing-library/jest-dom@^6.6.3", "@testing-library/jest-dom@^6.9.1": version "6.9.1" resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2" integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA== @@ -1637,10 +1600,10 @@ picocolors "^1.1.1" redent "^3.0.0" -"@testing-library/react@^16.3.0": - version "16.3.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.0.tgz#3a85bb9bdebf180cd76dba16454e242564d598a6" - integrity sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw== +"@testing-library/react@^16.3.2": + version "16.3.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.2.tgz#672883b7acb8e775fc0492d9e9d25e06e89786d0" + integrity sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g== dependencies: "@babel/runtime" "^7.12.5" @@ -1649,13 +1612,6 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== -"@tybys/wasm-util@^0.10.0": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" - integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== - dependencies: - tslib "^2.4.0" - "@types/argparse@1.0.38": version "1.0.38" resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9" @@ -1744,7 +1700,19 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": +"@types/glob-to-regexp@^0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@types/glob-to-regexp/-/glob-to-regexp-0.4.4.tgz#409e71290253203185b1ea8a3d6ea406a4bdc902" + integrity sha512-nDKoaKJYbnn1MZxUY0cA1bPmmgZbg0cTq7Rh13d0KWYNOiKbqoR+2d89SnRPszGh7ROzSwZ/GOjZ4jPbmmZ6Eg== + +"@types/hast@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== @@ -1763,27 +1731,15 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest-image-snapshot@^6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@types/jest-image-snapshot/-/jest-image-snapshot-6.4.0.tgz#641054d2fa2ff130a49c844ee7a9a68f281b6017" - integrity sha512-8TQ/EgqFCX0UWSpH488zAc21fCkJNpZPnnp3xWFMqElxApoJV5QOoqajnVRV7AhfF0rbQWTVyc04KG7tXnzCPA== - dependencies: - "@types/jest" "*" - "@types/pixelmatch" "*" - ssim.js "^3.1.1" - -"@types/jest@*": - version "30.0.0" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-30.0.0.tgz#5e85ae568006712e4ad66f25433e9bdac8801f1d" - integrity sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA== - dependencies: - expect "^30.0.0" - pretty-format "^30.0.0" +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/lodash@^4.17.20": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.21.tgz#b806831543d696b14f8112db600ea9d3a1df6ea4" - integrity sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ== + version "4.17.23" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.23.tgz#c1bb06db218acc8fc232da0447473fc2fb9d9841" + integrity sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA== "@types/mdx@^2.0.0": version "2.0.13" @@ -1791,9 +1747,9 @@ integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== "@types/node@*", "@types/node@>=13.7.0": - version "24.10.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26" - integrity sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA== + version "25.0.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.9.tgz#81ce3579ddf67cae812a9d49c8a0ab90c82e7782" + integrity sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw== dependencies: undici-types "~7.16.0" @@ -1804,17 +1760,20 @@ dependencies: undici-types "~5.26.4" -"@types/pixelmatch@*": - version "5.2.6" - resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.6.tgz#fba6de304ac958495f27d85989f5c6bb7499a686" - integrity sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg== - dependencies: - "@types/node" "*" +"@types/normalize-package-data@^2.4.0": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== + +"@types/react-dom@^19.2.3": + version "19.2.3" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" + integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== "@types/react@^19.2.2": - version "19.2.7" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.7.tgz#84e62c0f23e8e4e5ac2cadcea1ffeacccae7f62f" - integrity sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg== + version "19.2.10" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.10.tgz#f3ea799e6b4cebad6dfd231c238fc9de7652e2d2" + integrity sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw== dependencies: csstype "^3.2.2" @@ -1850,12 +1809,10 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== -"@types/wait-on@^5.2.0": - version "5.3.4" - resolved "https://registry.yarnpkg.com/@types/wait-on/-/wait-on-5.3.4.tgz#5ee270b3e073fb01073f9f044922c6893de8c4d2" - integrity sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw== - dependencies: - "@types/node" "*" +"@types/unist@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== "@types/yargs-parser@*": version "21.0.3" @@ -1869,172 +1826,177 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/project-service@8.49.0": - version "8.49.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.49.0.tgz#ce220525c88cb2d23792b391c07e14cb9697651a" - integrity sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g== +"@typescript-eslint/eslint-plugin@^8.53.1": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz#d8899e5c2eccf5c4a20d01c036a193753748454d" + integrity sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.49.0" - "@typescript-eslint/types" "^8.49.0" - debug "^4.3.4" + "@eslint-community/regexpp" "^4.12.2" + "@typescript-eslint/scope-manager" "8.54.0" + "@typescript-eslint/type-utils" "8.54.0" + "@typescript-eslint/utils" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" + ignore "^7.0.5" + natural-compare "^1.4.0" + ts-api-utils "^2.4.0" -"@typescript-eslint/scope-manager@8.49.0": - version "8.49.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz#a3496765b57fb48035d671174552e462e5bffa63" - integrity sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg== +"@typescript-eslint/parser@^8.53.1": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.54.0.tgz#3d01a6f54ed247deb9982621f70e7abf1810bd97" + integrity sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA== dependencies: - "@typescript-eslint/types" "8.49.0" - "@typescript-eslint/visitor-keys" "8.49.0" + "@typescript-eslint/scope-manager" "8.54.0" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/typescript-estree" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" + debug "^4.4.3" -"@typescript-eslint/tsconfig-utils@8.49.0", "@typescript-eslint/tsconfig-utils@^8.49.0": - version "8.49.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz#857777c8e35dd1e564505833d8043f544442fbf4" - integrity sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA== - -"@typescript-eslint/types@8.49.0", "@typescript-eslint/types@^8.49.0": - version "8.49.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.49.0.tgz#c1bd3ebf956d9e5216396349ca23c58d74f06aee" - integrity sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ== - -"@typescript-eslint/typescript-estree@8.49.0": - version "8.49.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz#99c5a53275197ccb4e849786dad68344e9924135" - integrity sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA== +"@typescript-eslint/project-service@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.54.0.tgz#f582aceb3d752544c8e1b11fea8d95d00cf9adc6" + integrity sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g== dependencies: - "@typescript-eslint/project-service" "8.49.0" - "@typescript-eslint/tsconfig-utils" "8.49.0" - "@typescript-eslint/types" "8.49.0" - "@typescript-eslint/visitor-keys" "8.49.0" - debug "^4.3.4" - minimatch "^9.0.4" - semver "^7.6.0" + "@typescript-eslint/tsconfig-utils" "^8.54.0" + "@typescript-eslint/types" "^8.54.0" + debug "^4.4.3" + +"@typescript-eslint/scope-manager@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz#307dc8cbd80157e2772c2d36216857415a71ab33" + integrity sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg== + dependencies: + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" + +"@typescript-eslint/tsconfig-utils@8.54.0", "@typescript-eslint/tsconfig-utils@^8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz#71dd7ba1674bd48b172fc4c85b2f734b0eae3dbc" + integrity sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw== + +"@typescript-eslint/type-utils@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz#64965317dd4118346c2fa5ee94492892200e9fb9" + integrity sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA== + dependencies: + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/typescript-estree" "8.54.0" + "@typescript-eslint/utils" "8.54.0" + debug "^4.4.3" + ts-api-utils "^2.4.0" + +"@typescript-eslint/types@8.54.0", "@typescript-eslint/types@^8.53.1", "@typescript-eslint/types@^8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.54.0.tgz#c12d41f67a2e15a8a96fbc5f2d07b17331130889" + integrity sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA== + +"@typescript-eslint/typescript-estree@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz#3c7716905b2b811fadbd2114804047d1bfc86527" + integrity sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA== + dependencies: + "@typescript-eslint/project-service" "8.54.0" + "@typescript-eslint/tsconfig-utils" "8.54.0" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/visitor-keys" "8.54.0" + debug "^4.4.3" + minimatch "^9.0.5" + semver "^7.7.3" tinyglobby "^0.2.15" - ts-api-utils "^2.1.0" + ts-api-utils "^2.4.0" -"@typescript-eslint/utils@^8.8.1": - version "8.49.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.49.0.tgz#43b3b91d30afd6f6114532cf0b228f1790f43aff" - integrity sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA== +"@typescript-eslint/utils@8.54.0", "@typescript-eslint/utils@^8.48.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.54.0.tgz#c79a4bcbeebb4f571278c0183ed1cb601d84c6c8" + integrity sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA== dependencies: - "@eslint-community/eslint-utils" "^4.7.0" - "@typescript-eslint/scope-manager" "8.49.0" - "@typescript-eslint/types" "8.49.0" - "@typescript-eslint/typescript-estree" "8.49.0" + "@eslint-community/eslint-utils" "^4.9.1" + "@typescript-eslint/scope-manager" "8.54.0" + "@typescript-eslint/types" "8.54.0" + "@typescript-eslint/typescript-estree" "8.54.0" -"@typescript-eslint/visitor-keys@8.49.0": - version "8.49.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz#8e450cc502c0d285cad9e84d400cf349a85ced6c" - integrity sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA== +"@typescript-eslint/visitor-keys@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz#0e4b50124b210b8600b245dd66cbad52deb15590" + integrity sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA== dependencies: - "@typescript-eslint/types" "8.49.0" + "@typescript-eslint/types" "8.54.0" eslint-visitor-keys "^4.2.1" -"@ungap/structured-clone@^1.2.0", "@ungap/structured-clone@^1.3.0": +"@ungap/structured-clone@^1.2.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== -"@unrs/resolver-binding-android-arm-eabi@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz#9f5b04503088e6a354295e8ea8fe3cb99e43af81" - integrity sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw== +"@vector-im/compound-design-tokens@^6.4.3": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-6.9.0.tgz#626d06fcf2afa37db7633425271a6de9e2b38933" + integrity sha512-LFmJW77wdV/b8ii/R2nrpge4ewtM/tSC3krDGFhENVNgYqbKUz00adZhKXAufJANxzGdAd4PsiFGa16YRlVLrQ== -"@unrs/resolver-binding-android-arm64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz#7414885431bd7178b989aedc4d25cccb3865bc9f" - integrity sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g== - -"@unrs/resolver-binding-darwin-arm64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz#b4a8556f42171fb9c9f7bac8235045e82aa0cbdf" - integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g== - -"@unrs/resolver-binding-darwin-x64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz#fd4d81257b13f4d1a083890a6a17c00de571f0dc" - integrity sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ== - -"@unrs/resolver-binding-freebsd-x64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz#d2513084d0f37c407757e22f32bd924a78cfd99b" - integrity sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw== - -"@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz#844d2605d057488d77fab09705f2866b86164e0a" - integrity sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw== - -"@unrs/resolver-binding-linux-arm-musleabihf@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz#204892995cefb6bd1d017d52d097193bc61ddad3" - integrity sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw== - -"@unrs/resolver-binding-linux-arm64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz#023eb0c3aac46066a10be7a3f362e7b34f3bdf9d" - integrity sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ== - -"@unrs/resolver-binding-linux-arm64-musl@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz#9e6f9abb06424e3140a60ac996139786f5d99be0" - integrity sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w== - -"@unrs/resolver-binding-linux-ppc64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz#b111417f17c9d1b02efbec8e08398f0c5527bb44" - integrity sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA== - -"@unrs/resolver-binding-linux-riscv64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz#92ffbf02748af3e99873945c9a8a5ead01d508a9" - integrity sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ== - -"@unrs/resolver-binding-linux-riscv64-musl@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz#0bec6f1258fc390e6b305e9ff44256cb207de165" - integrity sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew== - -"@unrs/resolver-binding-linux-s390x-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz#577843a084c5952f5906770633ccfb89dac9bc94" - integrity sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg== - -"@unrs/resolver-binding-linux-x64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz#36fb318eebdd690f6da32ac5e0499a76fa881935" - integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w== - -"@unrs/resolver-binding-linux-x64-musl@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz#bfb9af75f783f98f6a22c4244214efe4df1853d6" - integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA== - -"@unrs/resolver-binding-wasm32-wasi@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz#752c359dd875684b27429500d88226d7cc72f71d" - integrity sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ== +"@vector-im/compound-web@^8.3.6": + version "8.3.6" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-8.3.6.tgz#1f669e339d086faa14f4cd9f7a39e7d3d3e2a4d1" + integrity sha512-w7jjUJ8dXlLE2Ja/2J8Z433mtra3QF0cHDU5BGVLBBJnT/IwU4vazJRaU61dIOm3OnzDnsDreKcrMtWAUIhQkA== dependencies: - "@napi-rs/wasm-runtime" "^0.2.11" + "@floating-ui/react" "^0.27.0" + "@radix-ui/react-context-menu" "^2.2.16" + "@radix-ui/react-dropdown-menu" "^2.1.1" + "@radix-ui/react-form" "^0.1.0" + "@radix-ui/react-progress" "^1.1.0" + "@radix-ui/react-separator" "^1.1.0" + "@radix-ui/react-slot" "^1.1.0" + classnames "^2.5.1" + vaul "^1.0.0" -"@unrs/resolver-binding-win32-arm64-msvc@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz#ce5735e600e4c2fbb409cd051b3b7da4a399af35" - integrity sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw== +"@vitejs/plugin-react@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz#46f47be184c05a18839cb8705d79578b469ac6eb" + integrity sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ== + dependencies: + "@babel/core" "^7.28.5" + "@babel/plugin-transform-react-jsx-self" "^7.27.1" + "@babel/plugin-transform-react-jsx-source" "^7.27.1" + "@rolldown/pluginutils" "1.0.0-beta.53" + "@types/babel__core" "^7.20.5" + react-refresh "^0.18.0" -"@unrs/resolver-binding-win32-ia32-msvc@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz#72fc57bc7c64ec5c3de0d64ee0d1810317bc60a6" - integrity sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ== +"@vitest/browser-playwright@^4.0.17": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz#1a844a44cf2f1e2321ca70e405063104350e5472" + integrity sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g== + dependencies: + "@vitest/browser" "4.0.18" + "@vitest/mocker" "4.0.18" + tinyrainbow "^3.0.3" -"@unrs/resolver-binding-win32-x64-msvc@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777" - integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== +"@vitest/browser@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/browser/-/browser-4.0.18.tgz#9d826cc21f09c27f8fe758715a92a6a878236a02" + integrity sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng== + dependencies: + "@vitest/mocker" "4.0.18" + "@vitest/utils" "4.0.18" + magic-string "^0.30.21" + pixelmatch "7.1.0" + pngjs "^7.0.0" + sirv "^3.0.2" + tinyrainbow "^3.0.3" + ws "^8.18.3" -"@vector-im/compound-design-tokens@^6.3.0": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-6.4.2.tgz#29189d6480c8ccf09ce143cb4618fb13a56a7583" - integrity sha512-LHLGZgnatH3mQXn9TF+m/SUinPS2nKvuCT/r2AQ7HAgEIG/S/Ck6e/iV4IFQLSZnd9gU0RlMsLkP2UQ/AKUEBA== +"@vitest/coverage-v8@^4.0.17": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz#b9c4db7479acd51d5f0ced91b2853c29c3d0cda7" + integrity sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg== + dependencies: + "@bcoe/v8-coverage" "^1.0.2" + "@vitest/utils" "4.0.18" + ast-v8-to-istanbul "^0.3.10" + istanbul-lib-coverage "^3.2.2" + istanbul-lib-report "^3.0.1" + istanbul-reports "^3.2.0" + magicast "^0.5.1" + obug "^2.1.1" + std-env "^3.10.0" + tinyrainbow "^3.0.3" "@vitest/expect@3.2.4": version "3.2.4" @@ -2047,14 +2009,26 @@ chai "^5.2.0" tinyrainbow "^2.0.0" -"@vitest/mocker@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.4.tgz#4471c4efbd62db0d4fa203e65cc6b058a85cabd3" - integrity sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ== +"@vitest/expect@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.0.18.tgz#361510d99fbf20eb814222e4afcb8539d79dc94d" + integrity sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ== dependencies: - "@vitest/spy" "3.2.4" + "@standard-schema/spec" "^1.0.0" + "@types/chai" "^5.2.2" + "@vitest/spy" "4.0.18" + "@vitest/utils" "4.0.18" + chai "^6.2.1" + tinyrainbow "^3.0.3" + +"@vitest/mocker@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.0.18.tgz#b9735da114ef65ea95652c5bdf13159c6fab4865" + integrity sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ== + dependencies: + "@vitest/spy" "4.0.18" estree-walker "^3.0.3" - magic-string "^0.30.17" + magic-string "^0.30.21" "@vitest/pretty-format@3.2.4": version "3.2.4" @@ -2063,6 +2037,30 @@ dependencies: tinyrainbow "^2.0.0" +"@vitest/pretty-format@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.0.18.tgz#fbccd4d910774072ec15463553edb8ca5ce53218" + integrity sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw== + dependencies: + tinyrainbow "^3.0.3" + +"@vitest/runner@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.0.18.tgz#c2c0a3ed226ec85e9312f9cc8c43c5b3a893a8b1" + integrity sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw== + dependencies: + "@vitest/utils" "4.0.18" + pathe "^2.0.3" + +"@vitest/snapshot@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.0.18.tgz#bcb40fd6d742679c2ac927ba295b66af1c6c34c5" + integrity sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA== + dependencies: + "@vitest/pretty-format" "4.0.18" + magic-string "^0.30.21" + pathe "^2.0.3" + "@vitest/spy@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599" @@ -2070,6 +2068,24 @@ dependencies: tinyspy "^4.0.3" +"@vitest/spy@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.0.18.tgz#ba0f20503fb6d08baf3309d690b3efabdfa88762" + integrity sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw== + +"@vitest/ui@^4.0.17": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-4.0.18.tgz#ae5765c34e98bb5d7294eb38d624a87f7c8b0399" + integrity sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ== + dependencies: + "@vitest/utils" "4.0.18" + fflate "^0.8.2" + flatted "^3.3.3" + pathe "^2.0.3" + sirv "^3.0.2" + tinyglobby "^0.2.15" + tinyrainbow "^3.0.3" + "@vitest/utils@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.4.tgz#c0813bc42d99527fb8c5b138c7a88516bca46fea" @@ -2079,6 +2095,14 @@ loupe "^3.1.4" tinyrainbow "^2.0.0" +"@vitest/utils@4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.0.18.tgz#9636b16d86a4152ec68a8d6859cff702896433d4" + integrity sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA== + dependencies: + "@vitest/pretty-format" "4.0.18" + tinyrainbow "^3.0.3" + "@volar/language-core@2.4.23", "@volar/language-core@~2.4.11": version "2.4.23" resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.23.tgz#deb6dbc5fdbafa9bb7ba691fc59cb196cdb856d3" @@ -2163,19 +2187,11 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.15.0, acorn@^8.9.0: +acorn@^8.15.0, acorn@^8.8.2, acorn@^8.9.0: version "8.15.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - ajv-draft-04@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8" @@ -2233,20 +2249,6 @@ alien-signals@^0.4.9: resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-0.4.14.tgz#9ff8f72a272300a51692f54bd9bbbada78fbf539" integrity sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q== -ansi-escapes@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-escapes@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.2.0.tgz#31b25afa3edd3efc09d98c2fee831d460ff06b49" - integrity sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw== - dependencies: - environment "^1.0.0" - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -2257,13 +2259,6 @@ ansi-regex@^6.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -2281,21 +2276,6 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== -anymatch@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -append-transform@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12" - integrity sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg== - dependencies: - default-require-extensions "^3.0.0" - archiver-utils@^5.0.0, archiver-utils@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" @@ -2322,22 +2302,24 @@ archiver@^7.0.1: tar-stream "^3.0.0" zip-stream "^6.0.1" -archy@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" - integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -argparse@^1.0.7, argparse@~1.0.9: +argparse@~1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.2.4: + version "1.2.6" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" + integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== + dependencies: + tslib "^2.0.0" aria-query@5.3.0: version "5.3.0" @@ -2346,11 +2328,102 @@ aria-query@5.3.0: dependencies: dequal "^2.0.3" -aria-query@^5.0.0: +aria-query@^5.0.0, aria-query@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== +array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + +array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: + version "3.1.9" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" + integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.24.0" + es-object-atoms "^1.1.1" + get-intrinsic "^1.3.0" + is-string "^1.1.1" + math-intrinsics "^1.1.0" + +array.prototype.findlast@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.findlastindex@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564" + integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-shim-unscopables "^1.1.0" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" + +array.prototype.flatmap@^1.3.2, array.prototype.flatmap@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" + +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + is-array-buffer "^3.0.4" + asn1.js@^4.10.1: version "4.10.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" @@ -2383,6 +2456,11 @@ assertion-error@^2.0.1: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== + ast-types@^0.16.1: version "0.16.1" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2" @@ -2390,6 +2468,20 @@ ast-types@^0.16.1: dependencies: tslib "^2.0.1" +ast-v8-to-istanbul@^0.3.10: + version "0.3.11" + resolved "https://registry.yarnpkg.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz#725b1f5e2ffdc8d71620cb5e78d6dc976d65e97a" + integrity sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.31" + estree-walker "^3.0.3" + js-tokens "^10.0.0" + +async-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== + async-lock@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.1.tgz#56b8718915a9b68b10fce2f2a9a3dddf765ef53f" @@ -2412,12 +2504,17 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axe-core@^4.2.0, axe-core@~4.11.0: +axe-core@^4.10.0, axe-core@^4.2.0: + version "4.11.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.1.tgz#052ff9b2cbf543f5595028b583e4763b40c78ea7" + integrity sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A== + +axe-core@~4.11.0: version "4.11.0" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.0.tgz#16f74d6482e343ff263d4f4503829e9ee91a86b6" integrity sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ== -axios@^1.12.1, axios@^1.6.1: +axios@^1.12.1: version "1.13.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687" integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA== @@ -2426,71 +2523,16 @@ axios@^1.12.1, axios@^1.6.1: form-data "^4.0.4" proxy-from-env "^1.1.0" +axobject-query@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" + integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== + b4a@^1.6.4: version "1.7.3" resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.7.3.tgz#24cf7ccda28f5465b66aec2bac69e32809bf112f" integrity sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q== -babel-jest@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-30.2.0.tgz#fd44a1ec9552be35ead881f7381faa7d8f3b95ac" - integrity sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw== - dependencies: - "@jest/transform" "30.2.0" - "@types/babel__core" "^7.20.5" - babel-plugin-istanbul "^7.0.1" - babel-preset-jest "30.2.0" - chalk "^4.1.2" - graceful-fs "^4.2.11" - slash "^3.0.0" - -babel-plugin-istanbul@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz#d8b518c8ea199364cf84ccc82de89740236daf92" - integrity sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@istanbuljs/load-nyc-config" "^1.0.0" - "@istanbuljs/schema" "^0.1.3" - istanbul-lib-instrument "^6.0.2" - test-exclude "^6.0.0" - -babel-plugin-jest-hoist@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz#94c250d36b43f95900f3a219241e0f4648191ce2" - integrity sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA== - dependencies: - "@types/babel__core" "^7.20.5" - -babel-preset-current-node-syntax@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" - integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== - dependencies: - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-import-attributes" "^7.24.7" - "@babel/plugin-syntax-import-meta" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - -babel-preset-jest@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz#04717843e561347781d6d7f69c81e6bcc3ed11ce" - integrity sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ== - dependencies: - babel-plugin-jest-hoist "30.2.0" - babel-preset-current-node-syntax "^1.2.0" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -2544,9 +2586,9 @@ base64-js@^1.3.1: integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== baseline-browser-mapping@^2.9.0: - version "2.9.5" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz#47f9549e0be1a84cd16651ac4c3b7d87a71408e6" - integrity sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA== + version "2.9.19" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488" + integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg== bcrypt-pbkdf@^1.0.2: version "1.0.2" @@ -2670,7 +2712,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.24.0: +browserslist@^4.24.0, browserslist@^4.28.0: version "4.28.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== @@ -2681,23 +2723,11 @@ browserslist@^4.24.0: node-releases "^2.0.27" update-browserslist-db "^1.2.0" -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" - integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== - dependencies: - node-int64 "^0.4.0" - buffer-crc32@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -2724,6 +2754,11 @@ buildcheck@~0.0.6: resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.7.tgz#07a5e76c10ead8fa67d9e4c587b68f49e8f29d61" integrity sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA== +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -2741,16 +2776,6 @@ byline@^5.0.0: resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" integrity sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q== -caching-transform@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-4.0.0.tgz#00d297a4206d71e2163c39eaffa8157ac0651f0f" - integrity sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA== - dependencies: - hasha "^5.0.0" - make-dir "^3.0.0" - package-hash "^4.0.0" - write-file-atomic "^3.0.0" - call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" @@ -2777,25 +2802,15 @@ call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: call-bind-apply-helpers "^1.0.2" get-intrinsic "^1.3.0" -callsites@^3.0.0, callsites@^3.1.0: +callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - caniuse-lite@^1.0.30001759: - version "1.0.30001760" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz#bdd1960fafedf8d5f04ff16e81460506ff9b798f" - integrity sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw== + version "1.0.30001767" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz#0279c498e862efb067938bba0a0aabafe8d0b730" + integrity sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ== chai@^5.2.0: version "5.3.3" @@ -2808,7 +2823,12 @@ chai@^5.2.0: loupe "^3.1.0" pathval "^2.0.0" -chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2, chalk@~4.1.2: +chai@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e" + integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== + +chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2816,29 +2836,10 @@ chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2, chalk@~4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^5.2.0: - version "5.6.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" - integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== - -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== - check-error@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" - integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== + version "2.1.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.3.tgz#2427361117b70cca8dc89680ead32b157019caf5" + integrity sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA== chownr@^1.1.1: version "1.1.4" @@ -2850,7 +2851,7 @@ ci-info@^3.7.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -ci-info@^4.2.0: +ci-info@^4.0.0, ci-info@^4.1.0, ci-info@^4.2.0: version "4.3.1" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.1.tgz#355ad571920810b5623e11d40232f443f16f1daa" integrity sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA== @@ -2864,29 +2865,17 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: safe-buffer "^5.2.1" to-buffer "^1.2.2" -cjs-module-lexer@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz#bff23b0609cc9afa428bd35f1918f7d03b448562" - integrity sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ== - classnames@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== +clean-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7" + integrity sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw== dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" + escape-string-regexp "^1.0.5" cliui@^8.0.1: version "8.0.1" @@ -2897,23 +2886,6 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - -collect-v8-coverage@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz#cc1f01eb8d02298cbc9a437c74c70ab4e5210b80" - integrity sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -2921,11 +2893,6 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" @@ -2938,21 +2905,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== - -commander@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" - integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - compare-versions@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.1.tgz#7af3cc1099ba37d244b3145a9af5201b629148a9" @@ -2974,18 +2926,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concurrently@^9.2.1: - version "9.2.1" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.2.1.tgz#248ea21b95754947be2dad9c3e4b60f18ca4e44f" - integrity sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng== - dependencies: - chalk "4.1.2" - rxjs "7.8.2" - shell-quote "1.8.3" - supports-color "8.1.1" - tree-kill "1.2.2" - yargs "17.7.2" - confbox@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" @@ -3006,16 +2946,18 @@ constants-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== -convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +core-js-compat@^3.38.1: + version "3.47.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.47.0.tgz#698224bbdbb6f2e3f39decdda4147b161e3772a3" + integrity sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ== + dependencies: + browserslist "^4.28.0" + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -3089,7 +3031,7 @@ create-require@^1.1.1: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: +cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -3126,13 +3068,37 @@ csstype@^3.2.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== -cwd@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/cwd/-/cwd-0.10.0.tgz#172400694057c22a13b0cf16162c7e4b7a7fe567" - integrity sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA== +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== dependencies: - find-pkg "^0.1.2" - fs-exists-sync "^0.1.0" + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-data-view "^1.0.1" date-names@^0.1.11: version "0.1.13" @@ -3144,22 +3110,24 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4.3: +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== - -dedent@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.0.tgz#c1f9445335f0175a96587be245a282ff451446ca" - integrity sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ== +dedent@^1.5.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.1.tgz#364661eea3d73f3faba7089214420ec2f8f13e15" + integrity sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg== deep-eql@^5.0.1: version "5.0.2" @@ -3171,31 +3139,19 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - default-browser-id@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.1.tgz#f7a7ccb8f5104bf8e0f71ba3b1ccfa5eafdb21e8" integrity sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q== default-browser@^5.2.1: - version "5.4.0" - resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.4.0.tgz#b55cf335bb0b465dd7c961a02cd24246aa434287" - integrity sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg== + version "5.5.0" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.5.0.tgz#2792e886f2422894545947cc80e1a444496c5976" + integrity sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw== dependencies: bundle-name "^4.1.0" default-browser-id "^5.0.0" -default-require-extensions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.1.tgz#bfae00feeaeada68c2ae256c62540f60b80625bd" - integrity sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw== - dependencies: - strip-bom "^4.0.0" - define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -3237,17 +3193,10 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -detect-newline@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" - integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== - -diffable-html@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/diffable-html/-/diffable-html-4.1.0.tgz#e7a2d1de187c4e23a59751b4e4c17483a058c696" - integrity sha512-++kyNek+YBLH8cLXS+iTj/Hiy2s5qkRJEJ8kgu/WHbFrVY2vz9xPFUT+fii2zGF0m1CaojDlQJjkfrCt7YWM1g== - dependencies: - htmlparser2 "^3.9.2" +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== diffie-hellman@^5.0.3: version "5.0.3" @@ -3288,6 +3237,13 @@ dockerode@^4.0.9: tar-fs "^2.1.4" uuid "^10.0.0" +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -3305,45 +3261,12 @@ dom-accessibility-api@^0.6.3: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - domain-browser@4.22.0: version "4.22.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.22.0.tgz#6ddd34220ec281f9a65d3386d267ddd35c491f9f" integrity sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw== -domelementtype@1, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domutils@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -dunder-proto@^1.0.1: +dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== @@ -3358,9 +3281,9 @@ eastasianwidth@^0.2.0: integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== electron-to-chromium@^1.5.263: - version "1.5.267" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7" - integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw== + version "1.5.286" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz#142be1ab5e1cd5044954db0e5898f60a4960384e" + integrity sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A== elliptic@^6.5.3, elliptic@^6.6.1: version "6.6.1" @@ -3375,11 +3298,6 @@ elliptic@^6.5.3, elliptic@^6.6.1: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -emittery@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" - integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -3402,26 +3320,11 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -entities@^4.5.0: +entities@^4.4.0, entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== -environment@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" - integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== - error-ex@^1.3.1: version "1.3.4" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" @@ -3429,6 +3332,66 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0, es-abstract@^1.24.1: + version "1.24.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.1.tgz#f0c131ed5ea1bb2411134a8dd94def09c46c7899" + integrity sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.3.0" + get-proto "^1.0.1" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-negative-zero "^2.0.3" + is-regex "^1.2.1" + is-set "^2.0.3" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.1" + math-intrinsics "^1.1.0" + object-inspect "^1.13.4" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.4" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + stop-iteration-iterator "^1.1.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.19" + es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" @@ -3439,6 +3402,33 @@ es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-iterator-helpers@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz#d979a9f686e2b0b72f88dbead7229924544720bc" + integrity sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.24.1" + es-errors "^1.3.0" + es-set-tostringtag "^2.1.0" + function-bind "^1.1.2" + get-intrinsic "^1.3.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + iterator.prototype "^1.1.5" + safe-array-concat "^1.1.3" + +es-module-lexer@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" @@ -3456,12 +3446,23 @@ es-set-tostringtag@^2.1.0: has-tostringtag "^1.0.2" hasown "^2.0.2" -es6-error@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" - integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== +es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" + integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== + dependencies: + hasown "^2.0.2" -"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0": +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== + dependencies: + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" + +"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", esbuild@^0.27.0: version "0.27.2" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.2.tgz#d83ed2154d5813a5367376bb2292a9296fc83717" integrity sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw== @@ -3493,38 +3494,6 @@ es6-error@^4.0.1: "@esbuild/win32-ia32" "0.27.2" "@esbuild/win32-x64" "0.27.2" -esbuild@^0.25.0: - version "0.25.12" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5" - integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg== - 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" - escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -3545,17 +3514,163 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-config-google@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a" + integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== + +eslint-config-prettier@^10.1.8: + version "10.1.8" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97" + integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w== + +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-module-utils@^2.12.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff" + integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== + dependencies: + debug "^3.2.7" + +eslint-plugin-deprecate@^0.8.7: + version "0.8.7" + resolved "https://registry.yarnpkg.com/eslint-plugin-deprecate/-/eslint-plugin-deprecate-0.8.7.tgz#256783c785ea8f87a64ce23bf7de7e78064a6a22" + integrity sha512-G+xyAozC3M+lwZUmLU69N1GkcB1hkmzvTSbKsbSzMlqH0NpFwgosSE7pv58EBEqndeniNJpuReymK7xqAGXhWg== + +eslint-plugin-import@^2.32.0: + version "2.32.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" + integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== + dependencies: + "@rtsao/scc" "^1.1.0" + array-includes "^3.1.9" + array.prototype.findlastindex "^1.2.6" + array.prototype.flat "^1.3.3" + array.prototype.flatmap "^1.3.3" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.12.1" + hasown "^2.0.2" + is-core-module "^2.16.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + object.groupby "^1.0.3" + object.values "^1.2.1" + semver "^6.3.1" + string.prototype.trimend "^1.0.9" + tsconfig-paths "^3.15.0" + +eslint-plugin-jsx-a11y@^6.10.2: + version "6.10.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" + integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== + dependencies: + aria-query "^5.3.2" + array-includes "^3.1.8" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "^4.10.0" + axobject-query "^4.1.0" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + hasown "^2.0.2" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + safe-regex-test "^1.0.3" + string.prototype.includes "^2.0.1" + eslint-plugin-matrix-org@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-3.0.0.tgz#0e5eaa556b6780dcd616b1381baec252df78b56d" integrity sha512-xOPMeyLxOgoB2SsPhJmIc+drorZmXozSBo3X9whk/62DvbVpJcnththOCGx8ljYScADLb+baNOeN+wKZqwkldw== -eslint-plugin-storybook@^10.0.7: - version "10.1.10" - resolved "https://registry.yarnpkg.com/eslint-plugin-storybook/-/eslint-plugin-storybook-10.1.10.tgz#030a55e58a5e96a513cbf873434a840abdaf6a7d" - integrity sha512-ITr6Aq3buR/DuDATkq1BafUVJLybyo676fY+tj9Zjd1Ak+UXBAMQcQ++tiBVVHm1RqADwM3b1o6bnWHK2fPPKw== +eslint-plugin-react-compiler@^19.1.0-rc.2: + version "19.1.0-rc.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.1.0-rc.2.tgz#83343e7422e00fa61e729af8e8468f0ddec37925" + integrity sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw== dependencies: - "@typescript-eslint/utils" "^8.8.1" + "@babel/core" "^7.24.4" + "@babel/parser" "^7.24.4" + "@babel/plugin-proposal-private-methods" "^7.18.6" + hermes-parser "^0.25.1" + zod "^3.22.4" + zod-validation-error "^3.0.3" + +eslint-plugin-react-hooks@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169" + integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA== + dependencies: + "@babel/core" "^7.24.4" + "@babel/parser" "^7.24.4" + hermes-parser "^0.25.1" + zod "^3.25.0 || ^4.0.0" + zod-validation-error "^3.5.0 || ^4.0.0" + +eslint-plugin-react@^7.37.5: + version "7.37.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" + integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== + dependencies: + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.3" + array.prototype.tosorted "^1.1.4" + doctrine "^2.1.0" + es-iterator-helpers "^1.2.1" + estraverse "^5.3.0" + hasown "^2.0.2" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.9" + object.fromentries "^2.0.8" + object.values "^1.2.1" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.12" + string.prototype.repeat "^1.0.0" + +eslint-plugin-storybook@^10.0.7: + version "10.2.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-storybook/-/eslint-plugin-storybook-10.2.4.tgz#01215136f9727610de55c19822a94e5458f694ef" + integrity sha512-D8a6Y+iun2MSOpgps0Vd/t8y9Y5ZZ7O2VeKqw2PCv2+b7yInqogOS2VBMSRZVfP8TTGQgDpbUK67k7KZEUC7Ng== + dependencies: + "@typescript-eslint/utils" "^8.48.0" + +eslint-plugin-unicorn@^56.0.0: + version "56.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz#d10a3df69ba885939075bdc95a65a0c872e940d4" + integrity sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + "@eslint-community/eslint-utils" "^4.4.0" + ci-info "^4.0.0" + clean-regexp "^1.0.0" + core-js-compat "^3.38.1" + esquery "^1.6.0" + globals "^15.9.0" + indent-string "^4.0.0" + is-builtin-module "^3.2.1" + jsesc "^3.0.2" + pluralize "^8.0.0" + read-pkg-up "^7.0.1" + regexp-tree "^0.1.27" + regjsparser "^0.10.0" + semver "^7.6.3" + strip-indent "^3.0.0" eslint-scope@^7.2.2: version "7.2.2" @@ -3619,6 +3734,15 @@ eslint@8: strip-ansi "^6.0.1" text-table "^0.2.0" +espree@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== + dependencies: + acorn "^8.15.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.1" + espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -3628,7 +3752,7 @@ espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0, esprima@~4.0.0: +esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -3640,6 +3764,13 @@ esquery@^1.4.2: dependencies: estraverse "^5.1.0" +esquery@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -3647,7 +3778,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^5.1.0, estraverse@^5.2.0: +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== @@ -3701,44 +3832,12 @@ except@^0.1.3: dependencies: indexof "0.0.1" -execa@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" +expect-type@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68" + integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== -exit-x@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/exit-x/-/exit-x-0.2.2.tgz#1f9052de3b8d99a696b10dad5bced9bdd5c3aa64" - integrity sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ== - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - -expand-tilde@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" - integrity sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q== - dependencies: - os-homedir "^1.0.1" - -expect-playwright@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/expect-playwright/-/expect-playwright-0.8.0.tgz#6d4ebe0bdbdd3c1693d880d97153b96a129ae4e8" - integrity sha512-+kn8561vHAY+dt+0gMqqj1oY+g5xWrsuGMk4QGxotT2WS545nVqqjs37z6hrYfIuucwqthzwJfCJUEYqixyljg== - -expect@30.2.0, expect@^30.0.0: +expect@^30.2.0: version "30.2.0" resolved "https://registry.yarnpkg.com/expect/-/expect-30.2.0.tgz#d4013bed267013c14bc1199cec8aa57cee9b5869" integrity sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw== @@ -3770,7 +3869,7 @@ fast-fifo@^1.2.0, fast-fifo@^1.3.2: resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -3792,18 +3891,26 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fb-watchman@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== - dependencies: - bser "2.1.1" - fdir@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== +fetch-mock@^12.6.0: + version "12.6.0" + resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-12.6.0.tgz#e5ed5d471eeeb29478260ce48385dca6773b105e" + integrity sha512-oAy0OqAvjAvduqCeWveBix7LLuDbARPqZZ8ERYtBcCURA3gy7EALA3XWq0tCNxsSg+RmmJqyaeeZlOCV9abv6w== + dependencies: + "@types/glob-to-regexp" "^0.4.4" + dequal "^2.0.3" + glob-to-regexp "^0.4.1" + regexparam "^3.0.0" + +fflate@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -3818,40 +3925,7 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -find-cache-dir@^3.2.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-file-up@^0.1.2: - version "0.1.3" - resolved "https://registry.yarnpkg.com/find-file-up/-/find-file-up-0.1.3.tgz#cf68091bcf9f300a40da411b37da5cce5a2fbea0" - integrity sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A== - dependencies: - fs-exists-sync "^0.1.0" - resolve-dir "^0.1.0" - -find-pkg@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/find-pkg/-/find-pkg-0.1.2.tgz#1bdc22c06e36365532e2a248046854b9788da557" - integrity sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw== - dependencies: - find-file-up "^0.1.2" - -find-process@^1.4.4: - version "1.4.11" - resolved "https://registry.yarnpkg.com/find-process/-/find-process-1.4.11.tgz#f7246251d396b35b9ae41fff7b87137673567fcc" - integrity sha512-mAOh9gGk9WZ4ip5UjV0o6Vb4SrfnAmtsFNzkMRH9HQiFXVQnDyQFrSHTK5UoG6E+KV+s+cIznbtwpfN41l2nFA== - dependencies: - chalk "~4.1.2" - commander "^12.1.0" - loglevel "^1.9.2" - -find-up@^4.0.0, find-up@^4.1.0: +find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -3883,7 +3957,7 @@ flat-cache@^3.0.4: keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.2.9: +flatted@^3.2.9, flatted@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== @@ -3893,7 +3967,7 @@ follow-redirects@^1.15.6: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== -for-each@^0.3.5: +for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== @@ -3905,14 +3979,6 @@ foreachasync@^3.0.0: resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" integrity sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw== -foreground-child@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" - integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^3.0.2" - foreground-child@^3.1.0, foreground-child@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" @@ -3932,21 +3998,11 @@ form-data@^4.0.4: hasown "^2.0.2" mime-types "^2.1.12" -fromentries@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" - integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-exists-sync@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" - integrity sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg== - fs-extra@^10.0.0: version "10.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" @@ -3975,7 +4031,7 @@ fsevents@2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -fsevents@^2.3.3, fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -3985,6 +4041,23 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + generator-function@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2" @@ -3995,12 +4068,12 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -4016,17 +4089,17 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@ hasown "^2.0.2" math-intrinsics "^1.1.0" -get-package-type@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" - integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== get-port@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-7.1.0.tgz#d5a500ebfc7aa705294ec2b83cc38c5d0e364fec" integrity sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw== -get-proto@^1.0.1: +get-proto@^1.0.0, get-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== @@ -4034,15 +4107,14 @@ get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" -get-stdin@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" - integrity sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA== - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" glob-parent@^6.0.2: version "6.0.2" @@ -4051,7 +4123,12 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^10.0.0, glob@^10.3.10: +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^10.0.0: version "10.5.0" resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== @@ -4075,7 +4152,16 @@ glob@^11.1.0: package-json-from-dist "^1.0.0" path-scurry "^2.0.0" -glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.0.tgz#9d9233a4a274fc28ef7adce5508b7ef6237a1be3" + integrity sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA== + dependencies: + minimatch "^10.1.1" + minipass "^7.1.2" + path-scurry "^2.0.0" + +glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -4087,24 +4173,6 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -global-modules@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" - integrity sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA== - dependencies: - global-prefix "^0.1.4" - is-windows "^0.2.0" - -global-prefix@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" - integrity sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw== - dependencies: - homedir-polyfill "^1.0.0" - ini "^1.3.4" - is-windows "^0.2.0" - which "^1.2.12" - globals@^13.19.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" @@ -4112,17 +4180,25 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" -glur@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/glur/-/glur-1.1.2.tgz#f20ea36db103bfc292343921f1f91e83c3467689" - integrity sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA== +globals@^15.9.0: + version "15.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8" + integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== + +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4: +graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4132,10 +4208,10 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== has-flag@^4.0.0: version "4.0.0" @@ -4149,6 +4225,13 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" + has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" @@ -4187,14 +4270,6 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" -hasha@^5.0.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" - integrity sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ== - dependencies: - is-stream "^2.0.0" - type-fest "^0.8.0" - hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -4207,6 +4282,18 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hermes-estree@0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480" + integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw== + +hermes-parser@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1" + integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA== + dependencies: + hermes-estree "0.25.1" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -4216,40 +4303,21 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -homedir-polyfill@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -htmlparser2@^3.9.2: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -4260,6 +4328,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== +ignore@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + import-fresh@^3.2.1: version "3.3.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" @@ -4273,14 +4346,6 @@ import-lazy@~4.0.0: resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== -import-local@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" - integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -4309,10 +4374,14 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.4: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" is-arguments@^1.0.4: version "1.2.0" @@ -4322,23 +4391,94 @@ is-arguments@^1.0.4: call-bound "^1.0.2" has-tostringtag "^1.0.2" +is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-async-function@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== + dependencies: + async-function "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== + dependencies: + has-bigints "^1.0.2" + +is-boolean-object@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-builtin-module@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.16.0, is-core-module@^2.16.1: +is-ci@^4.0.0, is-ci@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-4.1.0.tgz#38780f471e4f67e3bec3f809d90cd74f40a0cfdc" + integrity sha512-Ab9bQDQ11lWootZUI5qxgN2ZXwxNI5hTwnsvOc1wyxQ7zQ8OkEDw79mI0+9jI3x432NfwbVRru+3noJfXF6lSQ== + dependencies: + ci-info "^4.1.0" + +is-core-module@^2.13.0, is-core-module@^2.16.0, is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: hasown "^2.0.2" +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== + dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + is-typed-array "^1.1.13" + +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + is-docker@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" @@ -4354,17 +4494,19 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== + dependencies: + call-bound "^1.0.3" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" - integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== - -is-generator-function@^1.0.7: +is-generator-function@^1.0.10, is-generator-function@^1.0.7: version "1.1.2" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.2.tgz#ae3b61e3d5ea4e4839b90bad22b02335051a17d5" integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== @@ -4389,6 +4531,11 @@ is-inside-container@^1.0.0: dependencies: is-docker "^3.0.0" +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + is-nan@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" @@ -4397,6 +4544,19 @@ is-nan@^1.3.2: call-bind "^1.0.0" define-properties "^1.1.3" +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -4417,32 +4577,66 @@ is-regex@^1.2.1: has-tostringtag "^1.0.2" hasown "^2.0.2" -is-stream@^2.0.0, is-stream@^2.0.1: +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + +is-stream@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-typed-array@^1.1.14, is-typed-array@^1.1.3: +is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15, is-typed-array@^1.1.3: version "1.1.15" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== dependencies: which-typed-array "^1.1.16" -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== -is-windows@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" - integrity sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q== +is-weakref@^1.0.2, is-weakref@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== + dependencies: + call-bound "^1.0.3" -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" is-wsl@^2.1.1: version "2.2.0" @@ -4478,52 +4672,12 @@ isomorphic-timers-promises@^1.0.1: resolved "https://registry.yarnpkg.com/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz#e4137c24dbc54892de8abae3a4b5c1ffff381598" integrity sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ== -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== -istanbul-lib-hook@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz#8f84c9434888cc6b1d0a9d7092a76d239ebf0cc6" - integrity sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ== - dependencies: - append-transform "^2.0.0" - -istanbul-lib-instrument@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== - dependencies: - "@babel/core" "^7.7.5" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" - semver "^6.3.0" - -istanbul-lib-instrument@^6.0.0, istanbul-lib-instrument@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" - integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== - dependencies: - "@babel/core" "^7.23.9" - "@babel/parser" "^7.23.9" - "@istanbuljs/schema" "^0.1.3" - istanbul-lib-coverage "^3.2.0" - semver "^7.5.4" - -istanbul-lib-processinfo@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz#366d454cd0dcb7eb6e0e419378e60072c8626169" - integrity sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg== - dependencies: - archy "^1.0.0" - cross-spawn "^7.0.3" - istanbul-lib-coverage "^3.2.0" - p-map "^3.0.0" - rimraf "^3.0.0" - uuid "^8.3.2" - -istanbul-lib-report@^3.0.0: +istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== @@ -4532,25 +4686,7 @@ istanbul-lib-report@^3.0.0: make-dir "^4.0.0" supports-color "^7.1.0" -istanbul-lib-source-maps@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" - integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" - -istanbul-lib-source-maps@^5.0.0: - version "5.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441" - integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== - dependencies: - "@jridgewell/trace-mapping" "^0.3.23" - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - -istanbul-reports@^3.0.2, istanbul-reports@^3.1.3: +istanbul-reports@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== @@ -4558,6 +4694,18 @@ istanbul-reports@^3.0.2, istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +iterator.prototype@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" + integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== + dependencies: + define-data-property "^1.1.4" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + get-proto "^1.0.0" + has-symbols "^1.1.0" + set-function-name "^2.0.2" + jackspeak@^3.1.2: version "3.4.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" @@ -4574,87 +4722,6 @@ jackspeak@^4.1.1: dependencies: "@isaacs/cliui" "^8.0.2" -jest-changed-files@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-30.2.0.tgz#602266e478ed554e1e1469944faa7efd37cee61c" - integrity sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ== - dependencies: - execa "^5.1.1" - jest-util "30.2.0" - p-limit "^3.1.0" - -jest-circus@30.2.0, jest-circus@^30.0.4: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-30.2.0.tgz#98b8198b958748a2f322354311023d1d02e7603f" - integrity sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg== - dependencies: - "@jest/environment" "30.2.0" - "@jest/expect" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/types" "30.2.0" - "@types/node" "*" - chalk "^4.1.2" - co "^4.6.0" - dedent "^1.6.0" - is-generator-fn "^2.1.0" - jest-each "30.2.0" - jest-matcher-utils "30.2.0" - jest-message-util "30.2.0" - jest-runtime "30.2.0" - jest-snapshot "30.2.0" - jest-util "30.2.0" - p-limit "^3.1.0" - pretty-format "30.2.0" - pure-rand "^7.0.0" - slash "^3.0.0" - stack-utils "^2.0.6" - -jest-cli@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-30.2.0.tgz#1780f8e9d66bf84a10b369aea60aeda7697dcc67" - integrity sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA== - dependencies: - "@jest/core" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/types" "30.2.0" - chalk "^4.1.2" - exit-x "^0.2.2" - import-local "^3.2.0" - jest-config "30.2.0" - jest-util "30.2.0" - jest-validate "30.2.0" - yargs "^17.7.2" - -jest-config@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-30.2.0.tgz#29df8c50e2ad801cc59c406b50176c18c362a90b" - integrity sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA== - dependencies: - "@babel/core" "^7.27.4" - "@jest/get-type" "30.1.0" - "@jest/pattern" "30.0.1" - "@jest/test-sequencer" "30.2.0" - "@jest/types" "30.2.0" - babel-jest "30.2.0" - chalk "^4.1.2" - ci-info "^4.2.0" - deepmerge "^4.3.1" - glob "^10.3.10" - graceful-fs "^4.2.11" - jest-circus "30.2.0" - jest-docblock "30.2.0" - jest-environment-node "30.2.0" - jest-regex-util "30.0.1" - jest-resolve "30.2.0" - jest-runner "30.2.0" - jest-util "30.2.0" - jest-validate "30.2.0" - micromatch "^4.0.8" - parse-json "^5.2.0" - pretty-format "30.2.0" - slash "^3.0.0" - strip-json-comments "^3.1.1" - jest-diff@30.2.0: version "30.2.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.2.0.tgz#e3ec3a6ea5c5747f605c9e874f83d756cba36825" @@ -4665,86 +4732,6 @@ jest-diff@30.2.0: chalk "^4.1.2" pretty-format "30.2.0" -jest-docblock@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-30.2.0.tgz#42cd98d69f887e531c7352309542b1ce4ee10256" - integrity sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA== - dependencies: - detect-newline "^3.1.0" - -jest-each@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-30.2.0.tgz#39e623ae71641c2ac3ee69b3ba3d258fce8e768d" - integrity sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ== - dependencies: - "@jest/get-type" "30.1.0" - "@jest/types" "30.2.0" - chalk "^4.1.2" - jest-util "30.2.0" - pretty-format "30.2.0" - -jest-environment-node@30.2.0, jest-environment-node@^30.0.4: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-30.2.0.tgz#3def7980ebd2fd86e74efd4d2e681f55ab38da0f" - integrity sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA== - dependencies: - "@jest/environment" "30.2.0" - "@jest/fake-timers" "30.2.0" - "@jest/types" "30.2.0" - "@types/node" "*" - jest-mock "30.2.0" - jest-util "30.2.0" - jest-validate "30.2.0" - -jest-haste-map@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-30.2.0.tgz#808e3889f288603ac70ff0ac047598345a66022e" - integrity sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw== - dependencies: - "@jest/types" "30.2.0" - "@types/node" "*" - anymatch "^3.1.3" - fb-watchman "^2.0.2" - graceful-fs "^4.2.11" - jest-regex-util "30.0.1" - jest-util "30.2.0" - jest-worker "30.2.0" - micromatch "^4.0.8" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.3" - -jest-image-snapshot@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/jest-image-snapshot/-/jest-image-snapshot-6.5.1.tgz#cc714aff86187ecf38f90c506b443769aff87499" - integrity sha512-xlJFufgfY2Z4DsRsjcnTwxuynvo1bKdhf4OfcEftNuUAK+BwSCUtPmwlBGJhQ0XJXfm9JMAi/4BhQiHbaV8HrA== - dependencies: - chalk "^4.0.0" - get-stdin "^5.0.1" - glur "^1.1.2" - lodash "^4.17.4" - pixelmatch "^5.1.0" - pngjs "^3.4.0" - ssim.js "^3.1.1" - -jest-junit@^16.0.0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-16.0.0.tgz#d838e8c561cf9fdd7eb54f63020777eee4136785" - integrity sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ== - dependencies: - mkdirp "^1.0.4" - strip-ansi "^6.0.1" - uuid "^8.3.2" - xml "^1.0.1" - -jest-leak-detector@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz#292fdca7b7c9cf594e1e570ace140b01d8beb736" - integrity sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ== - dependencies: - "@jest/get-type" "30.1.0" - pretty-format "30.2.0" - jest-matcher-utils@30.2.0: version "30.2.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz#69a0d4c271066559ec8b0d8174829adc3f23a783" @@ -4779,144 +4766,11 @@ jest-mock@30.2.0: "@types/node" "*" jest-util "30.2.0" -jest-pnp-resolver@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" - integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== - -jest-process-manager@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/jest-process-manager/-/jest-process-manager-0.4.0.tgz#fb05c8e09ad400fd038436004815653bb98f4e8b" - integrity sha512-80Y6snDyb0p8GG83pDxGI/kQzwVTkCxc7ep5FPe/F6JYdvRDhwr6RzRmPSP7SEwuLhxo80lBS/NqOdUIbHIfhw== - dependencies: - "@types/wait-on" "^5.2.0" - chalk "^4.1.0" - cwd "^0.10.0" - exit "^0.1.2" - find-process "^1.4.4" - prompts "^2.4.1" - signal-exit "^3.0.3" - spawnd "^5.0.0" - tree-kill "^1.2.2" - wait-on "^7.0.0" - -jest-regex-util@30.0.1, jest-regex-util@^30.0.0: +jest-regex-util@30.0.1: version "30.0.1" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b" integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== -jest-resolve-dependencies@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz#3370e2c0b49cc560f6a7e8ec3a59dd99525e1a55" - integrity sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w== - dependencies: - jest-regex-util "30.0.1" - jest-snapshot "30.2.0" - -jest-resolve@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-30.2.0.tgz#2e2009cbd61e8f1f003355d5ec87225412cebcd7" - integrity sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A== - dependencies: - chalk "^4.1.2" - graceful-fs "^4.2.11" - jest-haste-map "30.2.0" - jest-pnp-resolver "^1.2.3" - jest-util "30.2.0" - jest-validate "30.2.0" - slash "^3.0.0" - unrs-resolver "^1.7.11" - -jest-runner@30.2.0, jest-runner@^30.0.4: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-30.2.0.tgz#c62b4c3130afa661789705e13a07bdbcec26a114" - integrity sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ== - dependencies: - "@jest/console" "30.2.0" - "@jest/environment" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" - "@types/node" "*" - chalk "^4.1.2" - emittery "^0.13.1" - exit-x "^0.2.2" - graceful-fs "^4.2.11" - jest-docblock "30.2.0" - jest-environment-node "30.2.0" - jest-haste-map "30.2.0" - jest-leak-detector "30.2.0" - jest-message-util "30.2.0" - jest-resolve "30.2.0" - jest-runtime "30.2.0" - jest-util "30.2.0" - jest-watcher "30.2.0" - jest-worker "30.2.0" - p-limit "^3.1.0" - source-map-support "0.5.13" - -jest-runtime@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-30.2.0.tgz#395ea792cde048db1b0cd1a92dc9cb9f1921bf8a" - integrity sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg== - dependencies: - "@jest/environment" "30.2.0" - "@jest/fake-timers" "30.2.0" - "@jest/globals" "30.2.0" - "@jest/source-map" "30.0.1" - "@jest/test-result" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" - "@types/node" "*" - chalk "^4.1.2" - cjs-module-lexer "^2.1.0" - collect-v8-coverage "^1.0.2" - glob "^10.3.10" - graceful-fs "^4.2.11" - jest-haste-map "30.2.0" - jest-message-util "30.2.0" - jest-mock "30.2.0" - jest-regex-util "30.0.1" - jest-resolve "30.2.0" - jest-snapshot "30.2.0" - jest-util "30.2.0" - slash "^3.0.0" - strip-bom "^4.0.0" - -jest-serializer-html@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/jest-serializer-html/-/jest-serializer-html-7.1.0.tgz#0cfea8a03b9b82bc420fd2cb969bd76713a87c08" - integrity sha512-xYL2qC7kmoYHJo8MYqJkzrl/Fdlx+fat4U1AqYg+kafqwcKPiMkOcjWHPKhueuNEgr+uemhGc+jqXYiwCyRyLA== - dependencies: - diffable-html "^4.1.0" - -jest-snapshot@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-30.2.0.tgz#266fbbb4b95fc4665ce6f32f1f38eeb39f4e26d0" - integrity sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA== - dependencies: - "@babel/core" "^7.27.4" - "@babel/generator" "^7.27.5" - "@babel/plugin-syntax-jsx" "^7.27.1" - "@babel/plugin-syntax-typescript" "^7.27.1" - "@babel/types" "^7.27.3" - "@jest/expect-utils" "30.2.0" - "@jest/get-type" "30.1.0" - "@jest/snapshot-utils" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" - babel-preset-current-node-syntax "^1.2.0" - chalk "^4.1.2" - expect "30.2.0" - graceful-fs "^4.2.11" - jest-diff "30.2.0" - jest-matcher-utils "30.2.0" - jest-message-util "30.2.0" - jest-util "30.2.0" - pretty-format "30.2.0" - semver "^7.7.2" - synckit "^0.11.8" - jest-util@30.2.0: version "30.2.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.2.0.tgz#5142adbcad6f4e53c2776c067a4db3c14f913705" @@ -4929,95 +4783,21 @@ jest-util@30.2.0: graceful-fs "^4.2.11" picomatch "^4.0.2" -jest-validate@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-30.2.0.tgz#273eaaed4c0963b934b5b31e96289edda6e0a2ef" - integrity sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw== - dependencies: - "@jest/get-type" "30.1.0" - "@jest/types" "30.2.0" - camelcase "^6.3.0" - chalk "^4.1.2" - leven "^3.1.0" - pretty-format "30.2.0" - -jest-watch-typeahead@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-3.0.1.tgz#50cb9653190228b8ebd489b37645067ee0bb7584" - integrity sha512-SFmHcvdueTswZlVhPCWfLXMazvwZlA2UZTrcE7MC3NwEVeWvEcOx6HUe+igMbnmA6qowuBSW4in8iC6J2EYsgQ== - dependencies: - ansi-escapes "^7.0.0" - chalk "^5.2.0" - jest-regex-util "^30.0.0" - jest-watcher "^30.0.0" - slash "^5.0.0" - string-length "^6.0.0" - strip-ansi "^7.0.1" - -jest-watcher@30.2.0, jest-watcher@^30.0.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-30.2.0.tgz#f9c055de48e18c979e7756a3917e596e2d69b07b" - integrity sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg== - dependencies: - "@jest/test-result" "30.2.0" - "@jest/types" "30.2.0" - "@types/node" "*" - ansi-escapes "^4.3.2" - chalk "^4.1.2" - emittery "^0.13.1" - jest-util "30.2.0" - string-length "^4.0.2" - -jest-worker@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-30.2.0.tgz#fd5c2a36ff6058ec8f74366ec89538cc99539d26" - integrity sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g== - dependencies: - "@types/node" "*" - "@ungap/structured-clone" "^1.3.0" - jest-util "30.2.0" - merge-stream "^2.0.0" - supports-color "^8.1.1" - -jest@^30.0.4, jest@^30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-30.2.0.tgz#9f0a71e734af968f26952b5ae4b724af82681630" - integrity sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A== - dependencies: - "@jest/core" "30.2.0" - "@jest/types" "30.2.0" - import-local "^3.2.0" - jest-cli "30.2.0" - jju@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== -joi@^17.11.0: - version "17.13.3" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" - integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== - dependencies: - "@hapi/hoek" "^9.3.0" - "@hapi/topo" "^5.1.0" - "@sideway/address" "^4.1.5" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" +js-tokens@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-10.0.0.tgz#dffe7599b4a8bb7fe30aff8d0235234dffb79831" + integrity sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: - version "3.14.2" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0" - integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -5030,6 +4810,11 @@ jsesc@^3.0.2: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -5066,16 +4851,18 @@ json-stable-stringify@^1.0.2: jsonify "^0.0.1" object-keys "^1.1.1" +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonc-parser@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4" - integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== - jsonfile@^6.0.1: version "6.2.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" @@ -5090,6 +4877,16 @@ jsonify@^0.0.1: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -5104,16 +4901,23 @@ klaw-sync@^6.0.0: dependencies: graceful-fs "^4.1.11" -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - kolorist@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c" integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== +language-subtag-registry@^0.3.20: + version "0.3.23" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" + integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== + +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== + dependencies: + language-subtag-registry "^0.3.20" + lazystream@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" @@ -5121,11 +4925,6 @@ lazystream@^1.0.0: dependencies: readable-stream "^2.0.5" -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -5139,6 +4938,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + local-pkg@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-1.1.2.tgz#c03d208787126445303f8161619dc701afa4abb5" @@ -5162,41 +4968,38 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== +lodash-es@^4.17.23: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0" + integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg== lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== -lodash.flattendeep@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" - integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ== - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.4, lodash@~4.17.15: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -loglevel@^1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08" - integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg== +lodash@^4.17.15, lodash@^4.17.21, lodash@~4.17.15: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== long@^5.0.0: version "5.3.2" resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + loupe@^3.1.0, loupe@^3.1.4: version "3.2.1" resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76" @@ -5208,9 +5011,9 @@ lru-cache@^10.2.0: integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== lru-cache@^11.0.0: - version "11.2.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.4.tgz#ecb523ebb0e6f4d837c807ad1abaea8e0619770d" - integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== + version "11.2.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.5.tgz#6811ae01652ae5d749948cdd80bcc22218c6744f" + integrity sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw== lru-cache@^5.1.1: version "5.1.1" @@ -5226,12 +5029,17 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== -magic-string@^0.30.0, magic-string@^0.30.17: +magic-string@^0.30.0, magic-string@^0.30.17, magic-string@^0.30.21: version "0.30.21" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== @@ -5245,6 +5053,15 @@ magic-string@^0.30.3: dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" +magicast@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.5.1.tgz#518959aea78851cd35d4bb0da92f780db3f606d3" + integrity sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw== + dependencies: + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + source-map-js "^1.2.1" + mailpit-api@^1.2.0: version "1.5.4" resolved "https://registry.yarnpkg.com/mailpit-api/-/mailpit-api-1.5.4.tgz#b53e0d437c3a2dcd22a20db9dcadc99ffb3b789d" @@ -5252,13 +5069,6 @@ mailpit-api@^1.2.0: dependencies: axios "^1.12.1" -make-dir@^3.0.0, make-dir@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -5266,25 +5076,30 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -makeerror@1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" - integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== dependencies: - tmpl "1.0.5" + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -matrix-web-i18n@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/matrix-web-i18n/-/matrix-web-i18n-3.4.0.tgz#2f088a2ec2cb4598092b7e9dd2f24f65d820df0e" - integrity sha512-8Bftf3LkACR6oy0iXYuIsDuvHjoJoOCWA+gDuy/lvyPwEpwViMs3XP5mJsXla+51SZEULZRUUtTreufH6xVA8g== +matrix-web-i18n@3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/matrix-web-i18n/-/matrix-web-i18n-3.6.0.tgz#50efd34e454449685866155336e2ce3432e7a813" + integrity sha512-oY4YZCU8OqDZzmCiJpo8XrATa7XNOB5oFyIpE7O0jjaIEeu6LnQrHmm/fpsYkPUENVzGJm1YdoOtvcBrQx+h2g== dependencies: - "@babel/parser" "^7.18.5" - "@babel/traverse" "^7.18.5" + "@babel/parser" "^7.28.5" + "@babel/traverse" "^7.28.5" lodash "^4.17.21" minimist "^1.2.8" walk "^2.3.15" @@ -5298,10 +5113,17 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -merge-stream@^2.0.0: +mdurl@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + +memoize@^10.1.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/memoize/-/memoize-10.2.0.tgz#593f8066b922b791390d05e278dbeff163dad956" + integrity sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA== + dependencies: + mimic-function "^5.0.1" micromatch@^4.0.2, micromatch@^4.0.8: version "4.0.8" @@ -5331,10 +5153,10 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-function@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== min-indent@^1.0.0: version "1.0.1" @@ -5365,7 +5187,7 @@ minimatch@^10.1.1: dependencies: "@isaacs/brace-expansion" "^5.0.0" -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -5379,14 +5201,14 @@ minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.3, minimatch@^9.0.4: +minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" -minimist@^1.2.6, minimist@^1.2.8: +minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -5406,6 +5228,11 @@ mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + mlly@^1.7.4: version "1.8.0" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.8.0.tgz#e074612b938af8eba1eaf43299cbc89cb72d824e" @@ -5416,7 +5243,12 @@ mlly@^1.7.4: pkg-types "^1.3.1" ufo "^1.6.1" -ms@^2.1.3: +mrmime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc" + integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ== + +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -5436,34 +5268,17 @@ nanoid@^3.3.11: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== -napi-postinstall@^0.3.0: - version "0.3.4" - resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz#7af256d6588b5f8e952b9190965d6b019653bbb9" - integrity sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== - -node-preload@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301" - integrity sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ== - dependencies: - process-on-spawn "^1.0.0" - node-releases@^2.0.27: version "2.0.27" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== -node-stdlib-browser@^1.2.0: +node-stdlib-browser@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz#f41fa554f720a3df951e40339f4d92ac512222ac" integrity sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw== @@ -5496,52 +5311,27 @@ node-stdlib-browser@^1.2.0: util "^0.12.4" vm-browserify "^1.0.1" +normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -nyc@^15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.1.0.tgz#1335dae12ddc87b6e249d5a1994ca4bdaea75f02" - integrity sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A== - dependencies: - "@istanbuljs/load-nyc-config" "^1.0.0" - "@istanbuljs/schema" "^0.1.2" - caching-transform "^4.0.0" - convert-source-map "^1.7.0" - decamelize "^1.2.0" - find-cache-dir "^3.2.0" - find-up "^4.1.0" - foreground-child "^2.0.0" - get-package-type "^0.1.0" - glob "^7.1.6" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-hook "^3.0.0" - istanbul-lib-instrument "^4.0.0" - istanbul-lib-processinfo "^2.0.2" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - make-dir "^3.0.0" - node-preload "^0.2.1" - p-map "^3.0.0" - process-on-spawn "^1.0.0" - resolve-from "^5.0.0" - rimraf "^3.0.0" - signal-exit "^3.0.2" - spawn-wrap "^2.0.0" - test-exclude "^6.0.0" - yargs "^15.0.2" - -object-inspect@^1.13.3: +object-inspect@^1.13.3, object-inspect@^1.13.4: version "1.13.4" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== @@ -5559,7 +5349,7 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.4: +object.assign@^4.1.4, object.assign@^4.1.7: version "4.1.7" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== @@ -5571,6 +5361,50 @@ object.assign@^4.1.4: has-symbols "^1.1.0" object-keys "^1.1.1" +object.entries@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" + integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-object-atoms "^1.1.1" + +object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.groupby@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + +object.values@^1.1.6, object.values@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +obug@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/obug/-/obug-2.1.1.tgz#2cba74ff241beb77d63055ddf4cd1e9f90b538be" + integrity sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -5578,13 +5412,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - open@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/open/-/open-10.2.0.tgz#b9d855be007620e80b6fb05fac98141fe62db73c" @@ -5620,10 +5447,14 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== -os-homedir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ== +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" p-limit@^2.2.0: version "2.3.0" @@ -5632,7 +5463,7 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2, p-limit@^3.1.0: +p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -5653,29 +5484,12 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" - integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ== - dependencies: - aggregate-error "^3.0.0" - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -package-hash@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506" - integrity sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ== - dependencies: - graceful-fs "^4.1.15" - hasha "^5.0.0" - lodash.flattendeep "^4.4.0" - release-zalgo "^1.0.0" - -package-json-from-dist@^1.0.0: +package-json-from-dist@^1.0.0, package-json-from-dist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== @@ -5703,7 +5517,7 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.9: pbkdf2 "^3.1.5" safe-buffer "^5.2.1" -parse-json@^5.2.0: +parse-json@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -5713,11 +5527,6 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== - patch-package@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.1.tgz#79d02f953f711e06d1f8949c8a13e5d3d7ba1a60" @@ -5753,7 +5562,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.0.0, path-key@^3.1.0: +path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -5779,7 +5588,7 @@ path-scurry@^2.0.0: lru-cache "^11.0.0" minipass "^7.1.2" -pathe@^2.0.1, pathe@^2.0.3: +pathe@^2.0.0, pathe@^2.0.1, pathe@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== @@ -5806,7 +5615,7 @@ picocolors@1.1.1, picocolors@^1.1.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.3.1: +picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -5816,24 +5625,12 @@ picomatch@^4.0.2, picomatch@^4.0.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== -pirates@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" - integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== - -pixelmatch@^5.1.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.3.0.tgz#5e5321a7abedfb7962d60dbf345deda87cb9560a" - integrity sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q== +pixelmatch@7.1.0, pixelmatch@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-7.1.0.tgz#9d59bddc8c779340e791106c0f245ac33ae4d113" + integrity sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng== dependencies: - pngjs "^6.0.0" - -pkg-dir@^4.1.0, pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" + pngjs "^7.0.0" pkg-dir@^5.0.0: version "5.0.0" @@ -5860,34 +5657,34 @@ pkg-types@^2.3.0: exsolve "^1.0.7" pathe "^2.0.3" -playwright-core@1.57.0, playwright-core@>=1.2.0: - version "1.57.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.57.0.tgz#3dcc9a865af256fa9f0af0d67fc8dd54eecaebf5" - integrity sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ== +playwright-core@1.58.1: + version "1.58.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.58.1.tgz#d63be2c9b7dcbdb035beddd4b42437bd3ca89107" + integrity sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg== -playwright@1.57.0, playwright@^1.14.0: - version "1.57.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.57.0.tgz#74d1dacff5048dc40bf4676940b1901e18ad0f46" - integrity sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw== +playwright@1.58.1: + version "1.58.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.58.1.tgz#63300e77a604c77264e1b499c0d94b54ed96d6ba" + integrity sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ== dependencies: - playwright-core "1.57.0" + playwright-core "1.58.1" optionalDependencies: fsevents "2.3.2" +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + pluralizers@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/pluralizers/-/pluralizers-0.1.7.tgz#8d38dd0a1b660e739b10ab2eab10b684c9d50142" integrity sha512-mw6AejUiCaMQ6uPN9ObjJDTnR5AnBSmnHHy3uVTbxrSFSxO5scfwpTs8Dxyb6T2v7GSulhvOq+pm9y+hXUvtOA== -pngjs@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" - integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== - -pngjs@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821" - integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg== +pngjs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26" + integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow== possible-typed-array-names@^1.0.0: version "1.1.0" @@ -5909,11 +5706,11 @@ prelude-ls@^1.2.1: integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prettier@^3.6.2: - version "3.7.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f" - integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA== + version "3.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" + integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== -pretty-format@30.2.0, pretty-format@^30.0.0: +pretty-format@30.2.0: version "30.2.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.2.0.tgz#2d44fe6134529aed18506f6d11509d8a62775ebe" integrity sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA== @@ -5936,25 +5733,19 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process-on-spawn@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/process-on-spawn/-/process-on-spawn-1.1.0.tgz#9d5999ba87b3bf0a8acb05322d69f2f5aa4fb763" - integrity sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q== - dependencies: - fromentries "^1.2.0" - process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -prompts@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" proper-lockfile@^4.1.2: version "4.1.2" @@ -6015,6 +5806,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -6025,15 +5821,10 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -pure-rand@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-7.0.1.tgz#6f53a5a9e3e4a47445822af96821ca509ed37566" - integrity sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ== - qs@^6.12.3: - version "6.14.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" - integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + version "6.14.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159" + integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ== dependencies: side-channel "^1.1.0" @@ -6089,12 +5880,17 @@ react-docgen@^8.0.0, react-docgen@^8.0.2: strip-indent "^4.0.0" "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0": - version "19.2.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.1.tgz#ce3527560bda4f997e47d10dab754825b3061f59" - integrity sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg== + version "19.2.4" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591" + integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ== dependencies: scheduler "^0.27.0" +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" @@ -6110,10 +5906,66 @@ react-merge-refs@^3.0.2: resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-3.0.2.tgz#483b4e8029f89d805c4e55c8d22e9b8f77e3b58e" integrity sha512-MSZAfwFfdbEvwkKWP5EI5chuLYnNUxNS7vyS0i1Jp+wtd8J4Ga2ddzhaE68aMol2Z4vCnRM/oGOo1a3V75UPlw== +react-refresh@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" + integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== + +react-remove-scroll-bar@^2.3.7: + version "2.3.8" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" + integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== + dependencies: + react-style-singleton "^2.2.2" + tslib "^2.0.0" + +react-remove-scroll@^2.6.3: + version "2.7.2" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz#6442da56791117661978ae99cd29be9026fecca0" + integrity sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q== + dependencies: + react-remove-scroll-bar "^2.3.7" + react-style-singleton "^2.2.3" + tslib "^2.1.0" + use-callback-ref "^1.3.3" + use-sidecar "^1.1.3" + +react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" + integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== + dependencies: + get-nonce "^1.0.0" + tslib "^2.0.0" + +react-virtuoso@^4.14.0: + version "4.18.1" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.18.1.tgz#3eb7078f2739a31b96c723374019e587deeb6ebc" + integrity sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA== + "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0": - version "19.2.1" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.1.tgz#8600fa205e58e2e807f6ef431c9f6492591a2700" - integrity sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw== + version "19.2.4" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a" + integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" readable-stream@^2.0.5, readable-stream@^2.3.8: version "2.3.8" @@ -6174,12 +6026,48 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -release-zalgo@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" - integrity sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA== +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== dependencies: - es6-error "^4.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" + +regexp-tree@^0.1.27: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + +regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + +regexparam@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-3.0.0.tgz#1673e09d41cb7fd41eaafd4040a6aa90daa0a21a" + integrity sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q== + +regjsparser@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.10.0.tgz#b1ed26051736b436f22fdec1c8f72635f9f44892" + integrity sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA== + dependencies: + jsesc "~0.5.0" require-directory@^2.1.1: version "2.1.1" @@ -6191,35 +6079,19 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - -resolve-dir@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" - integrity sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA== - dependencies: - expand-tilde "^1.2.2" - global-modules "^0.2.3" - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve@^1.10.0, resolve@^1.22.1, resolve@^1.22.4, resolve@^1.22.8: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" resolve@^1.17.0, resolve@~1.22.1, resolve@~1.22.2: version "1.22.10" @@ -6230,12 +6102,12 @@ resolve@^1.17.0, resolve@~1.22.1, resolve@~1.22.2: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.22.1, resolve@^1.22.8: - version "1.22.11" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" - integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== dependencies: - is-core-module "^2.16.1" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -6249,13 +6121,21 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" +rimraf@^6.0.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.1.2.tgz#9a0f3cea2ab853e81291127422116ecf2a86ae89" + integrity sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g== + dependencies: + glob "^13.0.0" + package-json-from-dist "^1.0.1" + ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.3.tgz#9be54e4ba5e3559c8eee06a25cd7648bbccdf5a8" @@ -6265,34 +6145,37 @@ ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.3: inherits "^2.0.4" rollup@^4.43.0: - version "4.53.3" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.53.3.tgz#dbc8cd8743b38710019fb8297e8d7a76e3faa406" - integrity sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA== + version "4.55.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.55.2.tgz#fc1cd147b1ea72b62072fb12df5e6ea28a2c1c7b" + integrity sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg== dependencies: "@types/estree" "1.0.8" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.53.3" - "@rollup/rollup-android-arm64" "4.53.3" - "@rollup/rollup-darwin-arm64" "4.53.3" - "@rollup/rollup-darwin-x64" "4.53.3" - "@rollup/rollup-freebsd-arm64" "4.53.3" - "@rollup/rollup-freebsd-x64" "4.53.3" - "@rollup/rollup-linux-arm-gnueabihf" "4.53.3" - "@rollup/rollup-linux-arm-musleabihf" "4.53.3" - "@rollup/rollup-linux-arm64-gnu" "4.53.3" - "@rollup/rollup-linux-arm64-musl" "4.53.3" - "@rollup/rollup-linux-loong64-gnu" "4.53.3" - "@rollup/rollup-linux-ppc64-gnu" "4.53.3" - "@rollup/rollup-linux-riscv64-gnu" "4.53.3" - "@rollup/rollup-linux-riscv64-musl" "4.53.3" - "@rollup/rollup-linux-s390x-gnu" "4.53.3" - "@rollup/rollup-linux-x64-gnu" "4.53.3" - "@rollup/rollup-linux-x64-musl" "4.53.3" - "@rollup/rollup-openharmony-arm64" "4.53.3" - "@rollup/rollup-win32-arm64-msvc" "4.53.3" - "@rollup/rollup-win32-ia32-msvc" "4.53.3" - "@rollup/rollup-win32-x64-gnu" "4.53.3" - "@rollup/rollup-win32-x64-msvc" "4.53.3" + "@rollup/rollup-android-arm-eabi" "4.55.2" + "@rollup/rollup-android-arm64" "4.55.2" + "@rollup/rollup-darwin-arm64" "4.55.2" + "@rollup/rollup-darwin-x64" "4.55.2" + "@rollup/rollup-freebsd-arm64" "4.55.2" + "@rollup/rollup-freebsd-x64" "4.55.2" + "@rollup/rollup-linux-arm-gnueabihf" "4.55.2" + "@rollup/rollup-linux-arm-musleabihf" "4.55.2" + "@rollup/rollup-linux-arm64-gnu" "4.55.2" + "@rollup/rollup-linux-arm64-musl" "4.55.2" + "@rollup/rollup-linux-loong64-gnu" "4.55.2" + "@rollup/rollup-linux-loong64-musl" "4.55.2" + "@rollup/rollup-linux-ppc64-gnu" "4.55.2" + "@rollup/rollup-linux-ppc64-musl" "4.55.2" + "@rollup/rollup-linux-riscv64-gnu" "4.55.2" + "@rollup/rollup-linux-riscv64-musl" "4.55.2" + "@rollup/rollup-linux-s390x-gnu" "4.55.2" + "@rollup/rollup-linux-x64-gnu" "4.55.2" + "@rollup/rollup-linux-x64-musl" "4.55.2" + "@rollup/rollup-openbsd-x64" "4.55.2" + "@rollup/rollup-openharmony-arm64" "4.55.2" + "@rollup/rollup-win32-arm64-msvc" "4.55.2" + "@rollup/rollup-win32-ia32-msvc" "4.55.2" + "@rollup/rollup-win32-x64-gnu" "4.55.2" + "@rollup/rollup-win32-x64-msvc" "4.55.2" fsevents "~2.3.2" run-applescript@^7.0.0: @@ -6307,12 +6190,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@7.8.2, rxjs@^7.8.1: - version "7.8.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" - integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== dependencies: - tslib "^2.1.0" + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + isarray "^2.0.5" safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" @@ -6324,7 +6211,15 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-regex-test@^1.1.0: +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== + dependencies: + es-errors "^1.3.0" + isarray "^2.0.5" + +safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== @@ -6343,12 +6238,17 @@ scheduler@^0.27.0: resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: +"semver@2 || 3 || 4 || 5": + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.7.2: +semver@^7.5.3, semver@^7.6.3, semver@^7.7.3: version "7.7.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== @@ -6360,11 +6260,6 @@ semver@~7.5.4: dependencies: lru-cache "^6.0.0" -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -6377,6 +6272,25 @@ set-function-length@^1.2.2: gopd "^1.0.1" has-property-descriptors "^1.0.2" +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + setimmediate@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -6403,11 +6317,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" - integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== - side-channel-list@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" @@ -6448,7 +6357,12 @@ side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" -signal-exit@^3.0.2, signal-exit@^3.0.3: +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + +signal-exit@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -6458,10 +6372,14 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +sirv@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.2.tgz#f775fccf10e22a40832684848d636346f41cd970" + integrity sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" slash@^2.0.0: version "2.0.0" @@ -6473,50 +6391,41 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slash@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" - integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== - source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== -source-map-support@0.5.13: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: +source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -spawn-wrap@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-2.0.0.tgz#103685b8b8f9b79771318827aa78650a610d457e" - integrity sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg== +spdx-correct@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== dependencies: - foreground-child "^2.0.0" - is-windows "^1.0.2" - make-dir "^3.0.0" - rimraf "^3.0.0" - signal-exit "^3.0.2" - which "^2.0.1" + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" -spawnd@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/spawnd/-/spawnd-5.0.0.tgz#ea72200bdc468998e84e1c3e7b914ce85fc1c32c" - integrity sha512-28+AJr82moMVWolQvlAIv3JcYDkjkFTEmfDc503wxrF5l2rQ3dFz6DpbXp3kD4zmgGGldfM4xM4v1sFj/ZaIOA== +spdx-exceptions@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== dependencies: - exit "^0.1.2" - signal-exit "^3.0.3" - tree-kill "^1.2.2" - wait-port "^0.2.9" + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.22" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz#abf5a08a6f5d7279559b669f47f0a43e8f3464ef" + integrity sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ== split-ca@^1.0.1: version "1.0.1" @@ -6552,7 +6461,7 @@ ssh2@^1.15.0, ssh2@^1.4.0: cpu-features "~0.0.10" nan "^2.23.0" -ssim.js@^3.1.1: +ssim.js@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/ssim.js/-/ssim.js-3.5.0.tgz#d7276b9ee99b57a5ff0db34035f02f35197e62df" integrity sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g== @@ -6564,13 +6473,44 @@ stack-utils@^2.0.6: dependencies: escape-string-regexp "^2.0.0" +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + +std-env@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" + integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== + +stop-iteration-iterator@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== + dependencies: + es-errors "^1.3.0" + internal-slot "^1.1.0" + +storybook-addon-vis@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/storybook-addon-vis/-/storybook-addon-vis-3.1.2.tgz#e4fb20bce494755623c41160eceda4ac45cb1e69" + integrity sha512-eq+HQzjktd7L2onLPhYzC1j5k3j93ZbweQSN6PRvkrsUWSeUTcRNYEIq80zQlH6vcI88+hp/SoAB4FrteEYFRw== + dependencies: + "@storybook/icons" "^2.0.0" + glob "^13.0.0" + is-ci "^4.1.0" + memoize "^10.1.0" + pathe "^2.0.3" + type-plus "8.0.0-beta.7" + vitest-plugin-vis "^4.1.0" + storybook@^10.0.7: - version "10.1.10" - resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.1.10.tgz#9023e063f97cb2c38b4520e03d49336bcbf6661f" - integrity sha512-oK0t0jEogiKKfv5Z1ao4Of99+xWw1TMUGuGRYDQS4kp2yyBsJQEgu7NI7OLYsCDI6gzt5p3RPtl1lqdeVLUi8A== + version "10.2.4" + resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.2.4.tgz#d6393acb282d17dd87b8b3ef9dc9ebf885fe1698" + integrity sha512-LwF0VZsT4qkgx66Ad/q0QgZZrU2a5WftaADDEcJ3bGq3O2fHvwWPlSZjM1HiXD4vqP9U5JiMqQkV1gkyH0XJkw== dependencies: "@storybook/global" "^5.0.0" - "@storybook/icons" "^2.0.0" + "@storybook/icons" "^2.0.1" "@testing-library/jest-dom" "^6.6.3" "@testing-library/user-event" "^14.6.1" "@vitest/expect" "3.2.4" @@ -6578,7 +6518,7 @@ storybook@^10.0.7: esbuild "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0" open "^10.2.0" recast "^0.23.5" - semver "^7.6.2" + semver "^7.7.3" use-sync-external-store "^1.5.0" ws "^8.18.0" @@ -6614,21 +6554,6 @@ string-argv@~0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -string-length@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" - integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== - dependencies: - char-regex "^1.0.2" - strip-ansi "^6.0.0" - -string-length@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-6.0.0.tgz#1c7342bbf032129b2f80003e69f889c70231d791" - integrity sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg== - dependencies: - strip-ansi "^7.1.0" - "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -6656,6 +6581,74 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string.prototype.includes@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92" + integrity sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + +string.prototype.matchall@^4.0.12: + version "4.0.12" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-abstract "^1.23.6" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + gopd "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + regexp.prototype.flags "^1.5.3" + set-function-name "^2.0.2" + side-channel "^1.1.0" + +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" + +string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -6696,16 +6689,6 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -6723,20 +6706,6 @@ strip-json-comments@^3.1.1, strip-json-comments@~3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@8.1.1, supports-color@^8.1.1, supports-color@~8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -6744,17 +6713,22 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@~8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -synckit@^0.11.8: - version "0.11.11" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.11.tgz#c0b619cf258a97faa209155d9cd1699b5c998cb0" - integrity sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw== - dependencies: - "@pkgr/core" "^0.2.9" +tabbable@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.4.0.tgz#36eb7a06d80b3924a22095daf45740dea3bf5581" + integrity sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg== tar-fs@^2.1.4: version "2.1.4" @@ -6809,19 +6783,19 @@ temporal-spec@0.3.0: resolved "https://registry.yarnpkg.com/temporal-spec/-/temporal-spec-0.3.0.tgz#8c4210c575fb28ba0a1c2e02ad68d1be5956a11f" integrity sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ== -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== +tersify@^3.11.1: + version "3.12.1" + resolved "https://registry.yarnpkg.com/tersify/-/tersify-3.12.1.tgz#1aa5a709a02daba24d694f0896fb8a233f482361" + integrity sha512-VwzXGHZSOB4T27s4uvh9v8FYrNXyfVz0nBQi28TDwrZoQwT8ZJUp1W2Ff73ekN07stJSb0D+pr6iXeNeFqTI6Q== dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" + acorn "^8.8.2" + is-buffer "^2.0.5" + unpartial "^1.0.4" -testcontainers@^11.0.0, testcontainers@^11.9.0: - version "11.9.0" - resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-11.9.0.tgz#464c2feb31b56ef594563349048efeaf0af2bd1e" - integrity sha512-SQ6OqQUig7HcGVF72i+ZVIMvxPSpEz8cgC/B63ekqMzgf98DnveoBbOmqux/Wa5wQAQCt4mEPNMa/Jz7vMg9fQ== +testcontainers@^11.0.0, testcontainers@^11.11.0: + version "11.11.0" + resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-11.11.0.tgz#0f397cd3d2a71fc5e1573d36d78149ddb2e0ab3b" + integrity sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw== dependencies: "@balena/dockerignore" "^1.0.2" "@types/dockerode" "^3.3.47" @@ -6863,6 +6837,16 @@ tiny-invariant@^1.3.3: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.2.tgz#bdd2737fe2ba40bd6f918ae26642f264b99ca251" + integrity sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg== + tinyglobby@^0.2.15: version "0.2.15" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" @@ -6876,6 +6860,11 @@ tinyrainbow@^2.0.0: resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== +tinyrainbow@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.0.3.tgz#984a5b1c1b25854a9b6bccbe77964d0593d1ea42" + integrity sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q== + tinyspy@^4.0.3: version "4.0.4" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78" @@ -6886,11 +6875,6 @@ tmp@^0.2.4, tmp@^0.2.5: resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== -tmpl@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" - integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== - to-buffer@^1.2.0, to-buffer@^1.2.1, to-buffer@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.2.tgz#ffe59ef7522ada0a2d1cb5dfe03bb8abc3cdc133" @@ -6907,21 +6891,31 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tree-kill@1.2.2, tree-kill@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" - integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== -ts-api-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" - integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== +ts-api-utils@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" + integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== ts-dedent@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tsconfig-paths@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" @@ -6931,7 +6925,7 @@ tsconfig-paths@^4.2.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -6953,26 +6947,29 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== -type-fest@^0.8.0: +type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-plus@8.0.0-beta.7: + version "8.0.0-beta.7" + resolved "https://registry.yarnpkg.com/type-plus/-/type-plus-8.0.0-beta.7.tgz#8e81ea05182b57af654cfb4708a55e5308c96845" + integrity sha512-LTV9UqBJ20I48Ba2L8N25EOowtrs40MLzsqQFfvAXnPAN9z0ny4CjduFfNIfW7euZ6wr6ImShEtrhR1virHiNw== + dependencies: + tersify "^3.11.1" + unpartial "^1.0.4" + typed-array-buffer@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" @@ -6982,12 +6979,62 @@ typed-array-buffer@^1.0.3: es-errors "^1.3.0" is-typed-array "^1.1.14" -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== dependencies: - is-typedarray "^1.0.0" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" + +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" + +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" + +typedoc-plugin-markdown@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.9.0.tgz#88f37ba2417fc8b93951d457a3a557682ce5e01e" + integrity sha512-9Uu4WR9L7ZBgAl60N/h+jqmPxxvnC9nQAlnnO/OujtG2ubjnKTVUFY1XDhcMY+pCqlX3N2HsQM2QTYZIU9tJuw== + +typedoc-plugin-missing-exports@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-4.1.2.tgz#a125a679782082caad123e8b086b4ac9b28d08da" + integrity sha512-WNoeWX9+8X3E3riuYPduilUTFefl1K+Z+5bmYqNeH5qcWjtnTRMbRzGdEQ4XXn1WEO4WCIlU0vf46Ca2y/mspg== + +typedoc@^0.28.16: + version "0.28.16" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.28.16.tgz#3901672c48746587fa24390077d07317a1fd180f" + integrity sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog== + dependencies: + "@gerrit0/mini-shiki" "^3.17.0" + lunr "^2.3.9" + markdown-it "^14.1.0" + minimatch "^9.0.5" + yaml "^2.8.1" typescript@5.8.2: version "5.8.2" @@ -6999,11 +7046,26 @@ typescript@^5.9.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + ufo@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== + dependencies: + call-bound "^1.0.3" + has-bigints "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" @@ -7015,15 +7077,20 @@ undici-types@~7.16.0: integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== undici@^7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/undici/-/undici-7.16.0.tgz#cb2a1e957726d458b536e3f076bf51f066901c1a" - integrity sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g== + version "7.18.1" + resolved "https://registry.yarnpkg.com/undici/-/undici-7.18.1.tgz#b043b3c1e25319fe463029131c089cec971e2f0c" + integrity sha512-0L1RtVqD2twa4hYKeNitqG8zvwe+4cT7L2FDP+64QC8mxjA4TlKjSqPLyOjaRdnUnWYQyxKyhDkqOHLKXw+lkA== universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== +unpartial@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/unpartial/-/unpartial-1.0.5.tgz#ade2b97d4fa309dc6a7fdca6a6fdbf76817fdd7e" + integrity sha512-yAqaXcachjgZUnM2yIkf+4KJhmyuoj7stBvlnlZpB15OYVbKnLhgJfmLW7qkpzLHCdsm1bEFvhyN9hCmlZ3uuw== + unplugin@^2.3.5: version "2.3.11" resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.11.tgz#411e020dd2ba90e2fbe1e7bd63a5a399e6ee3b54" @@ -7034,37 +7101,10 @@ unplugin@^2.3.5: picomatch "^4.0.3" webpack-virtual-modules "^0.6.2" -unrs-resolver@^1.7.11: - version "1.11.1" - resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9" - integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg== - dependencies: - napi-postinstall "^0.3.0" - optionalDependencies: - "@unrs/resolver-binding-android-arm-eabi" "1.11.1" - "@unrs/resolver-binding-android-arm64" "1.11.1" - "@unrs/resolver-binding-darwin-arm64" "1.11.1" - "@unrs/resolver-binding-darwin-x64" "1.11.1" - "@unrs/resolver-binding-freebsd-x64" "1.11.1" - "@unrs/resolver-binding-linux-arm-gnueabihf" "1.11.1" - "@unrs/resolver-binding-linux-arm-musleabihf" "1.11.1" - "@unrs/resolver-binding-linux-arm64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-arm64-musl" "1.11.1" - "@unrs/resolver-binding-linux-ppc64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-riscv64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-riscv64-musl" "1.11.1" - "@unrs/resolver-binding-linux-s390x-gnu" "1.11.1" - "@unrs/resolver-binding-linux-x64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-x64-musl" "1.11.1" - "@unrs/resolver-binding-wasm32-wasi" "1.11.1" - "@unrs/resolver-binding-win32-arm64-msvc" "1.11.1" - "@unrs/resolver-binding-win32-ia32-msvc" "1.11.1" - "@unrs/resolver-binding-win32-x64-msvc" "1.11.1" - update-browserslist-db@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz#cfb4358afa08b3d5731a2ecd95eebf4ddef8033e" - integrity sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA== + version "1.2.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== dependencies: escalade "^3.2.0" picocolors "^1.1.1" @@ -7084,6 +7124,21 @@ url@^0.11.4: punycode "^1.4.1" qs "^6.12.3" +use-callback-ref@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" + integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== + dependencies: + tslib "^2.0.0" + +use-sidecar@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" + integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + use-sync-external-store@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" @@ -7110,19 +7165,20 @@ uuid@^10.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -v8-to-istanbul@^9.0.1: - version "9.3.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" - integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== dependencies: - "@jridgewell/trace-mapping" "^0.3.12" - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^2.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vaul@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.2.tgz#c959f8b9dc2ed4f7d99366caee433fbef91f5ba9" + integrity sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA== + dependencies: + "@radix-ui/react-dialog" "^1.1.1" vite-plugin-dts@^4.5.4: version "4.5.4" @@ -7139,20 +7195,20 @@ vite-plugin-dts@^4.5.4: local-pkg "^1.0.0" magic-string "^0.30.17" -vite-plugin-node-polyfills@^0.24.0: - version "0.24.0" - resolved "https://registry.yarnpkg.com/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.24.0.tgz#4a2e984bba134017fc88cace0149cf8afdb50b54" - integrity sha512-GA9QKLH+vIM8NPaGA+o2t8PDfFUl32J8rUp1zQfMKVJQiNkOX4unE51tR6ppl6iKw5yOrDAdSH7r/UIFLCVhLw== +vite-plugin-node-polyfills@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.25.0.tgz#dccbb76b3c81a4981a89403feabc1b74d2dc60d4" + integrity sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg== dependencies: "@rollup/plugin-inject" "^5.0.5" - node-stdlib-browser "^1.2.0" + node-stdlib-browser "^1.3.1" -vite@^7.1.9: - version "7.2.7" - resolved "https://registry.yarnpkg.com/vite/-/vite-7.2.7.tgz#0789a4c3206081699f34a9ecca2dda594a07478e" - integrity sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ== +"vite@^6.0.0 || ^7.0.0", vite@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507" + integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== dependencies: - esbuild "^0.25.0" + esbuild "^0.27.0" fdir "^6.5.0" picomatch "^4.0.3" postcss "^8.5.6" @@ -7161,6 +7217,58 @@ vite@^7.1.9: optionalDependencies: fsevents "~2.3.3" +vitest-browser-react@^2.0.2: + version "2.0.5" + resolved "https://registry.yarnpkg.com/vitest-browser-react/-/vitest-browser-react-2.0.5.tgz#286a5702c85b9391114924e70db719b73d38a66e" + integrity sha512-YODQX8mHTJCyKNVYTWJrLEYrUtw+QfLl78owgvuE7C5ydgmGBq6v5s4jK2w6wdPhIZsN9PpV1rQbmAevWJjO9g== + +vitest-plugin-vis@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vitest-plugin-vis/-/vitest-plugin-vis-4.1.0.tgz#25b67a88b99c97895001fe18d03a7039ed005b46" + integrity sha512-wEYILWeax3hMUNMLMJzZ0dwtasy/YKAZz5/pyqK+0tyETjNBhtdrWiRLZc6OktOMZJk2YPlpjSOB/qqldAlvMQ== + dependencies: + dedent "^1.5.1" + glob "^13.0.0" + is-ci "^4.0.0" + mkdirp "^3.0.1" + pathe "^2.0.0" + pixelmatch "^7.1.0" + pngjs "^7.0.0" + rimraf "^6.0.1" + ssim.js "^3.5.0" + type-plus "8.0.0-beta.7" + +vitest-sonar-reporter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/vitest-sonar-reporter/-/vitest-sonar-reporter-3.0.0.tgz#3bb34a9a46390dce83a50de16135fc325c73359d" + integrity sha512-QRT5m9Z/3Kt0WVDVcFHm5LC9Ba89yDP4twlX2QUAJ9PdqfJBkLAh/kXj7m6U3i0k6GxnIEiwCc295bmAYokmZg== + +vitest@^4.0.18: + version "4.0.18" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.0.18.tgz#56f966353eca0b50f4df7540cd4350ca6d454a05" + integrity sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ== + dependencies: + "@vitest/expect" "4.0.18" + "@vitest/mocker" "4.0.18" + "@vitest/pretty-format" "4.0.18" + "@vitest/runner" "4.0.18" + "@vitest/snapshot" "4.0.18" + "@vitest/spy" "4.0.18" + "@vitest/utils" "4.0.18" + es-module-lexer "^1.7.0" + expect-type "^1.2.2" + magic-string "^0.30.21" + obug "^2.1.1" + pathe "^2.0.3" + picomatch "^4.0.3" + std-env "^3.10.0" + tinybench "^2.9.0" + tinyexec "^1.0.2" + tinyglobby "^0.2.15" + tinyrainbow "^3.0.3" + vite "^6.0.0 || ^7.0.0" + why-is-node-running "^2.3.0" + vm-browserify@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" @@ -7171,26 +7279,6 @@ vscode-uri@^3.0.8: resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c" integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ== -wait-on@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-7.2.0.tgz#d76b20ed3fc1e2bebc051fae5c1ff93be7892928" - integrity sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ== - dependencies: - axios "^1.6.1" - joi "^17.11.0" - lodash "^4.17.21" - minimist "^1.2.8" - rxjs "^7.8.1" - -wait-port@^0.2.9: - version "0.2.14" - resolved "https://registry.yarnpkg.com/wait-port/-/wait-port-0.2.14.tgz#6df40629be2c95aa4073ceb895abef7d872b28c6" - integrity sha512-kIzjWcr6ykl7WFbZd0TMae8xovwqcqbx6FM9l+7agOgUByhzdjfzZBPK2CPufldTOMxbUivss//Sh9MFawmPRQ== - dependencies: - chalk "^2.4.2" - commander "^3.0.2" - debug "^4.1.1" - walk@^2.3.15: version "2.3.15" resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.15.tgz#1b4611e959d656426bc521e2da5db3acecae2424" @@ -7198,22 +7286,50 @@ walk@^2.3.15: dependencies: foreachasync "^3.0.0" -walker@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" - integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== - dependencies: - makeerror "1.0.12" - webpack-virtual-modules@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== -which-module@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" - integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" + +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== + dependencies: + call-bound "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" + is-generator-function "^1.0.10" + is-regex "^1.2.1" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.1.0" + which-collection "^1.0.2" + which-typed-array "^1.1.16" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" which-typed-array@^1.1.16, which-typed-array@^1.1.2: version "1.1.19" @@ -7228,12 +7344,18 @@ which-typed-array@^1.1.16, which-typed-array@^1.1.2: gopd "^1.2.0" has-tostringtag "^1.0.2" -which@^1.2.12: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== +which-typed-array@^1.1.19: + version "1.1.20" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122" + integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== dependencies: - isexe "^2.0.0" + available-typed-arrays "^1.0.7" + 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" which@^2.0.1: version "2.0.2" @@ -7242,6 +7364,14 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" @@ -7256,15 +7386,6 @@ word-wrap@^1.2.5: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -7288,28 +7409,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -write-file-atomic@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" - integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== - dependencies: - imurmurhash "^0.1.4" - signal-exit "^4.0.1" - -ws@^8.18.0: - version "8.18.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" - integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== +ws@^8.18.0, ws@^8.18.3: + version "8.19.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.19.0.tgz#ddc2bdfa5b9ad860204f5a72a4863a8895fd8c8b" + integrity sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg== wsl-utils@^0.1.0: version "0.1.0" @@ -7318,21 +7421,11 @@ wsl-utils@^0.1.0: dependencies: is-wsl "^3.1.0" -xml@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" - integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== - xtend@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -7348,25 +7441,17 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^2.2.2, yaml@^2.7.0: +yaml@^2.2.2, yaml@^2.7.0, yaml@^2.8.1: version "2.8.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5" integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A== -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@17.7.2, yargs@^17.7.2: +yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -7379,23 +7464,6 @@ yargs@17.7.2, yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yargs@^15.0.2: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" @@ -7409,3 +7477,23 @@ zip-stream@^6.0.1: archiver-utils "^5.0.0" compress-commons "^6.0.2" readable-stream "^4.0.0" + +zod-validation-error@^3.0.3: + version "3.5.4" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.5.4.tgz#9072f829e4b45b9e27317c3002408c0c4cdd2bb4" + integrity sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw== + +"zod-validation-error@^3.5.0 || ^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918" + integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ== + +zod@^3.22.4: + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== + +"zod@^3.25.0 || ^4.0.0": + version "4.3.5" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.5.tgz#aeb269a6f9fc259b1212c348c7c5432aaa474d2a" + integrity sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g== diff --git a/patches/@matrix-org+react-sdk-module-api+2.5.0.patch b/patches/@matrix-org+react-sdk-module-api+2.5.0.patch index 89cbca457f..17943f892d 100644 --- a/patches/@matrix-org+react-sdk-module-api+2.5.0.patch +++ b/patches/@matrix-org+react-sdk-module-api+2.5.0.patch @@ -11,6 +11,53 @@ index 917a7fc..a2710c6 100644 didOkOrSubmit: boolean; model: M; }>; +diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts +index cb5f2e5..51daa51 100644 +--- a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts ++++ b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.d.ts +@@ -66,23 +66,23 @@ export interface SetupEncryptionStoreProjection { + export interface ProvideCryptoSetupExtensions { + examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void; + persistCredentials(credentials: ExtendedMatrixClientCreds): void; +- getSecretStorageKey(): Uint8Array | null; +- createSecretStorageKey(): Uint8Array | null; ++ getSecretStorageKey(): Uint8Array | null; ++ createSecretStorageKey(): Uint8Array | null; + catchAccessSecretStorageError(e: Error): void; + setupEncryptionNeeded: (args: CryptoSetupArgs) => boolean; + /** @deprecated This callback is no longer used by matrix-react-sdk */ +- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise) | null; ++ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise>) | null; + SHOW_ENCRYPTION_SETUP_UI: boolean; + } + export declare abstract class CryptoSetupExtensionsBase implements ProvideCryptoSetupExtensions { + abstract examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void; + abstract persistCredentials(credentials: ExtendedMatrixClientCreds): void; +- abstract getSecretStorageKey(): Uint8Array | null; +- abstract createSecretStorageKey(): Uint8Array | null; ++ abstract getSecretStorageKey(): Uint8Array | null; ++ abstract createSecretStorageKey(): Uint8Array | null; + abstract catchAccessSecretStorageError(e: Error): void; + abstract setupEncryptionNeeded(args: CryptoSetupArgs): boolean; + /** `getDehydrationKeyCallback` is no longer used; we provide an empty impl for type compatibility. */ +- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise) | null; ++ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise>) | null; + abstract SHOW_ENCRYPTION_SETUP_UI: boolean; + } + export interface CryptoSetupArgs { +@@ -98,9 +98,9 @@ export declare class DefaultCryptoSetupExtensions extends CryptoSetupExtensionsB + SHOW_ENCRYPTION_SETUP_UI: boolean; + examineLoginResponse(response: any, credentials: ExtendedMatrixClientCreds): void; + persistCredentials(credentials: ExtendedMatrixClientCreds): void; +- getSecretStorageKey(): Uint8Array | null; +- createSecretStorageKey(): Uint8Array | null; ++ getSecretStorageKey(): Uint8Array | null; ++ createSecretStorageKey(): Uint8Array | null; + catchAccessSecretStorageError(e: Error): void; + setupEncryptionNeeded(args: CryptoSetupArgs): boolean; +- getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise) | null; ++ getDehydrationKeyCallback(): ((keyInfo: SecretStorageKeyDescription, checkFunc: (key: Uint8Array) => void) => Promise>) | null; + } diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js index 5d422ed..b823add 100644 --- a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js diff --git a/patches/@vector-im+matrix-wysiwyg+2.40.0.patch b/patches/@vector-im+matrix-wysiwyg+2.40.0.patch new file mode 100644 index 0000000000..9da3dcc34e --- /dev/null +++ b/patches/@vector-im+matrix-wysiwyg+2.40.0.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/@vector-im/matrix-wysiwyg/package.json b/node_modules/@vector-im/matrix-wysiwyg/package.json +index 8163a3b..dfef973 100644 +--- a/node_modules/@vector-im/matrix-wysiwyg/package.json ++++ b/node_modules/@vector-im/matrix-wysiwyg/package.json +@@ -11,6 +11,7 @@ + "license": "SEE LICENSE IN README.md", + "exports": { + ".": { ++ "types": "./dist/index.d.ts", + "import": "./dist/matrix-wysiwyg.js", + "require": "./dist/matrix-wysiwyg.umd.cjs" + } diff --git a/patches/await-lock+3.0.0.patch b/patches/await-lock+3.0.0.patch new file mode 100644 index 0000000000..8798538841 --- /dev/null +++ b/patches/await-lock+3.0.0.patch @@ -0,0 +1,14 @@ +diff --git a/node_modules/await-lock/package.json b/node_modules/await-lock/package.json +index 84a2734..cbda839 100644 +--- a/node_modules/await-lock/package.json ++++ b/node_modules/await-lock/package.json +@@ -6,7 +6,8 @@ + "exports": { + ".": { + "types": "./build/AwaitLock.d.ts", +- "import": "./build/AwaitLock.js" ++ "import": "./build/AwaitLock.js", ++ "require": "./build/AwaitLock.js" + } + }, + "files": [ diff --git a/patches/jest-fixed-jsdom+0.0.11.patch b/patches/jest-fixed-jsdom+0.0.11.patch new file mode 100644 index 0000000000..8dcce8b28b --- /dev/null +++ b/patches/jest-fixed-jsdom+0.0.11.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/jest-fixed-jsdom/index.js b/node_modules/jest-fixed-jsdom/index.js +index ac8033b..b1ba8f0 100644 +--- a/node_modules/jest-fixed-jsdom/index.js ++++ b/node_modules/jest-fixed-jsdom/index.js +@@ -21,9 +21,10 @@ class FixedJSDOMEnvironment extends JSDOMEnvironment { + this.global.TextEncoderStream = TextEncoderStream + this.global.ReadableStream = ReadableStream + +- this.global.Blob = Blob ++ // this.global.Blob = Blob ++ // this.global.File = File + this.global.Headers = Headers +- this.global.FormData = FormData ++ // this.global.FormData = FormData + this.global.Request = Request + this.global.Response = Response + this.global.fetch = fetch diff --git a/patches/linkify-html+4.3.2.patch b/patches/linkify-html+4.3.2.patch new file mode 100644 index 0000000000..383152d254 --- /dev/null +++ b/patches/linkify-html+4.3.2.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/linkify-html/dist/linkify-html.mjs b/node_modules/linkify-html/dist/linkify-html.mjs +index 63b1fe5..f61e3b7 100644 +--- a/node_modules/linkify-html/dist/linkify-html.mjs ++++ b/node_modules/linkify-html/dist/linkify-html.mjs +@@ -201,6 +201,8 @@ var EventedTokenizer = /** @class */function () { + this.consume(); + if (this.delegate.endDoctype) this.delegate.endDoctype(); + this.transitionTo("beforeData" /* beforeData */); ++ } else { ++ throw new Error("Unexpected token"); + } + }, + doctypePublicIdentifierDoubleQuoted: function () { diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index 282440f74e..28dd3b922d 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -23,6 +23,7 @@ const clickButtonReply = async (tile: Locator) => { }; test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { + test.slow(); test.use({ displayName: "Hanako", }); @@ -36,7 +37,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { // Wait until the file is sent await expect(page.locator(".mx_RoomView_statusArea_expanded")).not.toBeVisible(); - await expect(page.locator(".mx_EventTile.mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); + await expect(page.locator(".mx_EventTile.mx_EventTile_last").getByRole("status")).toHaveAccessibleName( + "Your message was sent", + ); // wait for the tile to finish loading await expect( page @@ -98,35 +101,39 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { .mx_MessageActionBar { display: none !important; } + /* Stabilize play button appearance in CI (disabled due to decoding) */ + button[aria-label="Play"] { + opacity: 1 !important; + } + button[aria-label="Play"] svg, + button[aria-label="Play"] path { + fill: magenta !important; + stroke: magenta !important; + } `, mask: [page.getByTestId("audio-player-seek")], + clip: undefined, }; // Take a snapshot of mx_EventTile_last on IRC layout - await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot( - `${detail.replaceAll(" ", "-")}-irc-layout.png`, - screenshotOptions, - ); + screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox(); + await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-irc-layout.png`, screenshotOptions); // Take a snapshot on modern/group layout await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); const groupTile = page.locator(".mx_EventTile_last[data-layout='group']"); await groupTile.locator(".mx_MessageTimestamp").click(); await checkPlayerVisibility(groupTile); - await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot( - `${detail.replaceAll(" ", "-")}-group-layout.png`, - screenshotOptions, - ); + screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox(); + await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-group-layout.png`, screenshotOptions); // Take a snapshot on bubble layout await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); const bubbleTile = page.locator(".mx_EventTile_last[data-layout='bubble']"); await bubbleTile.locator(".mx_MessageTimestamp").click(); await checkPlayerVisibility(bubbleTile); - await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot( - `${detail.replaceAll(" ", "-")}-bubble-layout.png`, - screenshotOptions, - ); + screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox(); + await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-bubble-layout.png`, screenshotOptions); }; test.beforeEach(async ({ page, app, user }) => { @@ -351,7 +358,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { const composer = thread.locator(".mx_MessageComposer--compact"); // Assert that the reply preview contains audio ReplyTile the file info button await expect( - composer.locator(".mx_ReplyPreview .mx_ReplyTile_audio .mx_MFileBody_info[role='button']"), + composer.locator(".mx_ReplyPreview .mx_ReplyTile .mx_MFileBody_info[role='button']"), ).toBeVisible(); // Select :smile: emoji and send it @@ -360,6 +367,6 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => { await composer.getByTestId("basicmessagecomposer").press("Enter"); // Assert that the file name is rendered on the file button - await expect(threadTile.locator(".mx_ReplyTile_audio .mx_MFileBody_info[role='button']")).toBeVisible(); + await expect(threadTile.locator(".mx_ReplyTile .mx_MFileBody_info[role='button']")).toBeVisible(); }); }); diff --git a/playwright/e2e/composer/CIDER.spec.ts b/playwright/e2e/composer/CIDER.spec.ts index 55cb84f37a..d164a689e1 100644 --- a/playwright/e2e/composer/CIDER.spec.ts +++ b/playwright/e2e/composer/CIDER.spec.ts @@ -80,7 +80,15 @@ test.describe("Composer", () => { test.use({ viewport: { width: 1280, height: 720 } }); test("render emoji picker", { tag: "@screenshot" }, async ({ page, app }) => { await app.getComposer(false).getByRole("button", { name: "Emoji" }).click(); - await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker.png"); + // Mask the background of the screenshot to avoid failing the test just because some + // other component have changed its rendering. + await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker.png", { + css: ` + .mx_ContextualMenu_background { + background-color: magenta !important; + } + `, + }); }); }); @@ -88,7 +96,15 @@ test.describe("Composer", () => { test.use({ viewport: { width: 1280, height: 360 } }); test("render emoji picker", { tag: "@screenshot" }, async ({ page, app }) => { await app.getComposer(false).getByRole("button", { name: "Emoji" }).click(); - await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker-small.png"); + // Mask the background of the screenshot to avoid failing the test just because some + // other component have changed its rendering. + await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker-small.png", { + css: ` + .mx_ContextualMenu_background { + background-color: magenta !important; + } + `, + }); }); }); diff --git a/playwright/e2e/crypto/backups-mas.spec.ts b/playwright/e2e/crypto/backups-mas.spec.ts index f048bc6a2b..521ec109d9 100644 --- a/playwright/e2e/crypto/backups-mas.spec.ts +++ b/playwright/e2e/crypto/backups-mas.spec.ts @@ -106,8 +106,8 @@ test.describe("Key backup reset from elsewhere", () => { // Should be the message we sent plus the room creation event await expect(page.locator(".mx_EventTile")).toHaveCount(2); await expect( - page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"), - ).toBeVisible(); + page.locator(".mx_RoomView_MessageList > .mx_EventTile_last").getByRole("status"), + ).toHaveAccessibleName("Your message was sent"); // Wait for it to try uploading the key await page.clock.fastForward(20000); diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index ae4db1b0c3..d03fa1454e 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -31,15 +31,11 @@ const startDMWithBob = async (page: Page, bob: Bot) => { const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => { // check the invite message - await expect( - page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon_warning"), - ).not.toBeVisible(); + await expect(page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); // Bob sends a response await bob.sendMessage(bobRoomId, "Hoo!"); - await expect( - page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"), - ).not.toBeVisible(); + await expect(page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); }; const bobJoin = async (page: Page, bob: Bot) => { diff --git a/playwright/e2e/crypto/decryption-failure-messages.spec.ts b/playwright/e2e/crypto/decryption-failure-messages.spec.ts index 306e073c00..e0343a8005 100644 --- a/playwright/e2e/crypto/decryption-failure-messages.spec.ts +++ b/playwright/e2e/crypto/decryption-failure-messages.spec.ts @@ -30,69 +30,80 @@ test.describe("Cryptography", function () { test.describe("decryption failure messages", () => { test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here"); - test("should handle device-relative historical messages", async ({ - homeserver, - page, - app, - credentials, - user, - }) => { - test.setTimeout(60000); + test( + "should handle device-relative historical messages", + { tag: "@screenshot" }, + async ({ homeserver, page, app, credentials, user }) => { + test.setTimeout(60000); - // Start with a logged-in session, without key backup, and send a message. - await createRoom(page, "Test room", true); - await sendMessageInCurrentRoom(page, "test test"); + // Start with a logged-in session, without key backup, and send a message. + await createRoom(page, "Test room", true); + await sendMessageInCurrentRoom(page, "test test"); - // Log out, discarding the key for the sent message. - await logOutOfElement(page, true); + // Log out, discarding the key for the sent message. + await logOutOfElement(page, true); - // Log in again, and see how the message looks. - await logIntoElement(page, credentials); - await app.viewRoomByName("Test room"); - const lastTile = page.locator(".mx_EventTile").last(); - await expect(lastTile).toContainText("Historical messages are not available on this device"); - await expect(lastTile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + // Log in again, and see how the message looks. + await logIntoElement(page, credentials); + await app.viewRoomByName("Test room"); + const lastTile = page.locator(".mx_EventTile").last(); + await expect(lastTile).toContainText("Historical messages are not available on this device"); + await expect(lastTile.locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName( + "This message could not be decrypted", + ); + await expect(lastTile).toMatchScreenshot("history-not-available.png", { + mask: [page.locator(".mx_MessageTimestamp")], + }); - // Now, we set up key backup, and then send another message. - const secretStorageKey = await enableKeyBackup(app); - await app.viewRoomByName("Test room"); - await sendMessageInCurrentRoom(page, "test2 test2"); + // Now, we set up key backup, and then send another message. + const secretStorageKey = await enableKeyBackup(app); + await app.viewRoomByName("Test room"); + await sendMessageInCurrentRoom(page, "test2 test2"); - // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for - // the key to be backed up. - await page.waitForTimeout(10000); + // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for + // the key to be backed up. + await page.waitForTimeout(10000); - // Finally, log out again, and back in, skipping verification for now, and see what we see. - await logOutOfElement(page); - await logIntoElement(page, credentials); - await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click(); - await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); - await app.viewRoomByName("Test room"); + // Finally, log out again, and back in, skipping verification for now, and see what we see. + await logOutOfElement(page); + await logIntoElement(page, credentials); + await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click(); + await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); + await app.viewRoomByName("Test room"); - // In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve - await page.waitForTimeout(1000); + // In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve + await page.waitForTimeout(1000); - // There should be two historical events in the timeline - const tiles = await page.locator(".mx_EventTile").all(); - expect(tiles.length).toBeGreaterThanOrEqual(2); - // look at the last two tiles only - for (const tile of tiles.slice(-2)) { - await expect(tile).toContainText("You need to verify this device for access to historical messages"); - await expect(tile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); - } + // There should be two historical events in the timeline + const tiles = await page.locator(".mx_EventTile").all(); + expect(tiles.length).toBeGreaterThanOrEqual(2); + // look at the last two tiles only + for (const tile of tiles.slice(-2)) { + await expect(tile).toContainText( + "You need to verify this device for access to historical messages", + ); + await expect(tile.locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName( + "This message could not be decrypted", + ); + } - // Now verify our device (setting up key backup), and check what happens - await verifySession(app, secretStorageKey); - const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2); + // Now verify our device (setting up key backup), and check what happens + await verifySession(app, secretStorageKey); + const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2); - // The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though. - await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message"); - await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + // The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though. + await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message"); + await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName( + "This message could not be decrypted", + ); - // The second message should now be decrypted, with a grey shield - await expect(tilesAfterVerify[1]).toContainText("test2 test2"); - await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible(); - }); + // The second message should now be decrypted, with a grey shield + await expect(tilesAfterVerify[1]).toContainText("test2 test2"); + await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName( + "The authenticity of this encrypted message can't be guaranteed on this device.", + ); + }, + ); test.describe("non-joined historical messages", () => { test.skip(isDendrite, "does not yet support membership on events"); @@ -186,7 +197,9 @@ test.describe("Cryptography", function () { // The first message from Bob was sent before Alice was in the room, so should // be different from the standard UTD message await expect(tiles[tiles.length - 5]).toContainText("You don't have access to this message"); - await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName( + "This message could not be decrypted", + ); // The second message from Bob should be decryptable await expect(tiles[tiles.length - 2]).toContainText("This should be decryptable"); @@ -196,7 +209,9 @@ test.describe("Cryptography", function () { // in the room and is expected to be decryptable, so this should have the // standard UTD message await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message"); - await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName( + "This message could not be decrypted", + ); }); test("should be able to jump to a message sent before our last join event", async ({ diff --git a/playwright/e2e/crypto/device-verification.spec.ts b/playwright/e2e/crypto/device-verification.spec.ts index eb43d4dc78..6beda36a98 100644 --- a/playwright/e2e/crypto/device-verification.spec.ts +++ b/playwright/e2e/crypto/device-verification.spec.ts @@ -68,7 +68,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { await doTwoWaySasVerification(page, verifier); await infoDialog.getByRole("button", { name: "They match" }).click(); - await expect(page.locator(".mx_E2EIcon_verified")).toMatchScreenshot("device-verified-e2eIcon.png"); + await expect(page.locator(".mx_E2EIcon")).toMatchScreenshot("device-verified-e2eIcon.png"); await infoDialog.getByRole("button", { name: "Got it" }).click(); // Check that our device is now cross-signed diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index 722a30a0d2..688b81b5b1 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -77,11 +77,8 @@ test.describe("Cryptography", function () { const last = page.locator(".mx_EventTile_last"); await expect(last).toContainText("Unable to decrypt message"); const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/); - await lastE2eIcon.focus(); - await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( - "This message could not be decrypted", - ); + await expect(lastE2eIcon).toHaveAccessibleName("This message could not be decrypted"); + await expect(lastE2eIcon).toMatchScreenshot("event-shield-utd.png"); /* Should show a red padlock for an unencrypted message in an e2e room */ await bob.evaluate( @@ -99,10 +96,8 @@ test.describe("Cryptography", function () { ); await expect(last).toContainText("test unencrypted"); - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); + await expect(lastE2eIcon).toHaveAccessibleName("Not encrypted"); await expect(lastE2eIcon).toMatchScreenshot("event-shield-warning.png"); - await lastE2eIcon.focus(); - await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted"); /* Should show no padlock for an unverified user */ // bob sends a valid event @@ -133,11 +128,8 @@ test.describe("Cryptography", function () { /* should show red padlock for a message from an unverified device */ await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified"); await expect(lastTile).toContainText("test encrypted from unverified"); - await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); - await lastTileE2eIcon.focus(); - await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText( - "Encrypted by a device not verified by its owner.", - ); + await expect(lastTileE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner."); + await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png"); /* Should show a red padlock for a message from an unverified device. * Rust crypto remembers the verification state of the sending device, so it will know that the device was @@ -153,64 +145,58 @@ test.describe("Cryptography", function () { await app.viewRoomByName("TestRoom"); await expect(last).toContainText("test encrypted from unverified"); - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); - await lastE2eIcon.focus(); - await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( - "Encrypted by a device not verified by its owner.", - ); + await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner."); + await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png"); }, ); - test("Should show a grey padlock for a key restored from backup", async ({ - page, - app, - bot: bob, - homeserver, - user: aliceCredentials, - }) => { - test.slow(); - const securityKey = await enableKeyBackup(app); + test( + "Should show a grey padlock for a key restored from backup", + { tag: "@screenshot" }, + async ({ page, app, bot: bob, homeserver, user: aliceCredentials }) => { + test.slow(); + const securityKey = await enableKeyBackup(app); - // bob sends a valid event - await bob.sendMessage(testRoomId, "test encrypted 1"); + // bob sends a valid event + await bob.sendMessage(testRoomId, "test encrypted 1"); - const lastTile = page.locator(".mx_EventTile_last"); - const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon"); - await expect(lastTile).toContainText("test encrypted 1"); - // no e2e icon - await expect(lastTileE2eIcon).not.toBeVisible(); + const lastTile = page.locator(".mx_EventTile_last"); + const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon"); + await expect(lastTile).toContainText("test encrypted 1"); + // no e2e icon + await expect(lastTileE2eIcon).not.toBeVisible(); - // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for - // the key to be backed up. - await page.waitForTimeout(10000); + // Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for + // the key to be backed up. + await page.waitForTimeout(10000); - /* log out, and back in */ - await logOutOfElement(page); - // Reload to work around a Rust crypto bug where it can hold onto the indexeddb even after logout - // https://github.com/element-hq/element-web/issues/25779 - await page.addInitScript(() => { - // When we reload, the initScript created by the `user`/`pageWithCredentials` fixtures - // will re-inject the original credentials into localStorage, which we don't want. - // To work around, we add a second initScript which will clear localStorage again. - window.localStorage.clear(); - }); - await page.reload(); - await logIntoElementAndVerify(page, aliceCredentials, securityKey); + /* log out, and back in */ + await logOutOfElement(page); + // Reload to work around a Rust crypto bug where it can hold onto the indexeddb even after logout + // https://github.com/element-hq/element-web/issues/25779 + await page.addInitScript(() => { + // When we reload, the initScript created by the `user`/`pageWithCredentials` fixtures + // will re-inject the original credentials into localStorage, which we don't want. + // To work around, we add a second initScript which will clear localStorage again. + window.localStorage.clear(); + }); + await page.reload(); + await logIntoElementAndVerify(page, aliceCredentials, securityKey); - /* go back to the test room and find Bob's message again */ - await app.viewRoomById(testRoomId); - await expect(lastTile).toContainText("test encrypted 1"); - // The gray shield would be a mx_EventTile_e2eIcon_normal. The red shield would be a mx_EventTile_e2eIcon_warning. - // No shield would have no div mx_EventTile_e2eIcon at all. - await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/); - await lastTileE2eIcon.hover(); - // The key is coming from backup, so it is not anymore possible to establish if the claimed device - // creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device." - // It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted. - await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText( - "The authenticity of this encrypted message can't be guaranteed on this device.", - ); - }); + /* go back to the test room and find Bob's message again */ + await app.viewRoomById(testRoomId); + await expect(lastTile).toContainText("test encrypted 1"); + // The gray shield would be a Compound info icon. The red shield would be a Compound error solid icon. + // No shield would have no div mx_EventTile_e2eIcon at all. + // The key is coming from backup, so it is not anymore possible to establish if the claimed device + // creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device." + // It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted. + await expect(lastTileE2eIcon).toHaveAccessibleName( + "The authenticity of this encrypted message can't be guaranteed on this device.", + ); + await expect(lastTileE2eIcon).toMatchScreenshot("event-shield-authenticity.png"); + }, + ); test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => { // bob has a second, not cross-signed, device @@ -224,7 +210,7 @@ test.describe("Cryptography", function () { // the message should appear, decrypted, with no warning await expect( - page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"), + page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon"), ).not.toBeVisible(); // bob sends an edit to the first message with his unverified device @@ -241,7 +227,7 @@ test.describe("Cryptography", function () { // the edit should have a warning await expect( - page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon_warning"), + page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon"), ).toBeVisible(); // a second edit from the verified device should be ok @@ -257,77 +243,69 @@ test.describe("Cryptography", function () { }); await expect( - page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"), + page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon"), ).not.toBeVisible(); }); - test("should show correct shields on events sent by devices which have since been deleted", async ({ - page, - app, - bot: bob, - homeserver, - }) => { - // Workaround for https://github.com/element-hq/element-web/issues/28640: - // make sure that Alice has seen Bob's identity before she goes offline. We do this by opening - // his user info. - await waitForDevices(app, bob.credentials.userId, 1); + test( + "should show correct shields on events sent by devices which have since been deleted", + { tag: "@screenshot" }, + async ({ page, app, bot: bob, homeserver }) => { + // Workaround for https://github.com/element-hq/element-web/issues/28640: + // make sure that Alice has seen Bob's identity before she goes offline. We do this by opening + // his user info. + await waitForDevices(app, bob.credentials.userId, 1); - // Our app is blocked from syncing while Bob sends his messages. - await app.client.network.goOffline(); + // Our app is blocked from syncing while Bob sends his messages. + await app.client.network.goOffline(); - // Bob sends a message from his verified device - await bob.sendMessage(testRoomId, "test encrypted from verified"); + // Bob sends a message from his verified device + await bob.sendMessage(testRoomId, "test encrypted from verified"); - // And one from a second, not cross-signed, device - const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); - await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted - await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified"); + // And one from a second, not cross-signed, device + const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); + await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted + await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified"); - // ... and then logs out both devices. - await bob.evaluate((cli) => cli.logout(true)); - await bobSecondDevice.evaluate((cli) => cli.logout(true)); + // ... and then logs out both devices. + await bob.evaluate((cli) => cli.logout(true)); + await bobSecondDevice.evaluate((cli) => cli.logout(true)); - // Let our app start syncing again - await app.client.network.goOnline(); + // Let our app start syncing again + await app.client.network.goOnline(); - // Wait for the messages to arrive. It can take quite a while for the sync to wake up. - const last = page.locator(".mx_EventTile_last"); - await expect(last).toContainText("test encrypted from unverified", { timeout: 20000 }); - const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); - await lastE2eIcon.focus(); - await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( - "Encrypted by a device not verified by its owner.", - ); + // Wait for the messages to arrive. It can take quite a while for the sync to wake up. + const last = page.locator(".mx_EventTile_last"); + await expect(last).toContainText("test encrypted from unverified", { timeout: 20000 }); + const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); + await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner."); + await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png"); - const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); - await assertNoE2EIcon(penultimate, app); - }); + const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); + await assertNoE2EIcon(penultimate, app); + }, + ); - test("should show correct shields on events sent by users with changed identity", async ({ - page, - app, - bot: bob, - homeserver, - }) => { - // Verify Bob - await verify(app, bob); + test( + "should show correct shields on events sent by users with changed identity", + { tag: "@screenshot" }, + async ({ page, app, bot: bob, homeserver }) => { + // Verify Bob + await verify(app, bob); - // Bob logs in a new device and resets cross-signing - const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); - await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true); + // Bob logs in a new device and resets cross-signing + const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); + await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true); - /* should show an error for a message from a previously verified device */ - await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified"); - const last = page.locator(".mx_EventTile_last"); - await expect(last).toContainText("test encrypted from user that was previously verified"); - const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); - await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); - await lastE2eIcon.focus(); - await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( - "Sender's verified identity was reset", - ); - }); + /* should show an error for a message from a previously verified device */ + await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified"); + const last = page.locator(".mx_EventTile_last"); + await expect(last).toContainText("test encrypted from user that was previously verified"); + const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); + await expect(lastE2eIcon).toHaveAccessibleName("Sender's verified identity was reset"); + await expect(lastE2eIcon).toMatchScreenshot("event-shield-identity-reset.png"); + }, + ); }); }); @@ -343,8 +321,6 @@ async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) { const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon"); if ((await e2eIcon.count()) > 0) { // uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error. - await e2eIcon.focus(); - const tooltip = await app.getTooltipForElement(e2eIcon); - throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`); + await expect(e2eIcon).toHaveAccessibleName("None"); } } diff --git a/playwright/e2e/crypto/history-sharing.spec.ts b/playwright/e2e/crypto/history-sharing.spec.ts index ee275da84a..820f5edc63 100644 --- a/playwright/e2e/crypto/history-sharing.spec.ts +++ b/playwright/e2e/crypto/history-sharing.spec.ts @@ -50,7 +50,9 @@ test.describe("History sharing", function () { // Bob should now be able to decrypt the event await expect(bobPage.getByText("A message from Alice")).toBeVisible(); - const mask = [bobPage.locator(".mx_MessageTimestamp")]; + // Exclude message timestamps and RR avatars from the screenshot. Bob sometimes sees Alice's RR on the + // previous event, which is surprising but not what we're testing here. + const mask = [bobPage.locator(".mx_MessageTimestamp"), bobPage.locator(".mx_ReadReceiptGroup_container")]; await expect(bobPage.locator(".mx_RoomView_body")).toMatchScreenshot("shared-history-invite-accepted.png", { mask, }); diff --git a/playwright/e2e/crypto/toasts.spec.ts b/playwright/e2e/crypto/toasts.spec.ts index 5e1dedfdc7..b67a43756f 100644 --- a/playwright/e2e/crypto/toasts.spec.ts +++ b/playwright/e2e/crypto/toasts.spec.ts @@ -37,7 +37,15 @@ test.describe("Key storage out of sync toast", () => { // playwright only evaluates the 'first()' call initially, not subsequent times it checks, so // it would always be checking the same toast, even if another one is now the first. await expect(page.getByRole("alert")).toHaveCount(2); - await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png"); + // Mask the background of the screenshot to avoid failing the test just because some + // other component have changed its rendering. + await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png", { + css: ` + .mx_ToastContainer { + background-color: magenta !important; + } + `, + }); await page.getByRole("button", { name: "Enter recovery key" }).click(); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index 8b677ed4cb..4aab27f51a 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -290,7 +290,7 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle div"); await expect(emojiBlocks).toHaveCount(emojis.length); // then, check that our application shows an emoji panel with the same emojis. diff --git a/playwright/e2e/feedback/rageshakes.spec.ts b/playwright/e2e/feedback/rageshakes.spec.ts new file mode 100644 index 0000000000..58476f04ac --- /dev/null +++ b/playwright/e2e/feedback/rageshakes.spec.ts @@ -0,0 +1,139 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { test, expect } from "../../element-web-test"; + +function formDataParser(data: string, contentType: string | null): Record { + const [, boundary] = + contentType + ?.split(";") + .map((v) => v.trim()) + .find((v) => v.startsWith("boundary=")) + ?.split("=") ?? []; + if (!boundary) { + throw Error("No boundary found in form data request"); + } + const dataMap: Record = {}; + for (const dataPart of data.split(boundary).map((p) => p.trim())) { + const lines = dataPart.split("\r\n"); + const fieldName = lines[0].match(/name="([^"]+)"/)?.[1]; + if (!fieldName) { + continue; + } + const data = lines.slice(1, -1).join("\n").trim(); + dataMap[fieldName] = data; + } + return dataMap; +} + +test.describe("Rageshakes", () => { + test.describe("visible when enabled", () => { + test.use({ + config: { + // Enable this just so the options show up. + bug_report_endpoint_url: "https://example.org/bug-report-place", + }, + }); + test("should be able to open bug report dialog via slash command", async ({ page, app, user }) => { + await app.client.createRoom({ name: "Test room" }); + await app.viewRoomByName("Test room"); + const composer = app.getComposer().locator("[contenteditable]"); + await composer.fill("/rageshake"); + await composer.press("Enter"); + await expect(page.getByRole("dialog", { name: "Submit debug logs" })).toBeVisible(); + }); + + test("should be able to open bug report dialog via feedback dialog", async ({ page, app, user }) => { + const menu = await app.openUserMenu(); + await menu.getByRole("menuitem", { name: "Feedback" }).click(); + const feedbackDialog = page.getByRole("dialog", { name: "Feedback" }); + await feedbackDialog.getByRole("button", { name: "debug logs" }).click(); + await expect(page.getByRole("dialog", { name: "Submit debug logs" })).toBeVisible(); + }); + test("should be able to open bug report dialog via Settings", async ({ page, app, user }) => { + const settings = await app.settings.openUserSettings("Help & About"); + await settings.getByRole("button", { name: "Submit debug logs" }).click(); + // Playwright can't see the dialog when both the settings and bug report dialogs are open, so key off heading. + await expect(page.locator(".mx_BugReportDialog")).toBeVisible(); + }); + }); + + test.describe("hidden when disabled", () => { + test("should NOT be able to open bug report dialog via slash command", async ({ page, app, user }) => { + await app.client.createRoom({ name: "Test room" }); + await app.viewRoomByName("Test room"); + const composer = app.getComposer().locator("[contenteditable]"); + await composer.fill("/rageshake"); + await composer.press("Enter"); + await expect(page.getByRole("dialog", { name: "Unknown command" })).toBeVisible(); + }); + + test("should NOT be able to open bug report dialog via feedback dialog", async ({ page, app, user }) => { + const menu = await app.openUserMenu(); + await expect(menu.getByRole("menuitem", { name: "Feedback" })).not.toBeVisible(); + }); + test("should NOT be able to open bug report dialog via Settings", async ({ page, app, user }) => { + const settings = await app.settings.openUserSettings("Help & About"); + await expect(settings.getByRole("menuitem", { name: "Submit debug logs" })).not.toBeVisible(); + }); + }); + + test.describe("via bug report endpoint", () => { + test.use({ + config: { + bug_report_endpoint_url: "http://example.org/bug-report-server", + }, + }); + + test("should be able to rageshake to a URL", { tag: "@screenshot" }, async ({ page, app, user }) => { + await page.route("http://example.org/bug-report-server", async (route, request) => { + if (request.method() !== "POST") { + throw Error("Expected POST"); + } + const fields = formDataParser(request.postData(), await request.headerValue("Content-Type")); + expect(fields.text).toEqual( + "These are some notes\n\nIssue: https://github.com/element-hq/element-web/12345", + ); + expect(fields.app).toEqual("element-web"); + expect(fields.user_id).toEqual(user.userId); + expect(fields.device_id).toEqual(user.deviceId); + // We don't check the logs contents, but we'd like for there to be a log. + expect(fields["compressed-log"]).toBeDefined(); + return route.fulfill({ json: {}, status: 200 }); + }); + + const settings = await app.settings.openUserSettings("Help & About"); + await settings.getByRole("button", { name: "Submit debug logs" }).click(); + const dialog = page.locator(".mx_BugReportDialog"); + await dialog + .getByRole("textbox", { name: "GitHub issue" }) + .fill("https://github.com/element-hq/element-web/12345"); + await dialog.getByRole("textbox", { name: "Notes" }).fill("These are some notes"); + await expect(dialog).toMatchScreenshot("rageshake_via_url.png"); + await dialog.getByRole("button", { name: "Send logs" }).click(); + await expect(page.getByRole("heading", { name: "Logs sent" })).toBeVisible(); + }); + }); + test.describe("via local download", () => { + test.use({ + config: { + bug_report_endpoint_url: "local", + }, + }); + + test("should be able to rageshake to local download", { tag: "@screenshot" }, async ({ page, app, user }) => { + const settings = await app.settings.openUserSettings("Help & About"); + await settings.getByRole("button", { name: "Download logs" }).click(); + const dialog = page.locator(".mx_BugReportDialog"); + await expect(dialog).toMatchScreenshot("rageshake_locally.png"); + const downloadPromise = page.waitForEvent("download"); + await dialog.getByRole("button", { name: "Download logs" }).click(); + const download = await downloadPromise; + await download.cancel(); + }); + }); +}); diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index 5cc78eca69..5907ad6d97 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -8,6 +8,8 @@ import { type Page } from "@playwright/test"; import { expect, test } from "../../../element-web-test"; +import { type Bot } from "../../../pages/bot"; +import { type ElementAppPage } from "../../../pages/ElementAppPage"; test.describe("Room list", () => { test.use({ @@ -35,6 +37,8 @@ test.describe("Room list", () => { }); test.describe("Room list", () => { + test.slow(); + test.beforeEach(async ({ page, app, user }) => { for (let i = 0; i < 30; i++) { await app.client.createRoom({ name: `room${i}` }); @@ -119,23 +123,24 @@ test.describe("Room list", () => { // It should make the room muted await page.getByRole("menuitem", { name: "Mute room" }).click(); - await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible(); - // Put focus on the room list await roomListView.getByRole("option", { name: "Open room room28" }).click(); + await roomItem.scrollIntoViewIfNeeded(); - // Scroll to the end of the room list - await app.scrollListToBottom(roomListView); + // Not hovered, the room decoration should be the muted icon + await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); - // The room decoration should have the muted icon + // During hover the room decoration should still be the muted icon await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); await roomItem.hover(); + // On hover, the room should show the muted icon await expect(roomItem).toMatchScreenshot("room-list-item-hover-silent.png"); roomItemMenu = roomItem.getByRole("button", { name: "Notification options" }); await roomItemMenu.click(); + // The Mute room option should be selected await expect(page.getByRole("menuitem", { name: "Mute room" })).toHaveAttribute("aria-selected", "true"); await expect(page).toMatchScreenshot("room-list-item-open-notification-options-selection.png"); @@ -392,13 +397,8 @@ test.describe("Room list", () => { await expect(room).toMatchScreenshot("room-list-item-mention.png"); }); - test("should render a message preview", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { - await app.settings.openUserSettings("Preferences"); - await page.getByRole("switch", { name: "Show message previews" }).click(); - await app.closeDialog(); - + async function checkMessagePreview(page: Page, app: ElementAppPage, bot: Bot) { const roomListView = getRoomList(page); - const roomId = await app.client.createRoom({ name: "activity" }); // focus the user menu to avoid to have hover decoration @@ -411,7 +411,30 @@ test.describe("Room list", () => { const room = roomListView.getByRole("option", { name: "activity" }); await expect(room.getByText("I am a robot. Beep.")).toBeVisible(); await expect(room).toMatchScreenshot("room-list-item-message-preview.png"); - }); + } + + test( + "should render a message preview when enable in settings", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + await app.settings.openUserSettings("Preferences"); + await page.getByRole("switch", { name: "Show message previews" }).click(); + await app.closeDialog(); + + await checkMessagePreview(page, app, bot); + }, + ); + + test( + "should render a message preview when enabled in header", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + await page.getByRole("button", { name: "Room Options" }).click(); + await page.getByRole("menuitemcheckbox", { name: "Show message previews" }).click(); + + await checkMessagePreview(page, app, bot); + }, + ); test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { const roomListView = getRoomList(page); diff --git a/playwright/e2e/messages/messages.spec.ts b/playwright/e2e/messages/messages.spec.ts index a5ea38482c..04763e1079 100644 --- a/playwright/e2e/messages/messages.spec.ts +++ b/playwright/e2e/messages/messages.spec.ts @@ -12,12 +12,16 @@ import { type Locator, type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; +async function waitForMessageSentStatus(msgTile: Locator): Promise { + await expect(msgTile.getByRole("status")).toHaveAccessibleName("Your message was sent"); +} + async function sendMessage(page: Page, message: string): Promise { await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill(message); await page.getByRole("button", { name: "Send message" }).click(); const msgTile = page.locator(".mx_EventTile_last"); - await msgTile.locator(".mx_EventTile_receiptSent").waitFor(); + await waitForMessageSentStatus(msgTile); return msgTile; } @@ -31,7 +35,7 @@ async function sendMultilineMessages(page: Page, messages: string[]) { await page.getByRole("button", { name: "Send message" }).click(); const msgTile = page.locator(".mx_EventTile_last"); - await msgTile.locator(".mx_EventTile_receiptSent").waitFor(); + await waitForMessageSentStatus(msgTile); return msgTile; } @@ -44,7 +48,7 @@ async function replyMessage(page: Page, message: Locator, replyMessage: string): await page.getByRole("button", { name: "Send message" }).click(); const msgTile = page.locator(".mx_EventTile_last"); - await msgTile.locator(".mx_EventTile_receiptSent").waitFor(); + await waitForMessageSentStatus(msgTile); return msgTile; } diff --git a/playwright/e2e/read-receipts/room-list-order.spec.ts b/playwright/e2e/read-receipts/room-list-order.spec.ts index 405cdf22ed..c13730107c 100644 --- a/playwright/e2e/read-receipts/room-list-order.spec.ts +++ b/playwright/e2e/read-receipts/room-list-order.spec.ts @@ -18,19 +18,28 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => { util, msg, page, + app, + bot, }) => { + // Create a third room to navigate to + const room3Id = await app.client.createRoom({ name: "Room Gamma", invite: [bot.credentials.userId] }); + await bot.awaitRoomMembership(room3Id); + const room3 = { name: "Room Gamma", roomId: room3Id }; + await util.goTo(room2); // Display the unread first room + await util.receiveMessages(room2, ["Msg2"]); await util.receiveMessages(room1, ["Msg1"]); await page.reload(); - // switch rooms so they can re-order in the list - await util.goTo(room1); + // Switch to room3 so neither room1 nor room2 is selected/sticky + // This allows them to reorder based on activity + await util.goTo(room3); // Room 1 has an unread message and should be displayed first // (as the default is to sort by activity) - await util.assertRoomListOrder([room1, room2]); + await util.assertRoomListOrder([room1, room2, room3]); }); test("Rooms with unread threads appear at the top of room list with default 'activity' order", async ({ @@ -38,18 +47,29 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => { roomBeta: room2, util, msg, + app, + bot, }) => { + // Create a third room to navigate to + const room3Id = await app.client.createRoom({ name: "Room Gamma", invite: [bot.credentials.userId] }); + await bot.awaitRoomMembership(room3Id); + const room3 = { name: "Room Gamma", roomId: room3Id }; + await util.goTo(room2); await util.receiveMessages(room1, ["Msg1"]); + await util.receiveMessages(room2, ["Msg2"]); await util.markAsRead(room1); await util.assertRead(room1); - // Display the unread first room + // Display the unread first room (room1 moves above room2 as it has an unread thread) await util.receiveMessages(room1, [msg.threadedOff("Msg1", "Resp1")]); await util.saveAndReload(); + // Switch to room3 so neither room1 nor room2 is selected/sticky + await util.goTo(room3); + // Room 1 has an unread message and should be displayed first - await util.assertRoomListOrder([room1, room2]); + await util.assertRoomListOrder([room1, room2, room3]); }); }); }); diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts index 5549b2d315..69e17eeef1 100644 --- a/playwright/e2e/right-panel/file-panel.spec.ts +++ b/playwright/e2e/right-panel/file-panel.spec.ts @@ -23,7 +23,9 @@ async function uploadFile(page: Page, file: string) { // Wait until the file is sent await expect(page.locator(".mx_RoomView_statusArea_expanded")).not.toBeVisible(); - await expect(page.locator(".mx_EventTile.mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); + await expect(page.locator(".mx_EventTile.mx_EventTile_last").getByRole("status")).toHaveAccessibleName( + "Your message was sent", + ); } test.describe("FilePanel", () => { diff --git a/playwright/e2e/room/create-room.spec.ts b/playwright/e2e/room/create-room.spec.ts index dd0ea63776..554f972c7d 100644 --- a/playwright/e2e/room/create-room.spec.ts +++ b/playwright/e2e/room/create-room.spec.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type Page } from "playwright-core"; +import { type Page } from "@playwright/test"; import { SettingLevel } from "../../../src/settings/SettingLevel"; import { UIFeature } from "../../../src/settings/UIFeature"; diff --git a/playwright/e2e/room/room-status-bar.spec.ts b/playwright/e2e/room/room-status-bar.spec.ts index 249aa6e9d5..78d5c49a30 100644 --- a/playwright/e2e/room/room-status-bar.spec.ts +++ b/playwright/e2e/room/room-status-bar.spec.ts @@ -83,6 +83,13 @@ test.describe("Room Status Bar", () => { const banner = page.getByRole("region", { name: "Room status bar" }); await expect(banner).toBeVisible({ timeout: 15000 }); await expect(banner).toMatchScreenshot("consent.png"); + + // Click consent + await banner.getByRole("link", { name: "View Terms and Conditions" }).click(); + await page.unroute("**/_matrix/client/**/send**"); + // Should now be allowed to retry. + await banner.getByRole("button", { name: "Retry all" }).click(); + await expect(banner).not.toBeVisible(); }, ); test.describe("Message fails to send", () => { @@ -161,7 +168,7 @@ test.describe("Room Status Bar", () => { await composer.fill("Hello"); await composer.press("Enter"); - const banner = page.getByText("!Some of your messages have"); + const banner = page.getByRole("status", { name: "Could not start a chat with this user" }); await expect(banner).toBeVisible(); await expect(banner).toMatchScreenshot("local_room_create_failed.png"); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts index df41ef4b70..c6af256a5d 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts @@ -56,4 +56,35 @@ test.describe("Appearance user settings tab", () => { // Assert that the font-family value was removed await expect(page.locator("body")).toHaveCSS("font-family", '""'); }); + + test( + "should keep same font and emoji when switching theme", + { tag: "@screenshot" }, + async ({ page, app, user, util }) => { + const roomId = await util.createAndDisplayRoom(); + await app.client.sendMessage(roomId, { body: "Message with 🦡", msgtype: "m.text" }); + + await app.settings.openUserSettings("Appearance"); + const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); + await tab.getByRole("button", { name: "Show advanced" }).click(); + await tab.getByRole("switch", { name: "Use bundled emoji font" }).click(); + await tab.getByRole("switch", { name: "Use a system font" }).click(); + + await app.closeDialog(); + await expect(page).toMatchScreenshot("window-before-switch.png", { + mask: [page.locator(".mx_MessageTimestamp")], + }); + + // Switch to dark theme + await app.settings.openUserSettings("Appearance"); + await util.getMatchSystemThemeSwitch().click(); + await util.getDarkTheme().click(); + + await app.closeDialog(); + // Font and emoji should remain the same after theme switch + await expect(page).toMatchScreenshot("window-after-switch.png", { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }, + ); }); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/index.ts b/playwright/e2e/settings/appearance-user-settings-tab/index.ts index 15f2f47888..d27b472017 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/index.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/index.ts @@ -150,9 +150,10 @@ class Helpers { /** * Create and display a room named Test Room */ - async createAndDisplayRoom() { - await this.app.client.createRoom({ name: "Test Room" }); + async createAndDisplayRoom(): Promise { + const roomId = await this.app.client.createRoom({ name: "Test Room" }); await this.app.viewRoomByName("Test Room"); + return roomId; } /** diff --git a/playwright/e2e/settings/room-settings/room-security-tab.spec.ts b/playwright/e2e/settings/room-settings/room-security-tab.spec.ts index 605d38abc2..6944067326 100644 --- a/playwright/e2e/settings/room-settings/room-security-tab.spec.ts +++ b/playwright/e2e/settings/room-settings/room-security-tab.spec.ts @@ -74,9 +74,7 @@ test.describe("Roles & Permissions room settings tab", () => { await settingsGroupAccess.getByText("Private (invite only)").click(); // Element should have automatically set the room to "sharing" history visibility - await expect( - settingsGroupHistory.getByText("Members only (since the point in time of selecting this option)"), - ).toBeChecked(); + await expect(settingsGroupHistory.getByText("Members (full history)")).toBeChecked(); }, ); diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index b31deadace..38aff5928d 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -38,7 +38,7 @@ const test = base.extend<{ test.describe("Sliding Sync", () => { const checkOrder = async (wantOrder: string[], page: Page) => { - await expect(page.getByTestId("room-list").locator(".mx_RoomListItemView_text")).toHaveText(wantOrder); + await expect(page.getByTestId("room-list").getByTestId("room-name")).toHaveText(wantOrder); }; const bumpRoom = async (roomId: string, app: ElementAppPage) => { diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts index cf889f64cc..c4781c1c27 100644 --- a/playwright/e2e/spaces/threads-activity-centre/index.ts +++ b/playwright/e2e/spaces/threads-activity-centre/index.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { type JSHandle, type Locator, type Page } from "@playwright/test"; -import type { MatrixEvent, IContent, Room } from "matrix-js-sdk/src/matrix"; +import type { MatrixEvent, IContent, Room, Preset } from "matrix-js-sdk/src/matrix"; import { test as base, expect } from "../../../element-web-test"; import { type Bot } from "../../../pages/bot"; import { type Client } from "../../../pages/client"; @@ -37,7 +37,11 @@ export const test = base.extend<{ room1Name: "Room 1", room1: async ({ room1Name: name, app, user, bot }, use) => { - const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] }); + const roomId = await app.client.createRoom({ + name, + invite: [bot.credentials.userId], + preset: "public_chat" as Preset, + }); await bot.awaitRoomMembership(roomId); await use({ name, roomId }); }, diff --git a/playwright/e2e/spotlight/spotlight.spec.ts b/playwright/e2e/spotlight/spotlight.spec.ts index e07643c193..c1eb2b5923 100644 --- a/playwright/e2e/spotlight/spotlight.spec.ts +++ b/playwright/e2e/spotlight/spotlight.spec.ts @@ -345,10 +345,10 @@ test.describe("Spotlight", () => { await expect(resultLocator).toHaveCount(1); await expect(resultLocator.first()).toContainText(bot2.credentials.displayName); - await expect(spotlight.dialog.locator(".mx_SpotlightDialog_startGroupChat")).toContainText( + await expect(spotlight.dialog.locator("#mx_SpotlightDialog_button_startGroupChat")).toContainText( "Start a group chat", ); - await spotlight.dialog.locator(".mx_SpotlightDialog_startGroupChat").click(); + await spotlight.dialog.locator("#mx_SpotlightDialog_button_startGroupChat").click(); await expect(page.getByRole("dialog")).toContainText("Direct Messages"); }); diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts index 5ffc2003b8..e9e40dc4b4 100644 --- a/playwright/e2e/threads/threads.spec.ts +++ b/playwright/e2e/threads/threads.spec.ts @@ -410,7 +410,9 @@ test.describe("Threads", () => { await textbox.fill("Please come here"); await textbox.press("Enter"); // Wait until the reply is sent - await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); + await expect(locator.locator(".mx_EventTile_last").getByRole("status")).toHaveAccessibleName( + "Your message was sent", + ); // Take a snapshot of reply to the shared location await page.addStyleTag({ content: css }); diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 806e7e9038..b4e927cdac 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -385,8 +385,8 @@ test.describe("Timeline", () => { // Make sure the second message was sent await expect( - page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"), - ).toBeVisible(); + page.locator(".mx_RoomView_MessageList > .mx_EventTile_last").getByRole("status"), + ).toHaveAccessibleName("Your message was sent"); // 1. Alignment of collapsed GELS (generic event list summary) and messages // Check inline start spacing of collapsed GELS @@ -468,8 +468,8 @@ test.describe("Timeline", () => { page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody"), ).toBeVisible(); await expect( - page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent"), - ).toBeVisible(); + page.locator(".mx_GenericEventListSummary .mx_EventTile_last").getByRole("status"), + ).toHaveAccessibleName("Your message was sent"); // Record alignment of expanded GELS and placeholder of deleted message on messagePanel await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( "expanded-gels-redaction-placeholder.png", @@ -502,8 +502,8 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_EventTile_emote .mx_EventTile_avatar")).toHaveCSS("margin-left", "99px"); // Make sure emote was sent await expect( - page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent"), - ).toBeVisible(); + page.locator(".mx_EventTile_last.mx_EventTile_emote").getByRole("status"), + ).toHaveAccessibleName("Your message was sent"); // Record alignment of expanded GELS, placeholder of deleted message, and emote await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", { // Exclude timestamp from snapshot of mx_MainSplit @@ -772,7 +772,9 @@ test.describe("Timeline", () => { // Wait until the file is sent await expect(page.locator(".mx_RoomView_statusArea_expanded")).not.toBeVisible(); - await expect(page.locator(".mx_EventTile.mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); + await expect(page.locator(".mx_EventTile.mx_EventTile_last").getByRole("status")).toHaveAccessibleName( + "Your message was sent", + ); // Assert that the file size is displayed in kibibytes (1024 bytes), not kilobytes (1000 bytes) // See: https://github.com/vector-im/element-web/issues/24866 @@ -947,6 +949,10 @@ test.describe("Timeline", () => { await page.getByRole("textbox", { name: "Edit message" }).press("Enter"); const newTile = page.locator(".mx_EventTile"); + const codeBlock = newTile.locator(".mx_EventTile_pre_container"); + await expect(codeBlock).toBeVisible(); + await codeBlock.hover(); + await expect(newTile.locator(".mx_EventTile_copyButton")).toBeVisible(); await expect(newTile).toMatchScreenshot("edited-code-block.png", { css: ` .mx_MessageTimestamp { @@ -1151,7 +1157,9 @@ test.describe("Timeline", () => { // Assert that 'reply2' was sent await expect(page.locator(".mx_RoomView_body .mx_EventTile_last").getByText(reply2)).toBeVisible(); - await expect(page.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); + await expect(page.locator(".mx_EventTile_last").getByRole("status")).toHaveAccessibleName( + "Your message was sent", + ); // Exclude timestamp and read marker from snapshot const screenshotOptions = { @@ -1301,7 +1309,7 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); const lastEventTileIrc = page.locator(".mx_EventTile_last[data-layout='irc']"); await expect(lastEventTileIrc.locator(".mx_MTextBody").first()).toBeVisible(); - await expect(lastEventTileIrc.locator(".mx_EventTile_receiptSent")).toBeVisible(); // rendered at the bottom of EventTile + await expect(lastEventTileIrc.getByRole("status")).toHaveAccessibleName("Your message was sent"); // rendered at the bottom of EventTile // Take a snapshot in IRC layout await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( "long-strings-with-reply-irc-layout.png", @@ -1314,7 +1322,7 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); const lastEventTileGroup = page.locator(".mx_EventTile_last[data-layout='group']"); await expect(lastEventTileGroup.locator(".mx_MTextBody").first()).toBeVisible(); - await expect(lastEventTileGroup.locator(".mx_EventTile_receiptSent")).toBeVisible(); + await expect(lastEventTileGroup.getByRole("status")).toHaveAccessibleName("Your message was sent"); await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( "long-strings-with-reply-modern-layout.png", screenshotOptions, @@ -1326,7 +1334,7 @@ test.describe("Timeline", () => { await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible(); const lastEventTileBubble = page.locator(".mx_EventTile_last[data-layout='bubble']"); await expect(lastEventTileBubble.locator(".mx_MTextBody").first()).toBeVisible(); - await expect(lastEventTileBubble.locator(".mx_EventTile_receiptSent")).toBeVisible(); + await expect(lastEventTileBubble.getByRole("status")).toHaveAccessibleName("Your message was sent"); await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot( "long-strings-with-reply-bubble-layout.png", screenshotOptions, diff --git a/playwright/e2e/voip/element-call.spec.ts b/playwright/e2e/voip/element-call.spec.ts index 7c2494d2ae..b481f241cc 100644 --- a/playwright/e2e/voip/element-call.spec.ts +++ b/playwright/e2e/voip/element-call.spec.ts @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { readFile } from "node:fs/promises"; -import { type Page } from "playwright-core"; +import { type Page } from "@playwright/test"; import type { EventType, Preset } from "matrix-js-sdk/src/matrix"; import { SettingLevel } from "../../../src/settings/SettingLevel"; @@ -74,7 +74,7 @@ async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "n }, "m.relates_to": { event_id: resp.event_id, - rel_type: "org.matrix.msc4075.rtc.notification.parent", + rel_type: "m.reference", }, "m.call.intent": intent, "notification_type": notification, @@ -82,6 +82,22 @@ async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "n }); } +test.use({ + synapseConfig: { + experimental_features: { + msc4143_enabled: true, + }, + matrix_rtc: { + transports: [ + { + type: "livekit", + livekit_service_url: "https://example.org/can-be-anything", + }, + ], + }, + }, +}); + test.describe("Element Call", () => { test.use({ config: { @@ -647,6 +663,10 @@ test.describe("Element Call", () => { // For this test we want to display the chat area alongside the widget await page.getByRole("button", { name: "Chat" }).click(); + // Wait for the right panel to show the timeline. + await expect( + page.locator(".mx_RightPanel .mx_TimelineCard").getByText("Alice created and configured the room."), + ).toBeVisible(); await page .locator('iframe[title="Element Call"]') @@ -654,7 +674,12 @@ test.describe("Element Call", () => { .getByRole("button", { name: "Send Room Message" }) .click(); - const messageSent = await page.getByText("I sent this once!!").count(); + const timelineLocator = page.locator(".mx_RightPanel .mx_TimelineCard"); + // First wait for the message to appear in the timeline then + // check the count. This improves test stability as we know the message has been sent. + await expect(timelineLocator.getByText("I sent this once!!")).toBeVisible(); + + const messageSent = await timelineLocator.getByText("I sent this once!!").count(); expect(messageSent).toBe(1); }); diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index e51ed7b5d4..e5a1aab31c 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -244,24 +244,6 @@ export class ElementAppPage { await this.page.getByRole("dialog").getByRole("button", { name: "Invite" }).click(); } - /** - * Get a locator for the tooltip associated with an element - * @param e The element with the tooltip - * @returns Locator to the tooltip - */ - public async getTooltipForElement(e: Locator): Promise { - const [labelledById, describedById] = await Promise.all([ - e.getAttribute("aria-labelledby"), - e.getAttribute("aria-describedby"), - ]); - if (!labelledById && !describedById) { - throw new Error( - "Element has no aria-labelledby or aria-describedy attributes! The tooltip should have added either one of these.", - ); - } - return this.page.locator(`id=${labelledById ?? describedById}`); - } - /** * Close the notification toast */ diff --git a/playwright/pages/bot.ts b/playwright/pages/bot.ts index c3168a89ac..300721d0ec 100644 --- a/playwright/pages/bot.ts +++ b/playwright/pages/bot.ts @@ -13,7 +13,7 @@ import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import type { Logger } from "matrix-js-sdk/src/logger"; import type { SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage"; import type { Credentials, HomeserverInstance } from "../plugins/homeserver"; -import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; +import type { CryptoCallbacks, GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; import { bootstrapCrossSigningForClient, Client } from "./client"; export interface CredentialsOptionalAccessToken extends Omit { @@ -141,22 +141,12 @@ export class Bot extends Client { const logger = getLogger(`bot ${credentials.userId}`); - const keys = {}; - - const getCrossSigningKey = (type: string) => { - return keys[type]; - }; - - const saveCrossSigningKeys = (k: Record) => { - Object.assign(keys, k); - }; - // Store the cached secret storage key and return it when `getSecretStorageKey` is called - let cachedKey: { keyId: string; key: Uint8Array }; + let cachedKey: { keyId: string; key: Uint8Array }; const cacheSecretStorageKey = ( keyId: string, keyInfo: SecretStorageKeyDescription, - key: Uint8Array, + key: Uint8Array, ) => { cachedKey = { keyId, @@ -165,11 +155,9 @@ export class Bot extends Client { }; const getSecretStorageKey = () => - Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]); + Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]); - const cryptoCallbacks = { - getCrossSigningKey, - saveCrossSigningKeys, + const cryptoCallbacks: CryptoCallbacks = { cacheSecretStorageKey, getSecretStorageKey, }; diff --git a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png index b0bb64273e..b55e83b09a 100644 Binary files a/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png and b/playwright/snapshots/app-loading/feature-detection.spec.ts/unsupported-browser-CompatibilityView-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png index ad46ca44d5..a0aa6bebde 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png index d4c123d787..7866accb70 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png index 97097a3f28..26e8bf6449 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png index 5d240ebd76..ba5d27771e 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png index 2620b9cd20..b60332c1a1 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png index 6c09f076f9..29e61d6968 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png index 06f850704d..17d3ccbd10 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png index 5f4e409c3a..16d212bfe0 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png index 74060e2f42..76d9745d47 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png index 706ed77713..a15bd80ccf 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png index ac0c53994d..e136a01f3b 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png index 07534229d8..e0dfb3601a 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png index a3a1894eac..bcab07b3f5 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png index aa9956a50d..d55718a5ae 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png index 45a80c8ed5..1d0d8f6118 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png index b12365c9ac..226fc99f00 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png index 724bcb482f..06d5b6bde8 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png differ diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png index 340015a06b..c843ff8d48 100644 Binary files a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png differ diff --git a/playwright/snapshots/composer/CIDER.spec.ts/emoji-picker-linux.png b/playwright/snapshots/composer/CIDER.spec.ts/emoji-picker-linux.png index 8c0daa555c..93889d05e3 100644 Binary files a/playwright/snapshots/composer/CIDER.spec.ts/emoji-picker-linux.png and b/playwright/snapshots/composer/CIDER.spec.ts/emoji-picker-linux.png differ diff --git a/playwright/snapshots/composer/CIDER.spec.ts/emoji-picker-small-linux.png b/playwright/snapshots/composer/CIDER.spec.ts/emoji-picker-small-linux.png index e3911831de..ffbbf10025 100644 Binary files a/playwright/snapshots/composer/CIDER.spec.ts/emoji-picker-small-linux.png and b/playwright/snapshots/composer/CIDER.spec.ts/emoji-picker-small-linux.png differ diff --git a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png index 14cb5ce372..433d80278d 100644 Binary files a/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png and b/playwright/snapshots/crypto/crypto.spec.ts/RoomSummaryCard-with-verified-e2ee-linux.png differ diff --git a/playwright/snapshots/crypto/decryption-failure-messages.spec.ts/history-not-available-linux.png b/playwright/snapshots/crypto/decryption-failure-messages.spec.ts/history-not-available-linux.png new file mode 100644 index 0000000000..351e2c5d02 Binary files /dev/null and b/playwright/snapshots/crypto/decryption-failure-messages.spec.ts/history-not-available-linux.png differ diff --git a/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-authenticity-linux.png b/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-authenticity-linux.png new file mode 100644 index 0000000000..5980ff3ca0 Binary files /dev/null and b/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-authenticity-linux.png differ diff --git a/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-identity-reset-linux.png b/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-identity-reset-linux.png new file mode 100644 index 0000000000..3499e8f704 Binary files /dev/null and b/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-identity-reset-linux.png differ diff --git a/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-not-verified-linux.png b/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-not-verified-linux.png new file mode 100644 index 0000000000..3499e8f704 Binary files /dev/null and b/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-not-verified-linux.png differ diff --git a/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-utd-linux.png b/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-utd-linux.png new file mode 100644 index 0000000000..3d80c319c8 Binary files /dev/null and b/playwright/snapshots/crypto/event-shields.spec.ts/event-shield-utd-linux.png differ diff --git a/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png b/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png index c33dbaadf9..8aa140d0d2 100644 Binary files a/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png and b/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png differ diff --git a/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png b/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png index c5ca8fdc02..5439a4cd5a 100644 Binary files a/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png and b/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png differ diff --git a/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-locally-linux.png b/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-locally-linux.png new file mode 100644 index 0000000000..c6a06a5035 Binary files /dev/null and b/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-locally-linux.png differ diff --git a/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-via-url-linux.png b/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-via-url-linux.png new file mode 100644 index 0000000000..01a70ee5c4 Binary files /dev/null and b/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-via-url-linux.png differ diff --git a/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png b/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png index ab42c30cd2..b39a8c82cb 100644 Binary files a/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png and b/playwright/snapshots/invite/invite-dialog.spec.ts/send-your-first-message-view-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png index ac3f26e529..64d49f10bc 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-linux.png index 5e6ddff442..8256ddbe61 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png index 43d8781239..583b9f36f9 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-header-space-menu-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-space-header-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-space-header-linux.png index c42c449281..008c9076b3 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-space-header-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-header.spec.ts/room-list-space-header-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png index 3795176be2..269c64d00c 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png index f21e92a373..3fa9fd4b0b 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png index b44e61b3eb..3e68b09ad6 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png index c182c853b9..68762d038d 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png index 950bc3a0eb..cdebb2131d 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png differ diff --git a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png index 92839f2ad9..9ce739c75f 100644 Binary files a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png and b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png index febb73c235..775857a5f7 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png differ diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png index 0645b780b8..abe08d0433 100644 Binary files a/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png and b/playwright/snapshots/right-panel/right-panel.spec.ts/with-name-and-address-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png index e02a1a24cf..102d66419a 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-no-results-linux.png differ diff --git a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png index 213cdb1d87..41d21f856a 100644 Binary files a/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png and b/playwright/snapshots/room-directory/room-directory.spec.ts/filtered-one-result-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png index 4ba22a5220..d30c0fe32d 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png index ef6112da1d..8f33116981 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png differ diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-with-icon-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-with-icon-linux.png index c872720f66..f7c2e3c950 100644 Binary files a/playwright/snapshots/room/room-header.spec.ts/room-header-with-icon-linux.png and b/playwright/snapshots/room/room-header.spec.ts/room-header-with-icon-linux.png differ diff --git a/playwright/snapshots/room/room-status-bar.spec.ts/connectivity-lost-linux.png b/playwright/snapshots/room/room-status-bar.spec.ts/connectivity-lost-linux.png index 106f16403c..0568892b39 100644 Binary files a/playwright/snapshots/room/room-status-bar.spec.ts/connectivity-lost-linux.png and b/playwright/snapshots/room/room-status-bar.spec.ts/connectivity-lost-linux.png differ diff --git a/playwright/snapshots/room/room-status-bar.spec.ts/consent-linux.png b/playwright/snapshots/room/room-status-bar.spec.ts/consent-linux.png index 13aa6a4833..c528eb619a 100644 Binary files a/playwright/snapshots/room/room-status-bar.spec.ts/consent-linux.png and b/playwright/snapshots/room/room-status-bar.spec.ts/consent-linux.png differ diff --git a/playwright/snapshots/room/room-status-bar.spec.ts/local-room-create-failed-linux.png b/playwright/snapshots/room/room-status-bar.spec.ts/local-room-create-failed-linux.png index a8fe32646e..304a425466 100644 Binary files a/playwright/snapshots/room/room-status-bar.spec.ts/local-room-create-failed-linux.png and b/playwright/snapshots/room/room-status-bar.spec.ts/local-room-create-failed-linux.png differ diff --git a/playwright/snapshots/room/room-status-bar.spec.ts/message-failed-linux.png b/playwright/snapshots/room/room-status-bar.spec.ts/message-failed-linux.png index cccc5fb675..fe23d40790 100644 Binary files a/playwright/snapshots/room/room-status-bar.spec.ts/message-failed-linux.png and b/playwright/snapshots/room/room-status-bar.spec.ts/message-failed-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png index 5a9190aa6b..dc019d4fd5 100644 Binary files a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/appearance-tab-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-after-switch-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-after-switch-linux.png new file mode 100644 index 0000000000..cdb4586302 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-after-switch-linux.png differ diff --git a/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-before-switch-linux.png b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-before-switch-linux.png new file mode 100644 index 0000000000..bc78959225 Binary files /dev/null and b/playwright/snapshots/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts/window-before-switch-linux.png differ diff --git a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png index c2d1cc52a0..4f5ed98c6e 100644 Binary files a/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png and b/playwright/snapshots/settings/general-room-settings-tab.spec.ts/General-room-settings-tab-should-be-rendered-properly-1-linux.png differ diff --git a/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png b/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png index 6149e4fe55..f3bd63e1d2 100644 Binary files a/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png and b/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-linux.png differ diff --git a/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png b/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png index fb48a55879..48f81bd7f9 100644 Binary files a/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png and b/playwright/snapshots/settings/room-settings/room-security-tab.spec.ts/room-security-settings-world-readable-linux.png differ diff --git a/playwright/snapshots/settings/room-settings/room-video-tab.spec.ts/room-video-settings-linux.png b/playwright/snapshots/settings/room-settings/room-video-tab.spec.ts/room-video-settings-linux.png index 37303257b7..aca5393f02 100644 Binary files a/playwright/snapshots/settings/room-settings/room-video-tab.spec.ts/room-video-settings-linux.png and b/playwright/snapshots/settings/room-settings/room-video-tab.spec.ts/room-video-settings-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png index 0785ecbe97..8aecb37019 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png differ diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png index 8b6af97d6e..4a0ac14609 100644 Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png differ diff --git a/playwright/snapshots/threads/threads.spec.ts/thread-panel-linux.png b/playwright/snapshots/threads/threads.spec.ts/thread-panel-linux.png index f0871c7625..c42007680d 100644 Binary files a/playwright/snapshots/threads/threads.spec.ts/thread-panel-linux.png and b/playwright/snapshots/threads/threads.spec.ts/thread-panel-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png index 1a0a852cd1..4b5c6ff7e9 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png index cf5e66851c..0ad4a0f191 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png index d9359dab70..10896ff87d 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png index 7c660888e6..2e02360ce9 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png index 63eb2da98f..e8c495e8e9 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png index f943108d73..f5dfef5714 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png index 912fa679b8..376f2d2d94 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png index 9fbe9c79e1..383a43bcae 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png index 39fffefc70..d05b9938d5 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png index 7c660888e6..2e02360ce9 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png index 3684f57b11..4196fedfbf 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png index 39c40e0b13..d0ec667834 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png index 74a2028787..8ab5bc91b7 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png index f6785d89c9..6b864dc8c0 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png index fdcbb28133..766a6fffa1 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png index 78196d2632..fa2d11b810 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png differ diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png index df6aca3083..a2dc6d35bb 100644 Binary files a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png differ diff --git a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png index 79335b66ae..a911341579 100644 Binary files a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png and b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png differ diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png index d25b9467d0..f3abb3442d 100644 Binary files a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-checked-linux.png differ diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png index 78a31d1662..069ef66fe1 100644 Binary files a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-video-toast-unchecked-linux.png differ diff --git a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-voice-toast-linux.png b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-voice-toast-linux.png index 29cf843d7b..6703827c6d 100644 Binary files a/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-voice-toast-linux.png and b/playwright/snapshots/voip/element-call.spec.ts/incoming-call-group-voice-toast-linux.png differ diff --git a/playwright/testcontainers/mas.ts b/playwright/testcontainers/mas.ts index 61cc53bbc9..5b2aea667c 100644 --- a/playwright/testcontainers/mas.ts +++ b/playwright/testcontainers/mas.ts @@ -10,7 +10,7 @@ import { type StartedPostgreSqlContainer, } from "@element-hq/element-web-playwright-common/lib/testcontainers"; -const TAG = "main@sha256:2c5966c2ff06458ac5cbae959f12e19d30e3ebb63c641d31ec1ae08abccb9c6d"; +const TAG = "main@sha256:2eec80f8348ed1f78414cc9258399b9329b53e6e9e2e634da484568996c1da8f"; /** * MatrixAuthenticationServiceContainer which freezes the docker digest to diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 1359cd0f69..c68668761a 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; -const TAG = "develop@sha256:7c3dce1d2b44fdc4b1494c5b8f4792018733ad323f823b88aac30c883d09fb35"; +const TAG = "develop@sha256:f0de453dbb284112c4bc91be01345f189f218bf51ee09ed565dab295c0a72b44"; /** * SynapseContainer which freezes the docker digest to stabilise tests, diff --git a/res/css/_common.pcss b/res/css/_common.pcss index ec83fef901..736c535d65 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -22,6 +22,7 @@ Please see LICENSE files in the repository root for full details. --buttons-dialog-gap-row: $spacing-8; --buttons-dialog-gap-column: $spacing-8; --MBody-border-radius: 8px; + --EventTileBubble_margin-block: 10px; /* Expected z-indexes for dialogs: 4000 - Default wrapper index @@ -496,10 +497,9 @@ legend { } @define-mixin customisedCancelButton { - cursor: pointer; - position: relative; - width: 28px; - height: 28px; + width: 20px; + height: 20px; + padding: var(--cpd-space-1x); border-radius: 14px; background-color: var(--cpd-color-bg-subtle-secondary); @@ -507,18 +507,10 @@ legend { background-color: var(--cpd-color-bg-subtle-primary); } - &::before { - content: ""; - width: 28px; - height: 28px; - left: 0; - top: 0; - position: absolute; - mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 20px; - background-color: var(--cpd-color-icon-secondary); + svg { + width: inherit; + height: inherit; + color: var(--cpd-color-icon-secondary); } } @@ -858,18 +850,16 @@ legend { line-height: $font-24px; margin-right: 0; - span { - display: flex; - align-items: center; + display: flex; + align-items: center; - &::before { - content: ""; - display: inline-block; - background-color: $button-fg-color; - mask-position: center; - mask-repeat: no-repeat; - margin-right: 8px; - } + svg { + width: 16px; + height: 16px; + display: inline-block; + color: $button-fg-color; + margin-right: 8px; + vertical-align: middle; } } @@ -882,15 +872,11 @@ legend { } @define-mixin ThreadSummaryIcon { - content: ""; display: inline-block; - mask-image: url("@vector-im/compound-design-tokens/icons/threads.svg"); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; height: 18px; - min-width: 18px; - background-color: $icon-button-color !important; + width: 18px; + flex-shrink: 0; + color: $icon-button-color; } @define-mixin composerButtonHighLight { diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 9711c1b064..53f99f8a35 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -74,7 +74,6 @@ @import "./structures/_QuickSettingsButton.pcss"; @import "./structures/_RightPanel.pcss"; @import "./structures/_RoomSearch.pcss"; -@import "./structures/_RoomStatusBar.pcss"; @import "./structures/_RoomView.pcss"; @import "./structures/_SearchBox.pcss"; @import "./structures/_SpaceHierarchy.pcss"; @@ -221,9 +220,7 @@ @import "./views/messages/_CallEvent.pcss"; @import "./views/messages/_CreateEvent.pcss"; @import "./views/messages/_DateSeparator.pcss"; -@import "./views/messages/_DecryptionFailureBody.pcss"; @import "./views/messages/_DisambiguatedProfile.pcss"; -@import "./views/messages/_EventTileBubble.pcss"; @import "./views/messages/_HiddenBody.pcss"; @import "./views/messages/_HiddenMediaPlaceholder.pcss"; @import "./views/messages/_JumpToDatePicker.pcss"; @@ -249,7 +246,6 @@ @import "./views/messages/_RedactedBody.pcss"; @import "./views/messages/_RoomAvatarEvent.pcss"; @import "./views/messages/_TextualEvent.pcss"; -@import "./views/messages/_TimelineSeparator.pcss"; @import "./views/messages/_UnknownBody.pcss"; @import "./views/messages/_ViewSourceEvent.pcss"; @import "./views/messages/_common_CryptoEvent.pcss"; @@ -267,15 +263,7 @@ @import "./views/right_panel/_VerificationPanel.pcss"; @import "./views/right_panel/_WidgetCard.pcss"; @import "./views/room_settings/_AliasSettings.pcss"; -@import "./views/rooms/RoomListPanel/_EmptyRoomList.pcss"; -@import "./views/rooms/RoomListPanel/_RoomList.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListItemView.pcss"; @import "./views/rooms/RoomListPanel/_RoomListPanel.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListSecondaryFilters.pcss"; -@import "./views/rooms/RoomListPanel/_RoomListSkeleton.pcss"; @import "./views/rooms/_AppsDrawer.pcss"; @import "./views/rooms/_Autocomplete.pcss"; @import "./views/rooms/_AuxPanel.pcss"; diff --git a/res/css/components/views/polls/_PollOption.pcss b/res/css/components/views/polls/_PollOption.pcss index 4fecce4752..2691cd90cb 100644 --- a/res/css/components/views/polls/_PollOption.pcss +++ b/res/css/components/views/polls/_PollOption.pcss @@ -59,28 +59,19 @@ Please see LICENSE files in the repository root for full details. /* override checked radio button styling to show checkmark instead */ .mx_StyledRadioButton_checked { input[type="radio"]:checked + div { - position: relative; - border-width: 2px; border-color: var(--cpd-color-icon-primary); background-color: var(--cpd-color-icon-primary); - &::before { - content: ""; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - mask-image: url("@vector-im/compound-design-tokens/icons/check.svg"); - mask-size: 12px 12px; - mask-repeat: no-repeat; - mask-position: center; - background-color: var(--cpd-color-icon-on-solid-primary); - width: 12px; - height: 12px; - } - div { - visibility: hidden; + background: none; + + svg { + color: var(--cpd-color-icon-on-solid-primary); + width: 12px; + height: 12px; + margin: -2px; + display: block; + } } } } diff --git a/res/css/structures/_HomePage.pcss b/res/css/structures/_HomePage.pcss index 505fc4bad6..df374df4b5 100644 --- a/res/css/structures/_HomePage.pcss +++ b/res/css/structures/_HomePage.pcss @@ -70,29 +70,13 @@ Please see LICENSE files in the repository root for full details. color: #fff; /* on all themes */ background-color: $accent; - &::before { + svg { top: 20px; left: 60px; /* (160px-40px)/2 */ width: 40px; height: 40px; - - content: ""; position: absolute; - background-color: #fff; /* on all themes */ - mask-repeat: no-repeat; - mask-size: contain; - } - - &.mx_HomePage_button_sendDm::before { - mask-image: url("$(res)/img/element-icons/feedback.svg"); - } - - &.mx_HomePage_button_explore::before { - mask-image: url("@vector-im/compound-design-tokens/icons/explore.svg"); - } - - &.mx_HomePage_button_createGroup::before { - mask-image: url("@vector-im/compound-design-tokens/icons/group.svg"); + color: #fff; /* on all themes */ } } } diff --git a/res/css/structures/_LeftPanel.pcss b/res/css/structures/_LeftPanel.pcss index f290772e20..45fc6b0a6a 100644 --- a/res/css/structures/_LeftPanel.pcss +++ b/res/css/structures/_LeftPanel.pcss @@ -36,6 +36,8 @@ Please see LICENSE files in the repository root for full details. display: flex; overflow: hidden; position: relative; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; &[data-collapsed] { max-width: var(--collapsedWidth); @@ -122,6 +124,8 @@ Please see LICENSE files in the repository root for full details. border-radius: 8px; background-color: $panel-actions; margin-left: 8px; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; svg { width: inherit; diff --git a/res/css/structures/_RoomStatusBar.pcss b/res/css/structures/_RoomStatusBar.pcss deleted file mode 100644 index f3b1737812..0000000000 --- a/res/css/structures/_RoomStatusBar.pcss +++ /dev/null @@ -1,175 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { - margin-left: 65px; - min-height: 50px; -} - -.mx_RoomStatusBar_typingIndicatorAvatars { - width: 52px; - margin-top: -1px; - text-align: left; -} - -.mx_RoomStatusBar_typingIndicatorRemaining { - display: inline-block; - color: #acacac; - background-color: #ddd; - border: 1px solid $background; - border-radius: 40px; - width: 24px; - height: 24px; - line-height: $font-24px; - font-size: 0.8em; - vertical-align: top; - text-align: center; - position: absolute; -} - -.mx_RoomStatusBar_scrollDownIndicator { - cursor: pointer; - padding-left: 1px; -} - -.mx_RoomStatusBar_unreadMessagesBar { - padding-top: 10px; - color: $alert; - cursor: pointer; -} - -.mx_RoomStatusBar_connectionLostBar { - display: flex; - - margin-top: 19px; - min-height: 58px; -} - -.mx_RoomStatusBar_unsentMessages { - > div[role="alert"] { - /* cheat some basic alignment */ - display: flex; - align-items: center; - min-height: 70px; - margin: 12px; - padding-left: 16px; - background-color: $header-panel-bg-color; - border-radius: 4px; - } - - .mx_RoomStatusBar_unsentBadge { - margin-right: 12px; - - .mx_NotificationBadge { - /* Override sizing from the default badge */ - width: 24px !important; - height: 24px !important; - border-radius: 24px !important; - - .mx_NotificationBadge_count { - font-size: $font-16px !important; /* override default */ - } - } - } - - .mx_RoomStatusBar_unsentTitle { - color: $alert; - font-size: $font-15px; - } - - .mx_RoomStatusBar_unsentDescription { - font-size: $font-12px; - } - - .mx_RoomStatusBar_unsentButtonBar { - flex-grow: 1; - text-align: right; - margin-right: 22px; - color: $muted-fg-color; - - .mx_AccessibleButton { - padding: 5px 10px; - padding-left: 30px; /* 18px for the icon, 2px margin to text, 10px regular padding */ - display: inline-block; - position: relative; - user-select: none; - - & + .mx_AccessibleButton { - border-left: 1px solid $resend-button-divider-color; - } - - svg { - left: 10px; /* inset for regular button padding */ - - width: 18px; - height: 18px; - vertical-align: middle; - color: $muted-fg-color; - } - } - - .mx_InlineSpinner { - vertical-align: middle; - margin-right: 5px; - top: 1px; /* just to help the vertical alignment be slightly better */ - - & + span { - margin-right: 10px; /* same margin/padding as the rightmost button */ - } - } - } -} - -.mx_RoomStatusBar_connectionLostBar svg { - padding-left: 10px; - padding-right: 10px; - vertical-align: middle; - float: left; -} - -.mx_RoomStatusBar_connectionLostBar_title { - color: $alert; -} - -.mx_RoomStatusBar_connectionLostBar_desc { - color: $primary-content; - font-size: $font-13px; - opacity: 0.5; - padding-bottom: 20px; -} - -.mx_RoomStatusBar_resend_link { - color: $primary-content !important; - text-decoration: underline !important; - cursor: pointer; -} - -.mx_RoomStatusBar_typingBar { - height: 50px; - line-height: 50px; - - color: $primary-content; - opacity: 0.5; - overflow-y: hidden; - display: block; -} - -.mx_MatrixChat_useCompactLayout { - .mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { - min-height: 40px; - } - - .mx_RoomStatusBar_indicator { - margin-top: 10px; - } - - .mx_RoomStatusBar_typingBar { - height: 40px; - line-height: 40px; - } -} diff --git a/res/css/structures/_RoomView.pcss b/res/css/structures/_RoomView.pcss index 03f95020da..6a5ceb641b 100644 --- a/res/css/structures/_RoomView.pcss +++ b/res/css/structures/_RoomView.pcss @@ -55,7 +55,7 @@ Please see LICENSE files in the repository root for full details. .mx_RoomView_messagePanelSearchSpinner { flex: 1; - background-image: url("$(res)/img/typing-indicator-2x.gif"); + background-image: url("/res/img/typing-indicator-2x.gif"); background-position: center 367px; background-size: 25px; background-repeat: no-repeat; diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index a20e26403d..f541aba051 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -22,6 +22,9 @@ Please see LICENSE files in the repository root for full details. /* Fix for the blurred avatar-background */ z-index: 1; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; + /* Create another flexbox so the Panel fills the container */ display: flex; flex-direction: column; @@ -66,6 +69,8 @@ Please see LICENSE files in the repository root for full details. &:hover .mx_SpacePanel_toggleCollapse { opacity: 1; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; } ul { diff --git a/res/css/structures/_SpaceRoomView.pcss b/res/css/structures/_SpaceRoomView.pcss index 25989fd937..60b7583443 100644 --- a/res/css/structures/_SpaceRoomView.pcss +++ b/res/css/structures/_SpaceRoomView.pcss @@ -138,17 +138,16 @@ Please see LICENSE files in the repository root for full details. .mx_FacePile { display: inline-block; cursor: pointer; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; } .mx_SpaceRoomView_landing_inviteButton, .mx_SpaceRoomView_landing_settingsButton { position: relative; - &::before { + svg { position: absolute; - content: ""; - mask-position: center; - mask-repeat: no-repeat; } } @@ -156,13 +155,11 @@ Please see LICENSE files in the repository root for full details. padding: 4px 18px 4px 40px; height: min-content; - &::before { + svg { left: 8px; height: 16px; width: 16px; - background: var(--cpd-color-icon-on-solid-primary); - mask-size: 16px; - mask-image: url("@vector-im/compound-design-tokens/icons/user-add.svg"); + color: var(--cpd-color-icon-on-solid-primary); } } @@ -170,14 +167,12 @@ Please see LICENSE files in the repository root for full details. width: 24px; height: 24px; - &::before { + svg { left: 0; top: 0; height: 24px; width: 24px; - background: $tertiary-content; - mask-size: contain; - mask-image: url("@vector-im/compound-design-tokens/icons/settings-solid.svg"); + color: $tertiary-content; } } } @@ -207,35 +202,23 @@ Please see LICENSE files in the repository root for full details. display: inline-block; padding-left: 32px; line-height: 24px; /* to center icons */ + color: var(--cpd-color-text-primary); + font-weight: var(--cpd-font-weight-semibold); + text-decoration: underline; - &::before { - content: ""; + svg { position: absolute; height: 24px; width: 24px; top: 0; left: 0; - background-color: $secondary-content; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; + color: var(--cpd-color-icon-primary); } & + .mx_AccessibleButton { margin-left: 32px; } } - - .mx_SpaceRoomView_inviteTeammates_inviteDialogButton { - color: var(--cpd-color-text-primary); - font-weight: var(--cpd-font-weight-semibold); - text-decoration: underline; - - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/user-add.svg"); - background-color: var(--cpd-color-icon-primary); - } - } } } } diff --git a/res/css/structures/_SplashPage.pcss b/res/css/structures/_SplashPage.pcss index 8f06541179..6eb01478c9 100644 --- a/res/css/structures/_SplashPage.pcss +++ b/res/css/structures/_SplashPage.pcss @@ -30,7 +30,7 @@ Please see LICENSE files in the repository root for full details. inset: -9px; mask: /* mask to dither resulting combined gradient */ - url("$(res)/img/noise.png"), + url("/res/img/noise.png"), /* gradient to apply different amounts of dithering to different parts of the gradient */ linear-gradient( to bottom, diff --git a/res/css/structures/_TabbedView.pcss b/res/css/structures/_TabbedView.pcss index 60fe2f7211..e02d42b199 100644 --- a/res/css/structures/_TabbedView.pcss +++ b/res/css/structures/_TabbedView.pcss @@ -37,27 +37,12 @@ Please see LICENSE files in the repository root for full details. .mx_TabbedView_tabLabel:hover, .mx_TabbedView_tabLabel_active { color: $tab-label-active-fg-color; - - .mx_TabbedView_maskedIcon::before { - background-color: var(--cpd-color-icon-primary); - } } .mx_TabbedView_tabLabel_active { background-color: var(--cpd-color-bg-subtle-secondary); - } - - .mx_TabbedView_maskedIcon { - width: 20px; - height: 20px; - margin-right: var(--cpd-space-3x); - } - - .mx_TabbedView_maskedIcon::before { - mask-size: 20px; - width: 20px; - height: 20px; - transition: background-color 0.1s; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; } } @@ -89,23 +74,6 @@ Please see LICENSE files in the repository root for full details. color: $accent; } } - - .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { - background-color: $accent; - } - - .mx_TabbedView_maskedIcon { - width: 22px; - height: 22px; - margin-left: 0px; - margin-right: 8px; - } - - .mx_TabbedView_maskedIcon::before { - mask-size: 22px; - width: inherit; - height: inherit; - } } .mx_TabbedView_tabLabels { @@ -136,18 +104,6 @@ Please see LICENSE files in the repository root for full details. } } -.mx_TabbedView_maskedIcon { - display: inline-block; -} - -.mx_TabbedView_maskedIcon::before { - display: inline-block; - background-color: var(--cpd-color-icon-secondary); - mask-repeat: no-repeat; - mask-position: center; - content: ""; -} - .mx_TabbedView_tabLabel_text { vertical-align: middle; } @@ -173,10 +129,6 @@ Please see LICENSE files in the repository root for full details. .mx_TabbedView_tabPanel { margin-left: 72px; /* 40px sidebar + 32px padding */ } - .mx_TabbedView_maskedIcon { - margin-right: auto; - margin-left: auto; - } .mx_TabbedView_tabLabels { width: auto; } diff --git a/res/css/structures/_ToastContainer.pcss b/res/css/structures/_ToastContainer.pcss index 68b39cae61..6f21e495d6 100644 --- a/res/css/structures/_ToastContainer.pcss +++ b/res/css/structures/_ToastContainer.pcss @@ -38,6 +38,7 @@ Please see LICENSE files in the repository root for full details. grid-template-columns: 22px 1fr; column-gap: 8px; row-gap: 4px; + align-items: center; padding: var(--cpd-space-3x); &.mx_Toast_hasIcon { @@ -47,10 +48,17 @@ Please see LICENSE files in the repository root for full details. grid-column: 1; } - .mx_Toast_title, - .mx_Toast_body { + .mx_Toast_title { grid-column: 2; } + + .mx_Toast_body { + grid-column: 2 / 4; + } + + .mx_Toast_closebutton { + grid-column: 3; + } } &:not(.mx_Toast_hasIcon) { padding-left: 12px; @@ -78,13 +86,6 @@ Please see LICENSE files in the repository root for full details. display: inline; width: auto; } - - .mx_Toast_title_countIndicator { - font-size: $font-12px; - line-height: $font-22px; - color: $secondary-content; - margin-inline-start: auto; /* on the end side of the div */ - } } .mx_Toast_body { diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index ddefc84d8f..89b15e68b2 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -99,6 +99,9 @@ Please see LICENSE files in the repository root for full details. display: flex; align-items: center; justify-content: center; + + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; } &.mx_UserMenu_contextMenu_guestPrompts { diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.pcss b/res/css/views/avatars/_DecoratedRoomAvatar.pcss index 6ac5d68496..2ad1b174af 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.pcss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.pcss @@ -13,7 +13,7 @@ Please see LICENSE files in the repository root for full details. line-height: 0; &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { - mask-image: url("$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg"); + mask-image: url("/res/img/element-icons/roomlist/decorated-avatar-mask.svg"); mask-position: center; mask-size: contain; mask-repeat: no-repeat; @@ -27,38 +27,42 @@ Please see LICENSE files in the repository root for full details. width: 25%; /* 8px for a 32x32 avatar */ height: 25%; border-radius: 50%; + + &::before, + svg { + width: 100%; + height: 100%; + right: 0; + position: absolute; + border-radius: 8px; + } } - .mx_DecoratedRoomAvatar_icon::before { - content: ""; - width: 100%; - height: 100%; - right: 0; - position: absolute; - border-radius: 8px; - } - - .mx_DecoratedRoomAvatar_icon_globe::before { - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background: $secondary-content; - mask-image: url("@vector-im/compound-design-tokens/icons/public.svg"); + .mx_DecoratedRoomAvatar_icon_globe svg { + /* Oversize the icon to account for the dead space around the icon within the canvas */ + width: 120%; + height: 120%; + margin: -10%; + color: $secondary-content; } .mx_DecoratedRoomAvatar_icon_offline::before { + content: ""; background-color: $presence-offline; } .mx_DecoratedRoomAvatar_icon_online::before { + content: ""; background-color: $accent; } .mx_DecoratedRoomAvatar_icon_away::before { + content: ""; background-color: $presence-away; } .mx_DecoratedRoomAvatar_icon_busy::before { + content: ""; background-color: $presence-busy; } diff --git a/res/css/views/avatars/_RoomAvatarView.pcss b/res/css/views/avatars/_RoomAvatarView.pcss index 0d5523a9f1..509b51c3e8 100644 --- a/res/css/views/avatars/_RoomAvatarView.pcss +++ b/res/css/views/avatars/_RoomAvatarView.pcss @@ -21,11 +21,11 @@ } .mx_RoomAvatarView_RoomAvatar_icon { - mask-image: url("$(res)/img/element-icons/roomlist/room-avatar-view-icon-mask.svg"); + mask-image: url("/res/img/element-icons/roomlist/room-avatar-view-icon-mask.svg"); } .mx_RoomAvatarView_RoomAvatar_presence { - mask-image: url("$(res)/img/element-icons/roomlist/room-avatar-view-presence-mask.svg"); + mask-image: url("/res/img/element-icons/roomlist/room-avatar-view-presence-mask.svg"); } .mx_RoomAvatarView_icon { diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss index cd5cff94a0..72c0ef9cff 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss @@ -118,23 +118,11 @@ Please see LICENSE files in the repository root for full details. padding: 8px 36px; } - .mx_AddExistingToSpace_retryButton { - margin-left: 12px; - padding-left: 24px; - position: relative; - - &::before { - content: ""; - position: absolute; - background-color: $primary-content; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - mask-image: url("@vector-im/compound-design-tokens/icons/restart.svg"); - width: 18px; - height: 18px; - left: 0; - } + .mx_AddExistingToSpaceDialog_retryButton svg { + color: $primary-content; + width: 18px; + height: 18px; + margin-right: var(--cpd-space-2x); } } } @@ -185,25 +173,17 @@ Please see LICENSE files in the repository root for full details. } } - .mx_Dropdown_menu { - .mx_SubspaceSelector_dropdownOptionActive { - color: $accent; - padding-right: 32px; - position: relative; + .mx_SubspaceSelector_dropdownOptionActive { + color: $accent; + padding-right: 24px; + position: relative; - &::before { - content: ""; - width: 20px; - height: 20px; - top: 8px; - right: 0; - position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background-color: $accent; - mask-image: url("@vector-im/compound-design-tokens/icons/check.svg"); - } + svg { + width: 20px; + height: 20px; + right: 0; + position: absolute; + color: $accent; } } } @@ -225,7 +205,8 @@ Please see LICENSE files in the repository root for full details. margin-bottom: auto; } - .mx_DecoratedRoomAvatar, /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ { + /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ + .mx_DecoratedRoomAvatar { margin-right: 12px; } diff --git a/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss b/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss index ace0060343..5bafec7c0f 100644 --- a/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss +++ b/res/css/views/dialogs/_AnalyticsLearnMoreDialog.pcss @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. .mx_AnalyticsLearnMoreDialog { max-width: 500px; .mx_AnalyticsLearnMore_image_holder { - background-image: url("$(res)/img/element-shiny.svg"); + background-image: url("/res/img/element-shiny.svg"); background-repeat: no-repeat; background-position: center top; height: 112px; @@ -42,18 +42,13 @@ Please see LICENSE files in the repository root for full details. vertical-align: middle; position: relative; - &::before { - content: ""; + svg { position: absolute; width: 26px; height: 26px; left: 0; top: 0; - background-color: #0dbd8b; - mask-image: url("@vector-im/compound-design-tokens/icons/check-circle.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; + color: #0dbd8b; } } } diff --git a/res/css/views/dialogs/_ForwardDialog.pcss b/res/css/views/dialogs/_ForwardDialog.pcss index 68132f0bbb..b155bd1f4a 100644 --- a/res/css/views/dialogs/_ForwardDialog.pcss +++ b/res/css/views/dialogs/_ForwardDialog.pcss @@ -40,8 +40,7 @@ Please see LICENSE files in the repository root for full details. /* that our preview is unencrypted, which doesn't actually matter */ /* We also hide download links to not encourage users to try interacting */ .mx_EventTile_msgOption, - .mx_EventTile_e2eIcon_unencrypted, - .mx_EventTile_e2eIcon_warning, + .mx_EventTile_e2eIcon, .mx_MFileBody_download { display: none; } @@ -135,35 +134,17 @@ Please see LICENSE files in the repository root for full details. visibility: hidden; } - .mx_ForwardList_sendIcon, - .mx_NotificationBadge { + & > svg { position: absolute; + width: 14px; + height: 14px; + color: $accent; } .mx_NotificationBadge { /* Match the failed to send indicator's color with the disabled button */ background-color: $button-danger-disabled-fg-color; } - - &.mx_ForwardList_sending .mx_ForwardList_sendIcon { - background-color: $accent; - mask-image: url("@vector-im/compound-design-tokens/icons/circle.svg"); - mask-position: center; - mask-repeat: no-repeat; - mask-size: 14px; - width: 14px; - height: 14px; - } - - &.mx_ForwardList_sent .mx_ForwardList_sendIcon { - background-color: $accent; - mask-image: url("@vector-im/compound-design-tokens/icons/check-circle.svg"); - mask-position: center; - mask-repeat: no-repeat; - mask-size: 14px; - width: 14px; - height: 14px; - } } } } diff --git a/res/css/views/dialogs/_PollCreateDialog.pcss b/res/css/views/dialogs/_PollCreateDialog.pcss index 0689938a24..ba7d9c644d 100644 --- a/res/css/views/dialogs/_PollCreateDialog.pcss +++ b/res/css/views/dialogs/_PollCreateDialog.pcss @@ -43,23 +43,17 @@ Please see LICENSE files in the repository root for full details. .mx_PollCreateDialog_removeOption { margin-left: 12px; - width: 20px; - height: 20px; + width: 16px; + height: 16px; + padding: var(--cpd-space-0-5x); border-radius: 50%; background-color: $quinary-content; cursor: pointer; - position: relative; - &::before { - content: ""; - mask: url("@vector-im/compound-design-tokens/icons/close.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 16px; + svg { width: inherit; height: inherit; - position: absolute; - background-color: $secondary-content; + color: $secondary-content; } } } diff --git a/res/css/views/dialogs/_RoomSettingsDialog.pcss b/res/css/views/dialogs/_RoomSettingsDialog.pcss index bc64e9543b..9967adca50 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.pcss +++ b/res/css/views/dialogs/_RoomSettingsDialog.pcss @@ -13,11 +13,3 @@ Please see LICENSE files in the repository root for full details. margin: 0 auto; padding-right: 80px; } - -/* show a different AvatarSetting placeholder for RoomProfileSettings which is basically a clone of ProfileSettings */ -.mx_RoomSettingsDialog .mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder::before { - mask: url("@vector-im/compound-design-tokens/icons/image.svg"); - mask-repeat: no-repeat; - mask-size: 36px; - mask-position: center; -} diff --git a/res/css/views/dialogs/_SpotlightDialog.pcss b/res/css/views/dialogs/_SpotlightDialog.pcss index 67d2ebcc39..dcb2247688 100644 --- a/res/css/views/dialogs/_SpotlightDialog.pcss +++ b/res/css/views/dialogs/_SpotlightDialog.pcss @@ -79,12 +79,8 @@ Please see LICENSE files in the repository root for full details. position: relative; padding: $spacing-4 $spacing-8 $spacing-4 37px; - &::before { - background-color: $secondary-content; - content: ""; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; + > svg { + color: $secondary-content; width: 18px; height: 18px; position: absolute; @@ -93,23 +89,11 @@ Please see LICENSE files in the repository root for full details. transform: translateY(-50%); } - &.mx_SpotlightDialog_filterPeople::before { - mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); - } - - &.mx_SpotlightDialog_filterPublicRooms::before { - mask-image: url("@vector-im/compound-design-tokens/icons/room.svg"); - } - - &.mx_SpotlightDialog_filterPublicSpaces::before { - mask-image: url("$(res)/img/element-icons/spaces.svg"); - } - .mx_SpotlightDialog_filter--close { - position: relative; display: inline-block; - width: 16px; - height: 16px; + width: 14px; + height: 14px; + padding: 1px; background: $system; border-radius: 8px; margin-left: $spacing-8; @@ -117,17 +101,10 @@ Please see LICENSE files in the repository root for full details. line-height: 16px; color: $secondary-content; - &::before { - background-color: $secondary-content; - content: ""; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 14px; + svg { + color: $secondary-content; width: inherit; height: inherit; - position: absolute; - left: 0; - mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); } } } @@ -386,45 +363,24 @@ Please see LICENSE files in the repository root for full details. margin: 0; padding: 3px $spacing-8 3px $spacing-28; - &::before { - content: ""; + svg { display: block; position: absolute; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; left: $spacing-8; width: 16px; height: 16px; - background: var(--cpd-color-icon-primary); + color: var(--cpd-color-icon-primary); } } } - .mx_SpotlightDialog_inviteLink .mx_AccessibleButton::before { - mask-image: url("@vector-im/compound-design-tokens/icons/link.svg"); - } - - .mx_SpotlightDialog_createRoom .mx_AccessibleButton::before { - mask-image: url("@vector-im/compound-design-tokens/icons/room.svg"); - } - .mx_SpotlightDialog_otherSearches { - .mx_SpotlightDialog_startChat, - .mx_SpotlightDialog_joinRoomAlias, - .mx_SpotlightDialog_explorePublicRooms, - .mx_SpotlightDialog_explorePublicSpaces, - .mx_SpotlightDialog_startGroupChat, - .mx_SpotlightDialog_searchMessages { + .mx_SpotlightDialog_option { padding-left: $spacing-32; position: relative; - &::before { - background-color: $secondary-content; - content: ""; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; + svg { + color: $secondary-content; width: 24px; height: 24px; position: absolute; @@ -434,30 +390,6 @@ Please see LICENSE files in the repository root for full details. } } - .mx_SpotlightDialog_startChat::before { - mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); - } - - .mx_SpotlightDialog_joinRoomAlias::before { - mask-image: url("@vector-im/compound-design-tokens/icons/room.svg"); - } - - .mx_SpotlightDialog_explorePublicRooms::before { - mask-image: url("@vector-im/compound-design-tokens/icons/room.svg"); - } - - .mx_SpotlightDialog_explorePublicSpaces::before { - mask-image: url("$(res)/img/element-icons/spaces.svg"); - } - - .mx_SpotlightDialog_startGroupChat::before { - mask-image: url("@vector-im/compound-design-tokens/icons/group.svg"); - } - - .mx_SpotlightDialog_searchMessages::before { - mask-image: url("@vector-im/compound-design-tokens/icons/chat.svg"); - } - .mx_SpotlightDialog_otherSearches_messageSearchText { font-size: $font-15px; line-height: $font-24px; @@ -496,27 +428,10 @@ Please see LICENSE files in the repository root for full details. display: none; } - .mx_SpotlightDialog_metaspaceResult { - background-color: $secondary-content; - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - - &.mx_SpotlightDialog_metaspaceResult_home-space { - mask-image: url("@vector-im/compound-design-tokens/icons/home-solid.svg"); - } - - &.mx_SpotlightDialog_metaspaceResult_favourites-space { - mask-image: url("@vector-im/compound-design-tokens/icons/favourite-solid.svg"); - } - - &.mx_SpotlightDialog_metaspaceResult_people-space { - mask-image: url("@vector-im/compound-design-tokens/icons/user-profile-solid.svg"); - } - - &.mx_SpotlightDialog_metaspaceResult_orphans-space { - mask-image: url("@vector-im/compound-design-tokens/icons/room.svg"); - } + .mx_SpotlightDialog_metaspaceResult svg { + color: $secondary-content; + width: inherit; + height: inherit; } } } diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss index 2c78a62f8d..ec4d58edaf 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss @@ -33,14 +33,11 @@ Please see LICENSE files in the repository root for full details. } .mx_AccessSecretStorageDialog_recoveryKeyFeedback { - &::before { - content: ""; + svg { display: inline-block; vertical-align: bottom; width: 20px; height: 20px; - mask-repeat: no-repeat; - mask-position: center; mask-size: 20px; margin-inline-end: 5px; } @@ -48,9 +45,8 @@ Please see LICENSE files in the repository root for full details. &.mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid { color: $alert; - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); - background-color: $alert; + svg { + color: $alert; } } } diff --git a/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss b/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss index e40458f48f..b056e959b8 100644 --- a/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_CreateSecretStorageDialog.pcss @@ -48,25 +48,6 @@ Please see LICENSE files in the repository root for full details. margin-bottom: 1em; } -.mx_CreateSecretStorageDialog_titleWithIcon::before { - content: ""; - display: inline-block; - width: 24px; - height: 24px; - margin-right: 8px; - position: relative; - top: 5px; - background-color: $primary-content; -} - -.mx_CreateSecretStorageDialog_secureBackupTitle::before { - mask-image: url("$(res)/img/feather-customised/secure-backup.svg"); -} - -.mx_CreateSecretStorageDialog_securePhraseTitle::before { - mask-image: url("$(res)/img/feather-customised/secure-phrase.svg"); -} - .mx_CreateSecretStorageDialog_centeredTitle, .mx_CreateSecretStorageDialog_centeredBody { text-align: center; @@ -100,24 +81,6 @@ Please see LICENSE files in the repository root for full details. padding-bottom: 10px; } -.mx_CreateSecretStorageDialog_optionIcon { - display: inline-block; - width: 24px; - height: 24px; - margin-right: 8px; - position: relative; - top: 5px; - background-color: $primary-content; -} - -.mx_CreateSecretStorageDialog_optionIcon_securePhrase { - mask-image: url("$(res)/img/feather-customised/secure-phrase.svg"); -} - -.mx_CreateSecretStorageDialog_optionIcon_secureBackup { - mask-image: url("$(res)/img/feather-customised/secure-backup.svg"); -} - .mx_CreateSecretStorageDialog_passPhraseContainer { display: flex; align-items: flex-start; diff --git a/res/css/views/elements/_AccessibleButton.pcss b/res/css/views/elements/_AccessibleButton.pcss index d9d78912f0..fafe75c642 100644 --- a/res/css/views/elements/_AccessibleButton.pcss +++ b/res/css/views/elements/_AccessibleButton.pcss @@ -63,22 +63,6 @@ Please see LICENSE files in the repository root for full details. font-weight: var(--cpd-font-weight-semibold); } - &.mx_AccessibleButton_kind_confirm_sm { - background-color: var(--cpd-color-bg-action-primary-rest); - - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/check.svg"); - } - } - - &.mx_AccessibleButton_kind_cancel_sm { - background-color: var(--cpd-color-bg-critical-primary); - - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); - } - } - &.mx_AccessibleButton_kind_icon, &.mx_AccessibleButton_kind_icon_primary, &.mx_AccessibleButton_kind_icon_primary_outline { @@ -114,13 +98,10 @@ Please see LICENSE files in the repository root for full details. text-decoration: underline; } - &.mx_AccessibleButton_kind_secondary_content { - color: $secondary-content; - } - &.mx_AccessibleButton_kind_danger { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-critical-primary); + border: 1px solid var(--cpd-color-bg-critical-primary); &.mx_AccessibleButton_disabled { color: var(--cpd-color-text-on-solid-primary); @@ -175,25 +156,4 @@ Please see LICENSE files in the repository root for full details. &.mx_AccessibleButton_kind_content_inline { display: inline; } - - &.mx_AccessibleButton_kind_confirm_sm, - &.mx_AccessibleButton_kind_cancel_sm { - padding: 0px; - width: 16px; - height: 16px; - border-radius: 100%; - position: relative; - display: block; - - &::before { - content: ""; - display: block; - position: absolute; - inset: 0; - background-color: #ffffff; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 80%; - } - } } diff --git a/res/css/views/elements/_DialPadBackspaceButton.pcss b/res/css/views/elements/_DialPadBackspaceButton.pcss index 4e305999ff..6325276901 100644 --- a/res/css/views/elements/_DialPadBackspaceButton.pcss +++ b/res/css/views/elements/_DialPadBackspaceButton.pcss @@ -7,26 +7,14 @@ Please see LICENSE files in the repository root for full details. */ .mx_DialPadBackspaceButton { - position: relative; - height: 28px; - width: 28px; + height: 20px; + width: 20px; + padding: var(--cpd-space-1x); - &::before { - /* force this element to appear on the DOM */ - content: ""; - - background-color: #8d97a5; + svg { + color: #8d97a5; width: inherit; height: inherit; - top: 0px; - left: 0px; - position: absolute; display: inline-block; - vertical-align: middle; - - mask-image: url("$(res)/img/element-icons/call/delete.svg"); - mask-position: 8px; - mask-size: 20px; - mask-repeat: no-repeat; } } diff --git a/res/css/views/elements/_FacePile.pcss b/res/css/views/elements/_FacePile.pcss index de37d5ba6d..791386cd4a 100644 --- a/res/css/views/elements/_FacePile.pcss +++ b/res/css/views/elements/_FacePile.pcss @@ -19,6 +19,8 @@ Please see LICENSE files in the repository root for full details. color: $tertiary-content; display: inline-block; z-index: 1; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; } .mx_FacePile_summary { diff --git a/res/css/views/elements/_Field.pcss b/res/css/views/elements/_Field.pcss index 90cb176251..dc9f287398 100644 --- a/res/css/views/elements/_Field.pcss +++ b/res/css/views/elements/_Field.pcss @@ -47,20 +47,14 @@ Please see LICENSE files in the repository root for full details. text-overflow: ellipsis; } -/* Can't add pseudo-elements to a select directly, so we use its parent. */ -.mx_Field_select::before { - content: ""; +.mx_Field_select_chevron { position: absolute; top: 50%; transform: translateY(-50%); right: 4px; width: 18px; height: 18px; - mask: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - background-color: $primary-content; + color: $primary-content; z-index: 1; pointer-events: none; } diff --git a/res/css/views/elements/_ImageView.pcss b/res/css/views/elements/_ImageView.pcss index d6649d15da..c025456157 100644 --- a/res/css/views/elements/_ImageView.pcss +++ b/res/css/views/elements/_ImageView.pcss @@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details. */ $button-size: 32px; -$icon-size: 22px; -$button-gap: 24px; +$button-gap: var(--cpd-space-3x); :root { --image-view-panel-height: 68px; @@ -82,6 +81,8 @@ $button-gap: 24px; } .mx_ImageView_toolbar { + --icon-size: 24px; + padding-right: 16px; pointer-events: initial; display: flex; @@ -89,60 +90,30 @@ $button-gap: 24px; flex-grow: 1; flex-basis: 0; justify-content: flex-end; - gap: calc($button-gap - ($button-size - $icon-size)); + gap: $button-gap; } .mx_ImageView_button { - padding: calc(($button-size - $icon-size) / 2); + padding: calc(($button-size - var(--icon-size)) / 2); + width: var(--icon-size); + height: var(--icon-size); display: block; - &::before { - content: ""; - height: $icon-size; - width: $icon-size; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; + svg { + height: inherit; + width: inherit; display: block; - background-color: $icon-button-color; + color: $icon-button-color; } } -.mx_ImageView_button_rotateCW::before { - mask-image: url("$(res)/img/image-view/rotate-cw.svg"); -} - -.mx_ImageView_button_rotateCCW::before { - mask-image: url("$(res)/img/image-view/rotate-ccw.svg"); -} - -.mx_ImageView_button_zoomOut::before { - mask-image: url("$(res)/img/image-view/zoom-out.svg"); -} - -.mx_ImageView_button_zoomIn::before { - mask-image: url("$(res)/img/image-view/zoom-in.svg"); -} - -.mx_ImageView_button_download::before { - mask-image: url("@vector-im/compound-design-tokens/icons/download.svg"); -} - -.mx_ImageView_button_more::before { - mask-image: url("@vector-im/compound-design-tokens/icons/overflow-horizontal.svg"); - mask-size: 28px; +.mx_ImageView_button_more { + --icon-size: 28px; } .mx_ImageView_button_close { - padding: calc($button-size - $button-size); border-radius: 100%; background: #21262c; /* same on all themes */ - &::before { - width: $button-size; - height: $button-size; - mask-image: url("@vector-im/compound-design-tokens/icons/close.svg"); - mask-size: 24px; - } } @media (prefers-reduced-motion) { diff --git a/res/css/views/elements/_Pill.pcss b/res/css/views/elements/_Pill.pcss index c466c4d6a3..446f4694b5 100644 --- a/res/css/views/elements/_Pill.pcss +++ b/res/css/views/elements/_Pill.pcss @@ -19,6 +19,8 @@ Please see LICENSE files in the repository root for full details. color: var(--cpd-color-text-on-solid-primary) !important; /* To override .markdown-body */ background-color: $pill-bg-color !important; /* To override .markdown-body */ + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; > * { pointer-events: none; diff --git a/res/css/views/elements/_ServerPicker.pcss b/res/css/views/elements/_ServerPicker.pcss index 5d04482334..0244673a35 100644 --- a/res/css/views/elements/_ServerPicker.pcss +++ b/res/css/views/elements/_ServerPicker.pcss @@ -23,28 +23,19 @@ Please see LICENSE files in the repository root for full details. } .mx_ServerPicker_help { - width: 20px; - height: 20px; + width: 24px; + height: 24px; border-radius: 10px; grid-column: 2; grid-row: 1; margin-left: auto; - text-align: center; - font-size: 16px; - position: relative; + margin-top: -2px; + margin-right: -2px; - &::before { - content: ""; - width: 24px; - height: 24px; - position: absolute; - top: -2px; - left: -2px; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - mask-image: url("@vector-im/compound-design-tokens/icons/info.svg"); - background: $icon-button-color; + svg { + width: inherit; + height: inherit; + color: $icon-button-color; } } diff --git a/res/css/views/elements/_StyledRadioButton.pcss b/res/css/views/elements/_StyledRadioButton.pcss index 13bcf309e7..90095ed21a 100644 --- a/res/css/views/elements/_StyledRadioButton.pcss +++ b/res/css/views/elements/_StyledRadioButton.pcss @@ -82,6 +82,8 @@ Please see LICENSE files in the repository root for full details. & > div { background: $active-radio-circle-color; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; } } } diff --git a/res/css/views/emojipicker/_EmojiPicker.pcss b/res/css/views/emojipicker/_EmojiPicker.pcss index bc14c2e958..60a364a6e3 100644 --- a/res/css/views/emojipicker/_EmojiPicker.pcss +++ b/res/css/views/emojipicker/_EmojiPicker.pcss @@ -31,17 +31,27 @@ Please see LICENSE files in the repository root for full details. .mx_EmojiPicker_anchor { border: none; - padding: 8px 8px 6px; + padding: var(--cpd-space-1x) 0; + /* We have to explicitly inherit the font as button browser styles are implicitly ignorant */ + font: inherit; + font-size: $font-20px; + line-height: 1; border-bottom: 2px solid transparent; background-color: transparent; border-radius: 4px 4px 0 0; width: 36px; - height: 38px; + height: 36px; + + color: $primary-content; + display: inline-block; &:not(:disabled) { cursor: pointer; } + &:disabled { + filter: opacity(0.3); + } &:not(:disabled):hover { background-color: $focus-bg-color; @@ -49,51 +59,6 @@ Please see LICENSE files in the repository root for full details. } } -.mx_EmojiPicker_anchor::before { - background-color: $primary-content; - content: ""; - display: inline-block; - mask-size: 100%; - mask-repeat: no-repeat; - width: 100%; - height: 100%; -} - -.mx_EmojiPicker_anchor:disabled::before { - background-color: $focus-bg-color; -} - -.mx_EmojiPicker_anchor_activity::before { - mask-image: url("$(res)/img/emojipicker/activity.svg"); -} -.mx_EmojiPicker_anchor_custom::before { - mask-image: url("$(res)/img/emojipicker/custom.svg"); -} -.mx_EmojiPicker_anchor_flags::before { - mask-image: url("$(res)/img/emojipicker/flags.svg"); -} -.mx_EmojiPicker_anchor_foods::before { - mask-image: url("$(res)/img/emojipicker/foods.svg"); -} -.mx_EmojiPicker_anchor_nature::before { - mask-image: url("$(res)/img/emojipicker/nature.svg"); -} -.mx_EmojiPicker_anchor_objects::before { - mask-image: url("$(res)/img/emojipicker/objects.svg"); -} -.mx_EmojiPicker_anchor_people::before { - mask-image: url("$(res)/img/emojipicker/people.svg"); -} -.mx_EmojiPicker_anchor_places::before { - mask-image: url("$(res)/img/emojipicker/places.svg"); -} -.mx_EmojiPicker_anchor_recent::before { - mask-image: url("$(res)/img/emojipicker/recent.svg"); -} -.mx_EmojiPicker_anchor_symbols::before { - mask-image: url("$(res)/img/emojipicker/symbols.svg"); -} - .mx_EmojiPicker_anchor_visible { border-bottom: 2px solid $accent; } @@ -124,34 +89,24 @@ Please see LICENSE files in the repository root for full details. align-self: center; width: 32px; height: 32px; + cursor: pointer; + + svg { + width: 100%; + height: 100%; + } } } -.mx_EmojiPicker_search_clear { - cursor: pointer; -} - .mx_EmojiPicker_search_icon { - width: 16px; + width: 18px; margin: 8px; -} -.mx_EmojiPicker_search_icon:not(.mx_EmojiPicker_search_clear) { - pointer-events: none; -} - -.mx_EmojiPicker_search_icon::after { - mask: url("$(res)/img/emojipicker/search.svg") no-repeat; - mask-size: 100%; - background-color: $primary-content; - content: ""; - display: inline-block; - width: 100%; - height: 100%; -} - -.mx_EmojiPicker_search_clear::after { - mask-image: url("$(res)/img/emojipicker/delete.svg"); + svg { + width: 100%; + height: 100%; + color: $primary-content; + } } .mx_EmojiPicker_category { diff --git a/res/css/views/messages/_CallEvent.pcss b/res/css/views/messages/_CallEvent.pcss index dd260189d8..1733fd8638 100644 --- a/res/css/views/messages/_CallEvent.pcss +++ b/res/css/views/messages/_CallEvent.pcss @@ -46,13 +46,10 @@ Please see LICENSE files in the repository root for full details. line-height: 24px; /* in px to match the avatar */ } -.mx_CallEvent_inactive .mx_CallEvent_title::before { +.mx_CallEvent_inactive .mx_CallEvent_title svg { display: inline-block; vertical-align: middle; - content: ""; - background-color: $secondary-content; - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg"); - mask-size: 16px; + color: $secondary-content; width: 16px; height: 16px; margin-right: $spacing-8; diff --git a/res/css/views/messages/_CreateEvent.pcss b/res/css/views/messages/_CreateEvent.pcss index 2f4d753436..710ca01be1 100644 --- a/res/css/views/messages/_CreateEvent.pcss +++ b/res/css/views/messages/_CreateEvent.pcss @@ -9,8 +9,7 @@ Please see LICENSE files in the repository root for full details. .mx_EventTileBubble.mx_CreateEvent { margin: var(--EventTileBubble_margin-block) auto; - &::before { - background-color: $header-panel-text-primary-color; - mask-image: url("@vector-im/compound-design-tokens/icons/chat-solid.svg"); + svg { + color: $header-panel-text-primary-color; } } diff --git a/res/css/views/messages/_DecryptionFailureBody.pcss b/res/css/views/messages/_DecryptionFailureBody.pcss deleted file mode 100644 index 4a4940abe3..0000000000 --- a/res/css/views/messages/_DecryptionFailureBody.pcss +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_DecryptionFailureBody { - color: $secondary-content; - font-style: italic; -} - -/* Formatting for errors due to sender trust requirement failures */ -.mx_DecryptionFailureSenderTrustRequirement > span { - /* some space between the (/) icon and text */ - display: inline-flex; - gap: var(--cpd-space-1x); - - /* Center vertically */ - align-items: center; -} diff --git a/res/css/views/messages/_EventTileBubble.pcss b/res/css/views/messages/_EventTileBubble.pcss deleted file mode 100644 index 4b47c8b78e..0000000000 --- a/res/css/views/messages/_EventTileBubble.pcss +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_EventTileBubble { - --EventTileBubble_margin-block: 10px; - - background-color: $dark-panel-bg-color; - padding: 10px; - border-radius: 8px; - /* Reserve space for external timestamps, but also cap the width */ - max-width: min(calc(100% - 2 * var(--MessageTimestamp-width)), 600px); - box-sizing: border-box; - display: grid; - grid-template-columns: 24px minmax(0, 1fr) min-content min-content; - - &::before, - &::after { - position: relative; - grid-column: 1; - grid-row: 1 / 3; - width: 16px; - height: 16px; - content: ""; - inset: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - margin-top: $spacing-4; - } - - .mx_EventTileBubble_title, - .mx_EventTileBubble_subtitle { - grid-column: 2; - overflow-wrap: break-word; - min-inline-size: 50px; - } - - .mx_EventTileBubble_title { - font-weight: var(--cpd-font-weight-semibold); - font-size: $font-15px; - grid-row: 1; - } - - .mx_EventTileBubble_subtitle { - font-size: $font-12px; - grid-row: 2; - } - - .mx_MessageTimestamp { - grid-column: 4; - grid-row: 1 / 3; - align-self: center; - margin-left: $spacing-16; - } -} diff --git a/res/css/views/messages/_HiddenBody.pcss b/res/css/views/messages/_HiddenBody.pcss index 30e3100dbb..3644db16e6 100644 --- a/res/css/views/messages/_HiddenBody.pcss +++ b/res/css/views/messages/_HiddenBody.pcss @@ -11,21 +11,12 @@ Please see LICENSE files in the repository root for full details. color: $muted-fg-color; vertical-align: middle; - padding-left: 20px; - position: relative; - - &::before { + svg { height: 14px; width: 14px; - background-color: $muted-fg-color; - mask-image: url("@vector-im/compound-design-tokens/icons/visibility-off.svg"); - - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - content: ""; - position: absolute; - top: 1px; - left: 0; + display: inline-block; + margin-right: var(--cpd-space-1-5x); + color: $muted-fg-color; + vertical-align: -2px; } } diff --git a/res/css/views/messages/_LegacyCallEvent.pcss b/res/css/views/messages/_LegacyCallEvent.pcss index 449268c678..16905485b6 100644 --- a/res/css/views/messages/_LegacyCallEvent.pcss +++ b/res/css/views/messages/_LegacyCallEvent.pcss @@ -30,67 +30,10 @@ Please see LICENSE files in the repository root for full details. .mx_LegacyCallEvent_iconButton { display: inline-flex; - &::before { - content: ""; - + svg { height: 16px; width: 16px; - background-color: $secondary-content; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } - } - - .mx_LegacyCallEvent_silence::before { - mask-image: url("@vector-im/compound-design-tokens/icons/volume-on-solid.svg"); - } - - .mx_LegacyCallEvent_unSilence::before { - mask-image: url("@vector-im/compound-design-tokens/icons/volume-off-solid.svg"); - } - - &.mx_LegacyCallEvent_voice { - .mx_LegacyCallEvent_type_icon::before, - .mx_LegacyCallEvent_content_button_callBack span::before, - .mx_LegacyCallEvent_content_button_answer span::before { - mask-image: url("@vector-im/compound-design-tokens/icons/voice-call-solid.svg"); - } - - &.mx_LegacyCallEvent_rejected, - &.mx_LegacyCallEvent_noAnswer { - .mx_LegacyCallEvent_type_icon::before { - mask-image: url("@vector-im/compound-design-tokens/icons/end-call.svg"); - } - } - } - - &.mx_LegacyCallEvent_video { - .mx_LegacyCallEvent_type_icon::before, - .mx_LegacyCallEvent_content_button_callBack span::before, - .mx_LegacyCallEvent_content_button_answer span::before { - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg"); - } - - &.mx_LegacyCallEvent_rejected, - &.mx_LegacyCallEvent_noAnswer { - .mx_LegacyCallEvent_type_icon::before { - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-declined-solid.svg"); - } - } - } - - &.mx_LegacyCallEvent_missed { - &.mx_LegacyCallEvent_voice { - .mx_LegacyCallEvent_type_icon::before { - mask-image: url("$(res)/img/voip/missed-voice.svg"); - } - } - - &.mx_LegacyCallEvent_video { - .mx_LegacyCallEvent_type_icon::before { - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-missed-solid.svg"); - } + color: $secondary-content; } } @@ -119,26 +62,8 @@ Please see LICENSE files in the repository root for full details. } .mx_LegacyCallEvent_type { - display: flex; - align-items: center; font-weight: 400; color: $secondary-content; - - .mx_LegacyCallEvent_type_icon { - height: 13px; - width: 13px; - margin-right: 5px; - - &::before { - content: ""; - position: absolute; - height: 13px; - width: 13px; - background-color: $secondary-content; - mask-repeat: no-repeat; - mask-size: contain; - } - } } } } @@ -158,19 +83,6 @@ Please see LICENSE files in the repository root for full details. @mixin LegacyCallButton; padding: 0 $spacing-12; font-size: inherit; - - span::before { - mask-size: 16px; - width: 16px; - height: 16px; - flex-shrink: 0; - } - } - - .mx_LegacyCallEvent_content_button_reject { - span::before { - mask-image: url("@vector-im/compound-design-tokens/icons/end-call.svg"); - } } .mx_LegacyCallEvent_content_tooltip { @@ -202,6 +114,21 @@ Please see LICENSE files in the repository root for full details. } } +.mx_LegacyCallEvent_type_icon { + height: 16px; + width: 16px; + margin-right: 6px; + display: inline-block; + vertical-align: -2px; + + svg { + position: absolute; + height: inherit; + width: inherit; + color: $tertiary-content; + } +} + .mx_EventTile[data-layout="bubble"] { .mx_EventTile_e2eIcon + .mx_LegacyCallEvent_wrapper { .mx_LegacyCallEvent { diff --git a/res/css/views/messages/_MFileBody.pcss b/res/css/views/messages/_MFileBody.pcss index 51ba681140..6293270c68 100644 --- a/res/css/views/messages/_MFileBody.pcss +++ b/res/css/views/messages/_MFileBody.pcss @@ -34,25 +34,17 @@ Please see LICENSE files in the repository root for full details. background-color: $system; border-radius: 20px; display: inline-block; - width: 32px; - height: 32px; - position: relative; + width: 16px; + height: 16px; + padding: var(--cpd-space-2x); vertical-align: middle; margin-right: 12px; - &::before { - content: ""; - mask-repeat: no-repeat; - mask-position: center; - mask-size: cover; - mask-image: url("@vector-im/compound-design-tokens/icons/attachment.svg"); - background-color: $secondary-content; - width: 16px; - height: 16px; - - position: absolute; - top: 8px; - left: 8px; + svg { + color: $secondary-content; + width: inherit; + height: inherit; + display: block; } } diff --git a/res/css/views/messages/_MJitsiWidgetEvent.pcss b/res/css/views/messages/_MJitsiWidgetEvent.pcss index 9884d8951f..7813354366 100644 --- a/res/css/views/messages/_MJitsiWidgetEvent.pcss +++ b/res/css/views/messages/_MJitsiWidgetEvent.pcss @@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details. */ .mx_EventTileBubble.mx_MJitsiWidgetEvent { - &::before { - background-color: $header-panel-text-primary-color; /* XXX: Variable abuse */ - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg"); + svg { + color: $header-panel-text-primary-color; /* XXX: Variable abuse */ } } diff --git a/res/css/views/messages/_ReactionsRow.pcss b/res/css/views/messages/_ReactionsRow.pcss index 833dee9a35..0c16480bd5 100644 --- a/res/css/views/messages/_ReactionsRow.pcss +++ b/res/css/views/messages/_ReactionsRow.pcss @@ -9,25 +9,19 @@ Please see LICENSE files in the repository root for full details. color: var(--cpd-color-text-primary); .mx_ReactionsRow_addReactionButton { - position: relative; display: inline-block; visibility: hidden; /* show on hover of the .mx_EventTile */ - width: 24px; - height: 24px; + width: 16px; + height: 16px; + padding: var(--cpd-space-1x); vertical-align: middle; margin-left: 4px; margin-right: 4px; - &::before { - content: ""; - position: absolute; - height: 100%; - width: 100%; - mask-size: 16px; - mask-repeat: no-repeat; - mask-position: center; - background-color: $tertiary-content; - mask-image: url("@vector-im/compound-design-tokens/icons/reaction-add.svg"); + svg { + height: inherit; + width: inherit; + color: $tertiary-content; } &.mx_ReactionsRow_addReactionButton_active { @@ -36,8 +30,8 @@ Please see LICENSE files in the repository root for full details. &:hover, &.mx_ReactionsRow_addReactionButton_active { - &::before { - background-color: $primary-content; + svg { + color: $primary-content; } } } diff --git a/res/css/views/messages/_RedactedBody.pcss b/res/css/views/messages/_RedactedBody.pcss index 7b114b1866..417048c7f5 100644 --- a/res/css/views/messages/_RedactedBody.pcss +++ b/res/css/views/messages/_RedactedBody.pcss @@ -11,20 +11,11 @@ Please see LICENSE files in the repository root for full details. color: $secondary-content; vertical-align: middle; - padding-left: 20px; - position: relative; - - &::before { + svg { + margin-right: 6px; height: 14px; width: 14px; - background-color: $icon-button-color; - mask-image: url("@vector-im/compound-design-tokens/icons/delete.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - content: ""; - position: absolute; - top: 1px; - left: 0; + color: $icon-button-color; + vertical-align: -2px; } } diff --git a/res/css/views/messages/_TimelineSeparator.pcss b/res/css/views/messages/_TimelineSeparator.pcss deleted file mode 100644 index aab77d4e03..0000000000 --- a/res/css/views/messages/_TimelineSeparator.pcss +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2017 Vector Creations Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_TimelineSeparator { - clear: both; - margin: 4px 0; - display: flex; - align-items: center; - font: var(--cpd-font-body-md-regular); - color: var(--cpd-color-text-primary); -} - -.mx_TimelineSeparator > hr { - flex: 1 1 0; - height: 0; - border: none; - border-bottom: 1px solid var(--cpd-color-gray-400); -} diff --git a/res/css/views/messages/_common_CryptoEvent.pcss b/res/css/views/messages/_common_CryptoEvent.pcss index 5f2e26fd1c..affbd26d64 100644 --- a/res/css/views/messages/_common_CryptoEvent.pcss +++ b/res/css/views/messages/_common_CryptoEvent.pcss @@ -9,28 +9,8 @@ Please see LICENSE files in the repository root for full details. .mx_EventTileBubble.mx_cryptoEvent { margin: var(--EventTileBubble_margin-block) auto; - /* white infill for the transparency */ - &.mx_cryptoEvent_icon::before { - background-color: #ffffff; - mask-image: url("@vector-im/compound-design-tokens/icons/lock-solid.svg"); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 80%; - } - - &.mx_cryptoEvent_icon::after { - mask-image: url("@vector-im/compound-design-tokens/icons/lock-solid.svg"); - background-color: $header-panel-text-primary-color; - } - - &.mx_cryptoEvent_icon_verified::after { - mask-image: url("@vector-im/compound-design-tokens/icons/shield.svg"); - background-color: $accent; - } - - &.mx_cryptoEvent_icon_warning::after { - mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); - background-color: $e2e-warning-color; + &.mx_cryptoEvent_icon svg { + color: $header-panel-text-primary-color; } .mx_cryptoEvent_state, diff --git a/res/css/views/right_panel/_BaseCard.pcss b/res/css/views/right_panel/_BaseCard.pcss index 035485428a..a5864c4e2a 100644 --- a/res/css/views/right_panel/_BaseCard.pcss +++ b/res/css/views/right_panel/_BaseCard.pcss @@ -61,24 +61,19 @@ Please see LICENSE files in the repository root for full details. .mx_BaseCard_header_title_button--option { position: relative; - width: var(--BaseCard_header-button-size); - height: var(--BaseCard_header-button-size); + width: calc(var(--BaseCard_header-button-size) - 4px); + height: calc(var(--BaseCard_header-button-size) - 4px); + padding: 2px; - &::after { - content: ""; - position: absolute; - inset-block-start: 0; - inset-inline-start: 0; - height: 100%; - width: 100%; - mask-repeat: no-repeat; - mask-position: center; - mask-image: url("@vector-im/compound-design-tokens/icons/overflow-horizontal.svg"); - background-color: $secondary-content; + svg { + width: inherit; + height: inherit; + display: block; + color: $secondary-content; } - &:hover::after { - background-color: $primary-content; + &:hover svg { + color: $primary-content; } } } diff --git a/res/css/views/right_panel/_ExtensionsCard.pcss b/res/css/views/right_panel/_ExtensionsCard.pcss index 10a9cb2d56..cda7541bf4 100644 --- a/res/css/views/right_panel/_ExtensionsCard.pcss +++ b/res/css/views/right_panel/_ExtensionsCard.pcss @@ -58,7 +58,13 @@ Please see LICENSE files in the repository root for full details. width: 24px; padding: var(--cpd-space-3x) var(--cpd-space-1x); box-sizing: border-box; - min-width: 24px; /* prevent flexbox crushing */ + flex-shrink: 0; + + svg { + width: 16px; + height: 16px; + color: $icon-button-color; + } &:hover { &::after { @@ -72,32 +78,14 @@ Please see LICENSE files in the repository root for full details. background-color: rgb(141, 151, 165, 0.1); } } - - &::before { - content: ""; - position: absolute; - height: 16px; - width: 16px; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 16px; - background-color: $icon-button-color; - } } .mx_ExtensionsCard_app_pinToggle { right: 8px; - - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/pin-solid.svg"); - } } .mx_ExtensionsCard_app_options { right: 32px; /* 24 + 8 */ - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/overflow-horizontal.svg"); - } } &.mx_ExtensionsCard_Button_pinned { @@ -105,8 +93,8 @@ Please see LICENSE files in the repository root for full details. opacity: 0.2; } - .mx_ExtensionsCard_app_pinToggle::before { - background-color: $accent; + .mx_ExtensionsCard_app_pinToggle svg { + color: $accent; } } diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index a5a6540dda..66536a7372 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -10,11 +10,6 @@ Please see LICENSE files in the repository root for full details. height: 100px; overflow: visible; - /* Unset flex on the thread list, but not the thread view */ - &:not(.mx_ThreadView) .mx_BaseCard_header .mx_BaseCard_header_title { - flex: unset; - } - .mx_ThreadPanelHeader { height: 60px; display: flex; diff --git a/res/css/views/right_panel/_TimelineCard.pcss b/res/css/views/right_panel/_TimelineCard.pcss index cbd373c7f5..14d33d4f99 100644 --- a/res/css/views/right_panel/_TimelineCard.pcss +++ b/res/css/views/right_panel/_TimelineCard.pcss @@ -43,6 +43,7 @@ Please see LICENSE files in the repository root for full details. &[data-layout="irc"], &[data-layout="group"] { --TimelineCard_ReadReceiptGroup-inset-block-start: -6px; + --EventTile_group_line-spacing-inline-start: var(--BaseCard_EventTile-spacing-inline); &.mx_EventTile_info .mx_EventTile_line, .mx_EventTile_line { @@ -77,7 +78,6 @@ Please see LICENSE files in the repository root for full details. } .mx_DisambiguatedProfile, - .mx_ReactionsRow, .mx_ThreadSummary { margin-inline-start: var(--BaseCard_EventTile-spacing-inline); } diff --git a/res/css/views/right_panel/_VerificationPanel.pcss b/res/css/views/right_panel/_VerificationPanel.pcss index 40b6de3266..37da9af79f 100644 --- a/res/css/views/right_panel/_VerificationPanel.pcss +++ b/res/css/views/right_panel/_VerificationPanel.pcss @@ -39,14 +39,6 @@ Please see LICENSE files in the repository root for full details. } } - .mx_EncryptionPanel_cancel { - @mixin customisedCancelButton; - position: absolute; - z-index: 100; - top: 14px; - right: 14px; - } - .mx_VerificationPanel_qrCode { padding: 4px 4px 0 4px; background: white; diff --git a/res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss b/res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss deleted file mode 100644 index a0fbfdaea7..0000000000 --- a/res/css/views/rooms/RoomListPanel/_EmptyRoomList.pcss +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_EmptyRoomList_GenericPlaceholder { - align-self: center; - /** It should take 2/3 of the width **/ - width: 66%; - /** It should be positioned at 1/3 of the height **/ - padding-top: 33%; - - .mx_EmptyRoomList_GenericPlaceholder_title { - font: var(--cpd-font-body-lg-semibold); - text-align: center; - } - - .mx_EmptyRoomList_GenericPlaceholder_description { - font: var(--cpd-font-body-sm-regular); - color: var(--cpd-color-text-secondary); - text-align: center; - } - - .mx_EmptyRoomList_DefaultPlaceholder { - margin-top: var(--cpd-space-4x); - } - - button { - width: 100%; - } -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss deleted file mode 100644 index 5427e1f133..0000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomListHeaderView.pcss +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_RoomListHeaderView { - flex: 0 0 60px; - padding: 0 var(--cpd-space-3x); - - .mx_RoomListHeaderView_title { - min-width: 0; - - h1 { - all: unset; - font: var(--cpd-font-heading-sm-semibold); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - } - - .mx_SpaceMenu_button { - svg { - transition: transform 0.1s linear; - } - } - - .mx_SpaceMenu_button[aria-expanded="true"] { - svg { - transform: rotate(180deg); - } - } - - .mx_RoomListHeaderView_ReleaseAnnouncementAnchor { - display: inline-flex; - } -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss deleted file mode 100644 index 7ce6280331..0000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -/** - * The RoomListItemView has the following structure: - * button--------------------------------------------------| - * | <-12px-> container------------------------------------| - * | | room avatar <-8px-> content----------------| - * | | | room_name <- 20px ->| - * | | | --------------------| <-- border - * |-------------------------------------------------------| - */ -.mx_RoomListItemView { - /* Remove button default style */ - background: unset; - border: none; - padding: 0; - text-align: unset; - - cursor: pointer; - height: 48px; - width: 100%; - - padding-left: var(--cpd-space-3x); - font: var(--cpd-font-body-md-regular); - - /* Hide the menu by default */ - .mx_RoomListItemView_menu { - display: none; - } - - &:hover, - &:focus-visible, - /* When the context menu is opened */ - &[data-state="open"], - /* When the options and notifications menu are opened */ - &:has(.mx_RoomListItemMenuView > button[data-state="open"]) { - background-color: var(--cpd-color-bg-action-secondary-hovered); - - .mx_RoomListItemView_menu { - display: flex; - } - - &.mx_RoomListItemView_has_menu { - /** - * The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331 - * the icon size of the menu is 18px instead of 20px with a different internal padding - * We need to use 18px to align the icon with the others icons - * 18px is not available in compound spacing - */ - .mx_RoomListItemView_content { - padding-right: 18px; - } - - /* When the menu is visible, hide the notification decoration to avoid clutter */ - .mx_RoomListItemView_notificationDecoration { - display: none; - } - } - } - - .mx_RoomListItemView_content { - height: 100%; - flex: 1; - /* The border is only under the room name and the future hover menu */ - border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary); - box-sizing: border-box; - min-width: 0; - padding-right: var(--cpd-space-5x); - - .mx_RoomListItemView_text { - min-width: 0; - } - - .mx_RoomListItemView_roomName { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .mx_RoomListItemView_messagePreview { - font: var(--cpd-font-body-sm-regular); - color: var(--cpd-color-text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } -} - -.mx_RoomListItemView_selected { - background-color: var(--cpd-color-bg-action-secondary-pressed); -} - -.mx_RoomListItemView_bold .mx_RoomListItemView_roomName { - font: var(--cpd-font-body-md-semibold); -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss b/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss deleted file mode 100644 index 378f2e75da..0000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_RoomListPrimaryFilters { - padding: var(--cpd-space-2x) var(--cpd-space-4x) var(--cpd-space-2x) var(--cpd-space-3x); - - .mx_RoomListPrimaryFilters_wrapping { - display: none; - } - - .mx_RoomListPrimaryFilters_list { - /** - * The InteractionObserver needs the height to be set to work properly. - */ - height: 100%; - flex: 1; - } - - .mx_RoomListPrimaryFilters_IconButton { - svg { - transition: transform 0.1s linear; - } - } - - .mx_RoomListPrimaryFilters_IconButton[aria-expanded="true"] { - svg { - transform: rotate(180deg); - } - } -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss b/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss deleted file mode 100644 index 2e644cbba1..0000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomListSkeleton.pcss +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_RoomListSkeleton { - position: relative; - margin-left: 4px; - height: 100%; - - &::before { - background-color: var(--cpd-color-bg-subtle-secondary); - width: 100%; - height: 100%; - - content: ""; - position: absolute; - mask-repeat: repeat-y; - mask-size: auto 96px; - mask-image: url("$(res)/img/element-icons/roomlist/room-list-item-skeleton.svg"); - } -} diff --git a/res/css/views/rooms/_E2EIcon.pcss b/res/css/views/rooms/_E2EIcon.pcss index efa8b1faff..55e3e02748 100644 --- a/res/css/views/rooms/_E2EIcon.pcss +++ b/res/css/views/rooms/_E2EIcon.pcss @@ -21,22 +21,3 @@ Please see LICENSE files in the repository root for full details. .mx_E2EIcon.mx_E2EIcon_inline { display: inline-block; } - -.mx_E2EIcon_warning { - color: $e2e-warning-color; -} - -.mx_E2EIcon_normal { - color: var(--cpd-color-icon-tertiary); -} - -.mx_E2EIcon_verified, -.mx_E2EIcon_warning { - .mx_E2EIcon_normal::after { - background-color: white; - } -} - -.mx_E2EIcon_verified { - color: $e2e-verified-color; -} diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 8b9a8f46e1..cc532079b6 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -38,34 +38,11 @@ $left-gutter: 64px; text-align: start; } - .mx_EventTile_receiptSent, - .mx_EventTile_receiptSending { - position: relative; + .mx_ReadReceiptGroup_container svg { display: inline-block; width: 16px; height: 16px; - - &::before { - background-color: var(--cpd-color-icon-tertiary); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 16px; - width: 16px; - height: 16px; - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - } - } - - .mx_EventTile_receiptSent::before { - mask-image: url("@vector-im/compound-design-tokens/icons/check-circle.svg"); - } - - .mx_EventTile_receiptSending::before { - mask-image: url("@vector-im/compound-design-tokens/icons/circle.svg"); + color: var(--cpd-color-icon-tertiary); } .mx_EventTile_content { @@ -498,7 +475,7 @@ $left-gutter: 64px; } .mx_EventTile_footer { - margin: var(--cpd-space-1x) var(--cpd-space-16x); + margin: var(--cpd-space-1x) var(--EventTile_group_line-spacing-inline-start); } > .mx_DisambiguatedProfile { @@ -759,6 +736,12 @@ $left-gutter: 64px; } */ + /* Support arbitrarily large marker numbers */ + ol { + list-style-position: inside; + padding-inline-start: 0.5em; + } + /* Override nested lists being lower-roman */ ol ol, ul ol { @@ -830,32 +813,11 @@ $left-gutter: 64px; width: 14px; height: 14px; display: block; - background-repeat: no-repeat; - background-size: contain; - &::after { - content: ""; + svg { + height: inherit; + width: inherit; display: block; - position: absolute; - inset: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - } - - &.mx_EventTile_e2eIcon_warning::after { - mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); - background-color: $e2e-warning-color; /* red */ - } - - &.mx_EventTile_e2eIcon_normal::after { - mask-image: url("@vector-im/compound-design-tokens/icons/info.svg"); - background-color: var(--cpd-color-icon-tertiary); /* grey */ - } - - &.mx_EventTile_e2eIcon_decryption_failure::after { - mask-image: url("@vector-im/compound-design-tokens/icons/error-solid.svg"); - background-color: var(--cpd-color-icon-tertiary); } } @@ -1103,7 +1065,7 @@ $left-gutter: 64px; position: relative; font: var(--cpd-font-body-sm-regular); - &::before { + > svg { @mixin ThreadSummaryIcon; } diff --git a/res/css/views/rooms/_HistoryTile.pcss b/res/css/views/rooms/_HistoryTile.pcss index 53b1ca2765..adfa20a43a 100644 --- a/res/css/views/rooms/_HistoryTile.pcss +++ b/res/css/views/rooms/_HistoryTile.pcss @@ -9,8 +9,7 @@ Please see LICENSE files in the repository root for full details. .mx_EventTileBubble.mx_HistoryTile { margin: var(--EventTileBubble_margin-block) auto; - &::before { - background-color: $header-panel-text-primary-color; - mask-image: url("@vector-im/compound-design-tokens/icons/visibility-off.svg"); + svg { + color: $header-panel-text-primary-color; } } diff --git a/res/css/views/rooms/_LegacyRoomListHeader.pcss b/res/css/views/rooms/_LegacyRoomListHeader.pcss index fd6d7b3b29..ec93c4e746 100644 --- a/res/css/views/rooms/_LegacyRoomListHeader.pcss +++ b/res/css/views/rooms/_LegacyRoomListHeader.pcss @@ -31,26 +31,17 @@ Please see LICENSE files in the repository root for full details. background-color: $quinary-content; } - &::before { - content: ""; + svg { width: 20px; height: 20px; top: 3px; right: 0; position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background-color: $tertiary-content; - mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); + color: $tertiary-content; } &[aria-expanded="true"] { background-color: $quinary-content; - - &::before { - transform: rotate(180deg); - } } } @@ -66,23 +57,18 @@ Please see LICENSE files in the repository root for full details. box-sizing: border-box; flex-shrink: 0; - &::before { - content: ""; + svg { width: 16px; height: 16px; position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background-color: $secondary-content; - mask-image: url("@vector-im/compound-design-tokens/icons/plus.svg"); + color: $secondary-content; } &:hover { background-color: $tertiary-content; - &::before { - background-color: $background; + svg { + color: $background; } } } diff --git a/res/css/views/rooms/_LiveContentSummary.pcss b/res/css/views/rooms/_LiveContentSummary.pcss index 04704c6a1a..be0c9d14fe 100644 --- a/res/css/views/rooms/_LiveContentSummary.pcss +++ b/res/css/views/rooms/_LiveContentSummary.pcss @@ -10,41 +10,28 @@ Please see LICENSE files in the repository root for full details. color: $secondary-content; .mx_LiveContentSummary_text { - &::before { + svg { display: inline-block; vertical-align: text-bottom; - content: ""; - background-color: $secondary-content; - mask-size: 16px; + color: $secondary-content; width: 16px; height: 16px; margin-right: 4px; } - &.mx_LiveContentSummary_text_video::before { - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg"); - } - - &.mx_LiveContentSummary_text_voice::before { - mask-image: url("@vector-im/compound-design-tokens/icons/voice-call-solid.svg"); - } - &.mx_LiveContentSummary_text_active { color: $accent; - &::before { - background-color: $accent; + svg { + color: $accent; } } } - .mx_LiveContentSummary_participants::before { + .mx_LiveContentSummary_participants svg { display: inline-block; - vertical-align: text-bottom; - content: ""; - background-color: $secondary-content; - mask-image: url("@vector-im/compound-design-tokens/icons/group.svg"); - mask-size: 16px; + vertical-align: middle; + color: $secondary-content; width: 16px; height: 16px; margin-right: 2px; diff --git a/res/css/views/rooms/_NewRoomIntro.pcss b/res/css/views/rooms/_NewRoomIntro.pcss index 9f18e790fd..bdd6244f42 100644 --- a/res/css/views/rooms/_NewRoomIntro.pcss +++ b/res/css/views/rooms/_NewRoomIntro.pcss @@ -25,23 +25,15 @@ Please see LICENSE files in the repository root for full details. line-height: $font-24px; display: inline-block; - &:not(.mx_AccessibleButton_kind_primary_outline)::before { - content: ""; + svg { display: inline-block; - background-color: $button-fg-color; - mask-position: center; - mask-repeat: no-repeat; - mask-size: 20px; + color: $button-fg-color; width: 20px; height: 20px; margin-right: 5px; vertical-align: text-bottom; } } - - .mx_NewRoomIntro_inviteButton::before { - mask-image: url("@vector-im/compound-design-tokens/icons/user-add.svg"); - } } > h2 { diff --git a/res/css/views/rooms/_NotificationBadge.pcss b/res/css/views/rooms/_NotificationBadge.pcss index 5f76d82bfa..d867f6f501 100644 --- a/res/css/views/rooms/_NotificationBadge.pcss +++ b/res/css/views/rooms/_NotificationBadge.pcss @@ -19,6 +19,8 @@ Please see LICENSE files in the repository root for full details. &.mx_NotificationBadge_visible { background-color: $roomtile-default-badge-bg-color; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; /* Create a flexbox to order the count a bit easier */ display: flex; @@ -55,8 +57,7 @@ Please see LICENSE files in the repository root for full details. background-color: var(--cpd-color-icon-critical-primary); } - &.mx_NotificationBadge_knocked { - mask-image: url("@vector-im/compound-design-tokens/icons/ask-to-join.svg"); + & > svg { width: 16px; height: 16px; } diff --git a/res/css/views/rooms/_PinnedMessageBanner.pcss b/res/css/views/rooms/_PinnedMessageBanner.pcss index d7f3ddd1fb..2b36559020 100644 --- a/res/css/views/rooms/_PinnedMessageBanner.pcss +++ b/res/css/views/rooms/_PinnedMessageBanner.pcss @@ -22,6 +22,7 @@ box-shadow: 0 var(--cpd-space-2x) var(--cpd-space-6x) calc(var(--cpd-space-2x) * -1) rgb(27, 29, 34, 0.1); .mx_PinnedMessageBanner_main { + color: inherit; background: transparent; border: none; text-align: start; diff --git a/res/css/views/rooms/_ReplyTile.pcss b/res/css/views/rooms/_ReplyTile.pcss index 05665887d3..64492762ad 100644 --- a/res/css/views/rooms/_ReplyTile.pcss +++ b/res/css/views/rooms/_ReplyTile.pcss @@ -11,14 +11,6 @@ Please see LICENSE files in the repository root for full details. padding: 2px 0; font: var(--cpd-font-body-md-regular); - &.mx_ReplyTile_audio .mx_MFileBody_info_icon::before { - mask-image: url("@vector-im/compound-design-tokens/icons/volume-on-solid.svg"); - } - - &.mx_ReplyTile_video .mx_MFileBody_info_icon::before { - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg"); - } - > a { display: grid; grid-template: diff --git a/res/css/views/rooms/_RoomCallBanner.pcss b/res/css/views/rooms/_RoomCallBanner.pcss index f4fd14c2fa..8e49f6c126 100644 --- a/res/css/views/rooms/_RoomCallBanner.pcss +++ b/res/css/views/rooms/_RoomCallBanner.pcss @@ -31,16 +31,12 @@ Please see LICENSE files in the repository root for full details. font-weight: var(--cpd-font-weight-semibold); padding-right: $spacing-8; - &::before { + svg { display: inline-block; vertical-align: middle; - content: ""; - background-color: $secondary-content; - mask-size: 16px; - mask-position: center; + color: $secondary-content; width: 16px; - height: 1.2em; /* to match line height */ + height: 16px; margin-right: 8px; - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg"); } } diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index 0e2b40e0bd..a42811fae2 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -13,6 +13,11 @@ Please see LICENSE files in the repository root for full details. border-bottom: 1px solid $separator; background-color: $background; transition: all 0.2s ease; + + button:has(svg.mx_RoomHeader_toggled) { + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; + } } .mx_RoomHeader:hover { @@ -21,6 +26,7 @@ Please see LICENSE files in the repository root for full details. .mx_RoomHeader_infoWrapper { /* unset button styles */ + color: inherit; background: unset; border: unset; flex: 1; @@ -90,6 +96,8 @@ Please see LICENSE files in the repository root for full details. background: var(--cpd-color-bg-success-subtle); color: var(--cpd-color-text-action-accent); font: var(--cpd-font-body-sm-semibold); + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; } } diff --git a/res/css/views/rooms/_RoomPreviewCard.pcss b/res/css/views/rooms/_RoomPreviewCard.pcss index b31e0a33cc..9fbcd835f7 100644 --- a/res/css/views/rooms/_RoomPreviewCard.pcss +++ b/res/css/views/rooms/_RoomPreviewCard.pcss @@ -95,6 +95,8 @@ Please see LICENSE files in the repository root for full details. display: flex; align-items: center; margin-top: $spacing-16; + /* For enhanced visibility under contrast control */ + outline: 1px solid transparent; :first-child { flex-shrink: 0; diff --git a/res/css/views/rooms/_RoomSublist.pcss b/res/css/views/rooms/_RoomSublist.pcss index 3361bce4bb..f1118ebed6 100644 --- a/res/css/views/rooms/_RoomSublist.pcss +++ b/res/css/views/rooms/_RoomSublist.pcss @@ -100,23 +100,19 @@ Please see LICENSE files in the repository root for full details. height: 24px; border-radius: 8px; - &::before { - content: ""; + svg { width: 16px; height: 16px; position: absolute; top: 4px; left: 4px; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background: var(--cpd-color-icon-secondary); + color: var(--cpd-color-icon-secondary); } } - .mx_RoomSublist_auxButton:hover, - .mx_RoomSublist_menuButton:hover { - background: $panel-actions; + .mx_RoomSublist_auxButton:hover svg, + .mx_RoomSublist_menuButton:hover svg { + color: $panel-actions; } /* Hide the menu button by default */ @@ -126,14 +122,6 @@ Please see LICENSE files in the repository root for full details. margin: 0; } - .mx_RoomSublist_auxButton::before { - mask-image: url("@vector-im/compound-design-tokens/icons/plus.svg"); - } - - .mx_RoomSublist_menuButton::before { - mask-image: url("@vector-im/compound-design-tokens/icons/overflow-horizontal.svg"); - } - .mx_RoomSublist_headerText { flex: 1; max-width: calc(100% - 16px); /* 16px is the badge width */ @@ -151,20 +139,11 @@ Please see LICENSE files in the repository root for full details. height: 14px; margin-right: 6px; - &::before { - content: ""; + svg { width: 18px; height: 18px; position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background-color: var(--cpd-color-icon-secondary); - mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); - } - - &.mx_RoomSublist_collapseBtn_collapsed::before { - transform: rotate(-90deg); + color: var(--cpd-color-icon-secondary); } } } @@ -267,21 +246,9 @@ Please see LICENSE files in the repository root for full details. height: 18px; margin-left: 12px; margin-right: 16px; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background: $tertiary-content; + color: $tertiary-content; left: -1px; /* adjust for image position */ } - - .mx_RoomSublist_showMoreButtonChevron, - .mx_RoomSublist_showLessButtonChevron { - mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); - } - - .mx_RoomSublist_showLessButtonChevron { - transform: rotate(180deg); - } } &.mx_RoomSublist_hasMenuOpen, @@ -320,7 +287,7 @@ Please see LICENSE files in the repository root for full details. background-color: $panel-actions; margin-top: 8px; - &::before { + svg { top: 8px; left: 8px; } @@ -359,7 +326,7 @@ Please see LICENSE files in the repository root for full details. /* to occlude the sublist title */ background-color: $roomlist-bg-color; - &::before { + svg { top: 0; left: 0; } @@ -412,7 +379,7 @@ Please see LICENSE files in the repository root for full details. position: absolute; mask-repeat: repeat-y; mask-size: auto 48px; - mask-image: url("$(res)/img/element-icons/roomlist/skeleton-ui.svg"); + mask-image: url("/res/img/element-icons/roomlist/skeleton-ui.svg"); } } diff --git a/res/css/views/rooms/_ThreadSummary.pcss b/res/css/views/rooms/_ThreadSummary.pcss index 0d88d4f8c6..a030918f24 100644 --- a/res/css/views/rooms/_ThreadSummary.pcss +++ b/res/css/views/rooms/_ThreadSummary.pcss @@ -112,7 +112,7 @@ Please see LICENSE files in the repository root for full details. display: inline-block; margin-bottom: $spacing-8; - &::before { + > svg { @mixin ThreadSummaryIcon; vertical-align: middle; margin-inline-end: $spacing-8; diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.pcss b/res/css/views/rooms/_VoiceRecordComposerTile.pcss index 1715a8efe5..9c54538673 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.pcss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.pcss @@ -7,23 +7,18 @@ Please see LICENSE files in the repository root for full details. */ .mx_VoiceRecordComposerTile_stop { - /* 28px plus a 2px border makes this a 32px square (as intended) */ - width: 28px; - height: 28px; + /* 20px + 4px padding + 2px border makes this a 32px square (as intended) */ + width: 20px; + height: 20px; + padding: var(--cpd-space-1x); border: 2px solid $voice-record-stop-border-color; border-radius: 32px; margin-right: 2px; /* between us and the waveform component */ - position: relative; - &::after { - content: ""; - width: 14px; - height: 14px; - position: absolute; - top: 7px; - left: 7px; - border-radius: 2px; - background-color: $voice-record-stop-symbol-color; + svg { + width: inherit; + height: inherit; + color: $voice-record-stop-symbol-color; } } diff --git a/res/css/views/rooms/_WhoIsTypingTile.pcss b/res/css/views/rooms/_WhoIsTypingTile.pcss index eb604155c5..08f519eaa6 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.pcss +++ b/res/css/views/rooms/_WhoIsTypingTile.pcss @@ -49,7 +49,7 @@ Please see LICENSE files in the repository root for full details. } .mx_WhoIsTypingTile_label > span { - background-image: url("$(res)/img/typing-indicator-2x.gif"); + background-image: url("/res/img/typing-indicator-2x.gif"); background-size: 25px; background-position: left bottom; background-repeat: no-repeat; diff --git a/res/css/views/settings/_ImageSizePanel.pcss b/res/css/views/settings/_ImageSizePanel.pcss index 174f5a262a..9112c5cdfe 100644 --- a/res/css/views/settings/_ImageSizePanel.pcss +++ b/res/css/views/settings/_ImageSizePanel.pcss @@ -17,20 +17,10 @@ Please see LICENSE files in the repository root for full details. } .mx_ImageSizePanel_size { - background-color: $quinary-content; - mask-repeat: no-repeat; - mask-size: 221px; - mask-position: center; + display: block; + color: $quinary-content; width: 221px; height: 148px; margin-bottom: 14px; /* move radio button away from bottom edge a bit */ - - &.mx_ImageSizePanel_sizeDefault { - mask: url("$(res)/img/element-icons/settings/img-size-normal.svg"); - } - - &.mx_ImageSizePanel_sizeLarge { - mask: url("$(res)/img/element-icons/settings/img-size-large.svg"); - } } } diff --git a/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss b/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss index 38609c7fd4..36c852c8df 100644 --- a/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss +++ b/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss @@ -17,21 +17,8 @@ Please see LICENSE files in the repository root for full details. font-weight: var(--cpd-font-weight-semibold); margin-top: 16px; position: relative; - padding-left: 8px; align-items: center; - &::before { - content: ""; - position: absolute; - height: 24px; - width: 24px; - left: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - background-color: $secondary-content; - } - input + div { margin-top: 8px; } @@ -44,22 +31,6 @@ Please see LICENSE files in the repository root for full details. margin-right: 32px; } } - - .mx_NotificationSettingsTab_defaultEntry::before { - mask-image: url("$(res)/img/element-icons/notifications.svg"); - } - - .mx_NotificationSettingsTab_allMessagesEntry::before { - mask-image: url("$(res)/img/element-icons/roomlist/notifications-default.svg"); - } - - .mx_NotificationSettingsTab_mentionsKeywordsEntry::before { - mask-image: url("$(res)/img/element-icons/roomlist/notifications-dm.svg"); - } - - .mx_NotificationSettingsTab_noneEntry::before { - mask-image: url("$(res)/img/element-icons/roomlist/notifications-off.svg"); - } } input[type="file"].mx_NotificationSound_soundUpload { diff --git a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.pcss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.pcss index 5ae0d5854c..f21b83b0fe 100644 --- a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.pcss +++ b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.pcss @@ -13,4 +13,8 @@ Please see LICENSE files in the repository root for full details. display: flex; align-items: center; column-gap: $spacing-4; + + svg { + color: #ff0064; /* Match legacy icon colour until redesign */ + } } diff --git a/res/css/views/spaces/_SpaceCreateMenu.pcss b/res/css/views/spaces/_SpaceCreateMenu.pcss index 8bc7f6e3ad..9b85b30d91 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.pcss +++ b/res/css/views/spaces/_SpaceCreateMenu.pcss @@ -35,26 +35,15 @@ Please see LICENSE files in the repository root for full details. } .mx_SpaceCreateMenu_back { - width: 28px; - height: 28px; - position: relative; + width: 24px; + height: 24px; + padding: var(--cpd-space-0-5x); background-color: $panel-actions; border-radius: 14px; margin-bottom: 12px; - &::before { - content: ""; - position: absolute; - height: 28px; - width: 28px; - top: 0; - left: 0; - background-color: $tertiary-content; - transform: rotate(90deg); - mask-repeat: no-repeat; - mask-position: 2px 3px; - mask-size: 24px; - mask-image: url("@vector-im/compound-design-tokens/icons/chevron-down.svg"); + svg { + color: $tertiary-content; } } diff --git a/res/css/views/terms/_InlineTermsAgreement.pcss b/res/css/views/terms/_InlineTermsAgreement.pcss index 3a783cfd32..3b1184ec9d 100644 --- a/res/css/views/terms/_InlineTermsAgreement.pcss +++ b/res/css/views/terms/_InlineTermsAgreement.pcss @@ -24,15 +24,3 @@ Please see LICENSE files in the repository root for full details. } } } - -.mx_InlineTermsAgreement_link { - display: inline-block; - mask-image: url("@vector-im/compound-design-tokens/icons/pop-out.svg"); - background-color: $accent; - mask-repeat: no-repeat; - mask-size: contain; - width: 12px; - height: 12px; - margin-left: 3px; - vertical-align: middle; -} diff --git a/res/css/views/toasts/_IncomingCallToast.pcss b/res/css/views/toasts/_IncomingCallToast.pcss index d13a4eb61a..95359a5fad 100644 --- a/res/css/views/toasts/_IncomingCallToast.pcss +++ b/res/css/views/toasts/_IncomingCallToast.pcss @@ -29,11 +29,6 @@ Please see LICENSE files in the repository root for full details. font-weight: var(--cpd-font-weight-semibold); } - .mx_LiveContentSummary_participants::before { - width: 15px; - height: 15px; - } - .mx_IncomingCallToast_buttons { display: flex; gap: var(--cpd-space-2x); diff --git a/res/css/views/toasts/_IncomingLegacyCallToast.pcss b/res/css/views/toasts/_IncomingLegacyCallToast.pcss index 667dd110d1..a1ec8867c4 100644 --- a/res/css/views/toasts/_IncomingLegacyCallToast.pcss +++ b/res/css/views/toasts/_IncomingLegacyCallToast.pcss @@ -44,36 +44,6 @@ Please see LICENSE files in the repository root for full details. display: flex; flex-direction: row; align-items: center; - - .mx_LegacyCallEvent_type_icon { - height: 16px; - width: 16px; - margin-right: 6px; - - &::before { - content: ""; - position: absolute; - height: inherit; - width: inherit; - background-color: $tertiary-content; - mask-repeat: no-repeat; - mask-size: contain; - } - } - } - - &.mx_IncomingLegacyCallToast_content_voice { - .mx_LegacyCallEvent_type .mx_LegacyCallEvent_type_icon::before, - .mx_IncomingLegacyCallToast_buttons .mx_IncomingLegacyCallToast_button_accept span::before { - mask-image: url("@vector-im/compound-design-tokens/icons/voice-call-solid.svg"); - } - } - - &.mx_IncomingLegacyCallToast_content_video { - .mx_LegacyCallEvent_type .mx_LegacyCallEvent_type_icon::before, - .mx_IncomingLegacyCallToast_buttons .mx_IncomingLegacyCallToast_button_accept span::before { - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg"); - } } .mx_IncomingLegacyCallToast_buttons { @@ -84,27 +54,10 @@ Please see LICENSE files in the repository root for full details. .mx_IncomingLegacyCallToast_button { @mixin LegacyCallButton; - padding: 0px 8px; + padding: 8px; flex-shrink: 0; flex-grow: 1; font-size: $font-15px; - - span { - padding: 8px 0; - } - - &.mx_IncomingLegacyCallToast_button_accept span::before { - mask-size: 13px; - width: 13px; - height: 13px; - } - - &.mx_IncomingLegacyCallToast_button_decline span::before { - mask-image: url("@vector-im/compound-design-tokens/icons/end-call.svg"); - mask-size: 16px; - width: 16px; - height: 16px; - } } } } @@ -114,23 +67,10 @@ Please see LICENSE files in the repository root for full details. height: 20px; width: 20px; - &::before { - content: ""; - + svg { height: inherit; width: inherit; - background-color: $tertiary-content; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; + color: $tertiary-content; } } - - .mx_IncomingLegacyCallToast_silence::before { - mask-image: url("@vector-im/compound-design-tokens/icons/volume-on-solid.svg"); - } - - .mx_IncomingLegacyCallToast_unSilence::before { - mask-image: url("@vector-im/compound-design-tokens/icons/volume-off-solid.svg"); - } } diff --git a/res/css/views/toasts/_NonUrgentEchoFailureToast.pcss b/res/css/views/toasts/_NonUrgentEchoFailureToast.pcss index 9a4afb98ab..af4637750a 100644 --- a/res/css/views/toasts/_NonUrgentEchoFailureToast.pcss +++ b/res/css/views/toasts/_NonUrgentEchoFailureToast.pcss @@ -7,15 +7,11 @@ Please see LICENSE files in the repository root for full details. */ .mx_NonUrgentEchoFailureToast { - .mx_NonUrgentEchoFailureToast_icon { + svg { display: inline-block; width: $font-18px; height: $font-18px; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background-color: #fff; /* we know that non-urgent toasts are always styled the same */ - mask-image: url("@vector-im/compound-design-tokens/icons/offline.svg"); + color: #fff; /* we know that non-urgent toasts are always styled the same */ margin-right: 8px; } diff --git a/res/css/views/verification/_VerificationShowSas.pcss b/res/css/views/verification/_VerificationShowSas.pcss index 9e4d1f138b..57fae76788 100644 --- a/res/css/views/verification/_VerificationShowSas.pcss +++ b/res/css/views/verification/_VerificationShowSas.pcss @@ -20,40 +20,9 @@ Please see LICENSE files in the repository root for full details. } .mx_VerificationShowSas_emojiSas { - text-align: center; - display: flex; - flex-wrap: wrap; - justify-content: center; margin: 25px 0; } -.mx_VerificationShowSas_emojiSas_block { - display: inline-block; - margin-bottom: 16px; - position: relative; - width: 52px; -} - -.mx_Dialog .mx_VerificationShowSas_emojiSas_block, -.mx_AuthPage_modal .mx_VerificationShowSas_emojiSas_block { - width: 60px; -} - -.mx_VerificationShowSas_emojiSas_emoji { - font-size: $font-32px; - /* Use the Twemoji font for consistency with other clients */ - font-family: Twemoji, var(--cpd-font-family-sans); -} - -.mx_VerificationShowSas_emojiSas_label { - font-size: $font-12px; - word-break: break-word; -} - -.mx_VerificationShowSas_emojiSas_break { - flex-basis: 100%; -} - .mx_VerificationShowSas_buttonRow { text-align: center; display: flex; diff --git a/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss b/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss index ab0146fadc..c4ad182fea 100644 --- a/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss +++ b/res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss @@ -44,15 +44,10 @@ Please see LICENSE files in the repository root for full details. box-shadow: 0px 4px 4px 0px #00000026; /* Same on both themes */ - &::before { - content: ""; + svg { display: inline-block; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - - background-color: $call-view-button-on-foreground; + color: $call-view-button-on-foreground; height: 24px; width: 24px; @@ -66,103 +61,60 @@ Please see LICENSE files in the repository root for full details. right: 0; bottom: 0; - &::before { + svg { width: 16px; height: 16px; - mask-image: url("@vector-im/compound-design-tokens/icons/chevron-up.svg"); } + } - &.mx_LegacyCallViewButtons_dropdownButton_collapsed::before { - transform: rotate(180deg); - } + &.mx_LegacyCallViewButtons_button_mic svg { + height: 20px; + width: 20px; } /* State buttons */ &.mx_LegacyCallViewButtons_button_on { background-color: $call-view-button-on-background; - &::before { - background-color: $call-view-button-on-foreground; - } - - &.mx_LegacyCallViewButtons_button_mic::before { - height: 20px; - mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg"); - width: 20px; - } - - &.mx_LegacyCallViewButtons_button_vid::before { - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg"); + svg { + color: $call-view-button-on-foreground; } &.mx_LegacyCallViewButtons_button_screensharing { background-color: $accent; - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/share-screen-solid.svg"); - background-color: white; /* Same on both themes */ + svg { + color: white; /* Same on both themes */ } } - - &.mx_LegacyCallViewButtons_button_sidebar::before { - mask-image: url("$(res)/img/voip/call-view/sidebar-on.svg"); - } } &.mx_LegacyCallViewButtons_button_off { background-color: $call-view-button-off-background; - &::before { - background-color: $call-view-button-off-foreground; - } - - &.mx_LegacyCallViewButtons_button_mic::before { - height: 20px; - mask-image: url("@vector-im/compound-design-tokens/icons/mic-off-solid.svg"); - width: 20px; - } - - &.mx_LegacyCallViewButtons_button_vid::before { - mask-image: url("@vector-im/compound-design-tokens/icons/video-call-off-solid.svg"); - } - - &.mx_LegacyCallViewButtons_button_screensharing { - background-color: $call-view-button-on-background; - - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/share-screen-solid.svg"); - background-color: $call-view-button-on-foreground; - } + svg { + color: $call-view-button-off-foreground; } + &.mx_LegacyCallViewButtons_button_screensharing, &.mx_LegacyCallViewButtons_button_sidebar { background-color: $call-view-button-on-background; - &::before { - mask-image: url("$(res)/img/voip/call-view/sidebar-off.svg"); - background-color: $call-view-button-on-foreground; + svg { + color: $call-view-button-on-foreground; } } } /* State buttons */ /* Stateless buttons */ - &.mx_LegacyCallViewButtons_dialpad::before { - mask-image: url("@vector-im/compound-design-tokens/icons/dial-pad.svg"); - } - &.mx_LegacyCallViewButtons_button_hangup { background-color: $alert; - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/end-call.svg"); - background-color: white; /* Same on both themes */ + svg { + color: white; /* Same on both themes */ } } - - &.mx_LegacyCallViewButtons_button_more::before { - mask-image: url("@vector-im/compound-design-tokens/icons/overflow-horizontal.svg"); - } /* Stateless buttons */ /* Invisible state */ diff --git a/res/css/views/voip/_DialPad.pcss b/res/css/views/voip/_DialPad.pcss index 717ec335b8..42b46600e0 100644 --- a/res/css/views/voip/_DialPad.pcss +++ b/res/css/views/voip/_DialPad.pcss @@ -44,16 +44,11 @@ Please see LICENSE files in the repository root for full details. grid-column: 2; background-color: $accent; - &::before { - content: ""; + svg { display: inline-block; - height: 40px; - width: 40px; - vertical-align: middle; - mask-repeat: no-repeat; - mask-size: 20px; - mask-position: center; - background-color: #fff; /* on all themes */ - mask-image: url("@vector-im/compound-design-tokens/icons/voice-call-solid.svg"); + height: 20px; + width: 20px; + padding: 10px; + color: #fff; /* on all themes */ } } diff --git a/res/css/views/voip/_LegacyCallView.pcss b/res/css/views/voip/_LegacyCallView.pcss index 93d3e23f8b..a8d13adc62 100644 --- a/res/css/views/voip/_LegacyCallView.pcss +++ b/res/css/views/voip/_LegacyCallView.pcss @@ -110,7 +110,7 @@ Please see LICENSE files in the repository root for full details. content: ""; width: 40px; height: 40px; - background-image: url("$(res)/img/voip/paused.svg"); + background-image: url("/res/img/voip/paused.svg"); background-position: center; background-size: cover; } diff --git a/res/css/views/voip/_LegacyCallViewHeader.pcss b/res/css/views/voip/_LegacyCallViewHeader.pcss index 234ecba41f..e9d81ce0f3 100644 --- a/res/css/views/voip/_LegacyCallViewHeader.pcss +++ b/res/css/views/voip/_LegacyCallViewHeader.pcss @@ -46,34 +46,12 @@ Please see LICENSE files in the repository root for full details. vertical-align: middle; cursor: pointer; - &::before { - content: ""; + svg { display: inline-block; height: 20px; width: 20px; vertical-align: middle; - background-color: $secondary-content; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } - - &.mx_LegacyCallViewHeader_button_fullscreen { - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/expand.svg"); - } - } - - &.mx_LegacyCallViewHeader_button_pin { - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/pin-solid.svg"); - } - } - - &.mx_LegacyCallViewHeader_button_expand { - &::before { - mask-image: url("@vector-im/compound-design-tokens/icons/pop-out.svg"); - } + color: $secondary-content; } } @@ -105,17 +83,12 @@ Please see LICENSE files in the repository root for full details. width: 16px; vertical-align: middle; - &::before { - content: ""; + svg { display: inline-block; vertical-align: top; height: 16px; width: 16px; - background-color: $secondary-content; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - mask-image: url("@vector-im/compound-design-tokens/icons/voice-call-solid.svg"); + color: $secondary-content; } } diff --git a/res/img/element-desktop-logo.svg b/res/img/element-desktop-logo.svg index 2031733ce3..2483c75f98 100644 --- a/res/img/element-desktop-logo.svg +++ b/res/img/element-desktop-logo.svg @@ -1,157 +1,87 @@ - - - + + + + + + + + - - + + + + + + + + - - - - - - - - + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/element-icons/call/delete.svg b/res/img/element-icons/call/delete.svg deleted file mode 100644 index 133bdad4ca..0000000000 --- a/res/img/element-icons/call/delete.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/element-icons/child-relationship.svg b/res/img/element-icons/child-relationship.svg deleted file mode 100644 index 5a848c0d97..0000000000 --- a/res/img/element-icons/child-relationship.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/element-icons/collapse-message.svg b/res/img/element-icons/collapse-message.svg deleted file mode 100644 index da8bfb76b6..0000000000 --- a/res/img/element-icons/collapse-message.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/element-icons/expand-message.svg b/res/img/element-icons/expand-message.svg deleted file mode 100644 index c3abd0c471..0000000000 --- a/res/img/element-icons/expand-message.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/element-icons/feedback.svg b/res/img/element-icons/feedback.svg deleted file mode 100644 index 3ee20d18d9..0000000000 --- a/res/img/element-icons/feedback.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/notifications.svg b/res/img/element-icons/notifications.svg deleted file mode 100644 index 7709c673f1..0000000000 --- a/res/img/element-icons/notifications.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/element-icons/room/settings/advanced.svg b/res/img/element-icons/room/settings/advanced.svg deleted file mode 100644 index 2adbc7d3de..0000000000 --- a/res/img/element-icons/room/settings/advanced.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/roomlist/dark-light-mode.svg b/res/img/element-icons/roomlist/dark-light-mode.svg deleted file mode 100644 index a6a6464b5c..0000000000 --- a/res/img/element-icons/roomlist/dark-light-mode.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/roomlist/hash-video.svg b/res/img/element-icons/roomlist/hash-video.svg deleted file mode 100644 index b0e1decf68..0000000000 --- a/res/img/element-icons/roomlist/hash-video.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/element-icons/roomlist/notifications-default.svg b/res/img/element-icons/roomlist/notifications-default.svg deleted file mode 100644 index c3af0ef809..0000000000 --- a/res/img/element-icons/roomlist/notifications-default.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/element-icons/roomlist/notifications-dm.svg b/res/img/element-icons/roomlist/notifications-dm.svg deleted file mode 100644 index 9259c4d880..0000000000 --- a/res/img/element-icons/roomlist/notifications-dm.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/roomlist/notifications-off.svg b/res/img/element-icons/roomlist/notifications-off.svg deleted file mode 100644 index bbc2dcfeb9..0000000000 --- a/res/img/element-icons/roomlist/notifications-off.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/res/img/element-icons/settings/desktop.svg b/res/img/element-icons/settings/desktop.svg deleted file mode 100644 index 7d6ca10079..0000000000 --- a/res/img/element-icons/settings/desktop.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/settings/img-size-large.svg b/res/img/element-icons/settings/img-size-large.svg index 749a5c7ecb..64046e515a 100644 --- a/res/img/element-icons/settings/img-size-large.svg +++ b/res/img/element-icons/settings/img-size-large.svg @@ -7,9 +7,9 @@ - - - - - + + + + + diff --git a/res/img/element-icons/settings/img-size-normal.svg b/res/img/element-icons/settings/img-size-normal.svg index 96d8fd3fb4..3c74116920 100644 --- a/res/img/element-icons/settings/img-size-normal.svg +++ b/res/img/element-icons/settings/img-size-normal.svg @@ -7,14 +7,14 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/res/img/element-icons/settings/mobile.svg b/res/img/element-icons/settings/mobile.svg deleted file mode 100644 index 45170b2c15..0000000000 --- a/res/img/element-icons/settings/mobile.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/settings/unknown-device.svg b/res/img/element-icons/settings/unknown-device.svg deleted file mode 100644 index c02c2eadf5..0000000000 --- a/res/img/element-icons/settings/unknown-device.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/settings/web.svg b/res/img/element-icons/settings/web.svg deleted file mode 100644 index 95bd1ba24e..0000000000 --- a/res/img/element-icons/settings/web.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/spaces.svg b/res/img/element-icons/spaces.svg deleted file mode 100644 index 7183b4eca9..0000000000 --- a/res/img/element-icons/spaces.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/view-in-room.svg b/res/img/element-icons/view-in-room.svg deleted file mode 100644 index abaee191f4..0000000000 --- a/res/img/element-icons/view-in-room.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/img/emojipicker/activity.svg b/res/img/emojipicker/activity.svg deleted file mode 100644 index d921667e7a..0000000000 --- a/res/img/emojipicker/activity.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/res/img/emojipicker/custom.svg b/res/img/emojipicker/custom.svg deleted file mode 100644 index 814cd8ec13..0000000000 --- a/res/img/emojipicker/custom.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - diff --git a/res/img/emojipicker/delete.svg b/res/img/emojipicker/delete.svg deleted file mode 100644 index 5f5d4e52eb..0000000000 --- a/res/img/emojipicker/delete.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/res/img/emojipicker/flags.svg b/res/img/emojipicker/flags.svg deleted file mode 100644 index bd0a935265..0000000000 --- a/res/img/emojipicker/flags.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/res/img/emojipicker/foods.svg b/res/img/emojipicker/foods.svg deleted file mode 100644 index 57a15976d8..0000000000 --- a/res/img/emojipicker/foods.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/res/img/emojipicker/nature.svg b/res/img/emojipicker/nature.svg deleted file mode 100644 index a4778be927..0000000000 --- a/res/img/emojipicker/nature.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/res/img/emojipicker/objects.svg b/res/img/emojipicker/objects.svg deleted file mode 100644 index e0d39f985a..0000000000 --- a/res/img/emojipicker/objects.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/res/img/emojipicker/people.svg b/res/img/emojipicker/people.svg deleted file mode 100644 index c2fdb579f6..0000000000 --- a/res/img/emojipicker/people.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/res/img/emojipicker/places.svg b/res/img/emojipicker/places.svg deleted file mode 100644 index 0947baaf04..0000000000 --- a/res/img/emojipicker/places.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/res/img/emojipicker/recent.svg b/res/img/emojipicker/recent.svg deleted file mode 100644 index 2fdcc65cd2..0000000000 --- a/res/img/emojipicker/recent.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/res/img/emojipicker/search.svg b/res/img/emojipicker/search.svg deleted file mode 100644 index b5f660b3ac..0000000000 --- a/res/img/emojipicker/search.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/res/img/emojipicker/symbols.svg b/res/img/emojipicker/symbols.svg deleted file mode 100644 index a2b86d9ec8..0000000000 --- a/res/img/emojipicker/symbols.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/bridge.svg b/res/img/feather-customised/bridge.svg deleted file mode 100644 index f8f3468155..0000000000 --- a/res/img/feather-customised/bridge.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/res/img/feather-customised/bug.svg b/res/img/feather-customised/bug.svg deleted file mode 100644 index ea2e4222aa..0000000000 --- a/res/img/feather-customised/bug.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/feather-customised/secure-backup.svg b/res/img/feather-customised/secure-backup.svg deleted file mode 100644 index c06f93c1fe..0000000000 --- a/res/img/feather-customised/secure-backup.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/res/img/feather-customised/secure-phrase.svg b/res/img/feather-customised/secure-phrase.svg deleted file mode 100644 index eb13d3f048..0000000000 --- a/res/img/feather-customised/secure-phrase.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/res/img/image-view/rotate-ccw.svg b/res/img/image-view/rotate-ccw.svg deleted file mode 100644 index 85ea3198de..0000000000 --- a/res/img/image-view/rotate-ccw.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/image-view/rotate-cw.svg b/res/img/image-view/rotate-cw.svg deleted file mode 100644 index e337f3420e..0000000000 --- a/res/img/image-view/rotate-cw.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/image-view/zoom-in.svg b/res/img/image-view/zoom-in.svg deleted file mode 100644 index c0816d489e..0000000000 --- a/res/img/image-view/zoom-in.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/image-view/zoom-out.svg b/res/img/image-view/zoom-out.svg deleted file mode 100644 index 0539e8c81a..0000000000 --- a/res/img/image-view/zoom-out.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/voip/call-view/sidebar-off.svg b/res/img/voip/call-view/sidebar-off.svg deleted file mode 100644 index 86aae6a05a..0000000000 --- a/res/img/voip/call-view/sidebar-off.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/voip/call-view/sidebar-on.svg b/res/img/voip/call-view/sidebar-on.svg deleted file mode 100644 index 535b589098..0000000000 --- a/res/img/voip/call-view/sidebar-on.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/voip/missed-voice.svg b/res/img/voip/missed-voice.svg deleted file mode 100644 index 5e3993584e..0000000000 --- a/res/img/voip/missed-voice.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/warning.svg b/res/img/warning.svg deleted file mode 100644 index b9a96a88e5..0000000000 --- a/res/img/warning.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/themes/dark-custom/css/dark-custom.pcss b/res/themes/dark-custom/css/dark-custom.pcss index dad1ede631..7eaffcee0c 100644 --- a/res/themes/dark-custom/css/dark-custom.pcss +++ b/res/themes/dark-custom/css/dark-custom.pcss @@ -1,9 +1,9 @@ @import "../../../../res/css/_font-sizes.pcss"; -@import "../../legacy-light/css/_paths.pcss"; @import "../../legacy-light/css/_fonts.pcss"; @import "../../legacy-light/css/_legacy-light.pcss"; @import "../../legacy-dark/css/_legacy-dark.pcss"; @import "../../light-custom/css/_custom.pcss"; @import "../../../../res/css/_components.pcss"; @import "../../../../res/css/_compound.pcss"; +@import url("highlight.js/styles/atom-one-light.min.css"); @import url("github-markdown-css/github-markdown-dark.css"); diff --git a/res/themes/dark/css/dark.pcss b/res/themes/dark/css/dark.pcss index b81a2793f0..05cc9dfb6a 100644 --- a/res/themes/dark/css/dark.pcss +++ b/res/themes/dark/css/dark.pcss @@ -1,5 +1,4 @@ @import "../../../../res/css/_font-sizes.pcss"; -@import "../../light/css/_paths.pcss"; @import "../../light/css/_fonts.pcss"; @import "../../light/css/_light.pcss"; @import "_dark.pcss"; diff --git a/res/themes/legacy-dark/css/legacy-dark.pcss b/res/themes/legacy-dark/css/legacy-dark.pcss index 240175c9d1..d816c8c24d 100644 --- a/res/themes/legacy-dark/css/legacy-dark.pcss +++ b/res/themes/legacy-dark/css/legacy-dark.pcss @@ -1,5 +1,4 @@ @import "../../../../res/css/_font-sizes.pcss"; -@import "../../legacy-light/css/_paths.pcss"; @import "../../legacy-light/css/_fonts.pcss"; @import "../../legacy-light/css/_legacy-light.pcss"; @import "_legacy-dark.pcss"; diff --git a/res/themes/legacy-light/css/_fonts.pcss b/res/themes/legacy-light/css/_fonts.pcss index bc65ec2d6e..dd22ecef34 100644 --- a/res/themes/legacy-light/css/_fonts.pcss +++ b/res/themes/legacy-light/css/_fonts.pcss @@ -23,17 +23,17 @@ font-family: "Nunito"; font-style: normal; font-weight: 400; - src: url("$(res)/fonts/Nunito/Nunito-Regular.ttf") format("truetype"); + src: url("/res/fonts/Nunito/Nunito-Regular.ttf") format("truetype"); } @font-face { font-family: "Nunito"; font-style: normal; font-weight: 600; - src: url("$(res)/fonts/Nunito/Nunito-SemiBold.ttf") format("truetype"); + src: url("/res/fonts/Nunito/Nunito-SemiBold.ttf") format("truetype"); } @font-face { font-family: "Nunito"; font-style: normal; font-weight: 700; - src: url("$(res)/fonts/Nunito/Nunito-Bold.ttf") format("truetype"); + src: url("/res/fonts/Nunito/Nunito-Bold.ttf") format("truetype"); } diff --git a/res/themes/legacy-light/css/_paths.pcss b/res/themes/legacy-light/css/_paths.pcss deleted file mode 100644 index d35445dfd9..0000000000 --- a/res/themes/legacy-light/css/_paths.pcss +++ /dev/null @@ -1,3 +0,0 @@ -/* Path from root SCSS file (such as `light.pcss`) to `res` dir in the source tree */ -/* This value is overridden by external themes in `element-web`. */ -$res: ../../..; diff --git a/res/themes/legacy-light/css/legacy-light.pcss b/res/themes/legacy-light/css/legacy-light.pcss index 7e42d75bfc..d196b2850c 100644 --- a/res/themes/legacy-light/css/legacy-light.pcss +++ b/res/themes/legacy-light/css/legacy-light.pcss @@ -1,5 +1,4 @@ @import "../../../../res/css/_font-sizes.pcss"; -@import "_paths.pcss"; @import "_fonts.pcss"; @import "_legacy-light.pcss"; @import "../../../../res/css/_components.pcss"; diff --git a/res/themes/light-custom/css/light-custom.pcss b/res/themes/light-custom/css/light-custom.pcss index d58df8e68f..d02c0eeef4 100644 --- a/res/themes/light-custom/css/light-custom.pcss +++ b/res/themes/light-custom/css/light-custom.pcss @@ -1,8 +1,8 @@ @import "../../../../res/css/_font-sizes.pcss"; -@import "../../legacy-light/css/_paths.pcss"; @import "../../legacy-light/css/_fonts.pcss"; @import "../../legacy-light/css/_legacy-light.pcss"; @import "_custom.pcss"; @import "../../../../res/css/_components.pcss"; @import "../../../../res/css/_compound.pcss"; +@import url("highlight.js/styles/atom-one-light.min.css"); @import url("github-markdown-css/github-markdown-light.css"); diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.pcss b/res/themes/light-high-contrast/css/_light-high-contrast.pcss index 94774bc5b8..ada87d7a33 100644 --- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss @@ -160,12 +160,8 @@ $accent-1400: var(--cpd-color-green-1400); background-color: $quinary-content !important; color: $background !important; - &.mx_SpotlightDialog_startChat::before, - &.mx_SpotlightDialog_joinRoomAlias::before, - &.mx_SpotlightDialog_explorePublicRooms::before, - &.mx_SpotlightDialog_startGroupChat::before, - &.mx_SpotlightDialog_searchMessages::before { - background-color: $background !important; + svg { + color: $background !important; } .mx_DecoratedRoomAvatar_icon::before { diff --git a/res/themes/light-high-contrast/css/light-high-contrast.pcss b/res/themes/light-high-contrast/css/light-high-contrast.pcss index 2556c2c4f5..c794d07499 100644 --- a/res/themes/light-high-contrast/css/light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/light-high-contrast.pcss @@ -1,5 +1,4 @@ @import "../../../../res/css/_font-sizes.pcss"; -@import "../../light/css/_paths.pcss"; @import "../../light/css/_fonts.pcss"; @import "../../light/css/_light.pcss"; @import "_light-high-contrast.pcss"; diff --git a/res/themes/light/css/_fonts.pcss b/res/themes/light/css/_fonts.pcss index 8044f47b21..878920102f 100644 --- a/res/themes/light/css/_fonts.pcss +++ b/res/themes/light/css/_fonts.pcss @@ -5,16 +5,16 @@ @font-face { font-family: "Twemoji"; font-weight: 400; - src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); + src: url("/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); } /* For at least Chrome on Windows 10, we have to explictly add extra weights for the emoji to appear in bold messages, etc. */ @font-face { font-family: "Twemoji"; font-weight: 600; - src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); + src: url("/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); } @font-face { font-family: "Twemoji"; font-weight: 700; - src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); + src: url("/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2"); } diff --git a/res/themes/light/css/_mods.pcss b/res/themes/light/css/_mods.pcss index 357a8a11d1..2764c8762f 100644 --- a/res/themes/light/css/_mods.pcss +++ b/res/themes/light/css/_mods.pcss @@ -8,8 +8,15 @@ background-color: transparent !important; } -a:hover, -a:link, -a:visited { - text-decoration: none; +/* + data-kind is used by the Compound component and therefore does not override them. + This is horrible, but will get less horrible when links in the app are replaced with + . +*/ +a:not([data-kind]) { + &:hover, + &:link, + &:visited { + text-decoration: none; + } } diff --git a/res/themes/light/css/_paths.pcss b/res/themes/light/css/_paths.pcss deleted file mode 100644 index d35445dfd9..0000000000 --- a/res/themes/light/css/_paths.pcss +++ /dev/null @@ -1,3 +0,0 @@ -/* Path from root SCSS file (such as `light.pcss`) to `res` dir in the source tree */ -/* This value is overridden by external themes in `element-web`. */ -$res: ../../..; diff --git a/res/themes/light/css/light.pcss b/res/themes/light/css/light.pcss index 3fd90482f1..d3abfe669c 100644 --- a/res/themes/light/css/light.pcss +++ b/res/themes/light/css/light.pcss @@ -1,5 +1,4 @@ @import "../../../../res/css/_font-sizes.pcss"; -@import "_paths.pcss"; @import "_fonts.pcss"; @import "_light.pcss"; @import "_mods.pcss"; diff --git a/scripts/copy-res.ts b/scripts/copy-res.ts index e74eb6fd2d..94e456700d 100755 --- a/scripts/copy-res.ts +++ b/scripts/copy-res.ts @@ -9,8 +9,10 @@ import _ from "lodash"; import webpack from "webpack"; import type { Translations } from "matrix-web-i18n"; -const I18N_BASE_PATH = "src/i18n/strings/"; -const INCLUDE_LANGS = [...new Set([...fs.readdirSync(I18N_BASE_PATH)])] +const EW_I18N_BASE_PATH = "src/i18n/strings/"; +const SC_I18N_BASE_PATH = "packages/shared-components/src/i18n/strings/"; + +const INCLUDE_LANGS = [...new Set([...fs.readdirSync(EW_I18N_BASE_PATH)])] .filter((fn) => fn.endsWith(".json")) .map((f) => f.slice(0, -5)); @@ -41,11 +43,17 @@ const logWatch = (path: string) => { } }; -function prepareLangFile(lang: string, dest: string): [filename: string, json: string] { - const path = I18N_BASE_PATH + lang + ".json"; +/* + * Make a JSON language file for the given language by merging all translations + * into a single file (ie. element-web and shared-components). + * Returns the filename (including hash) and JSON content. + */ +function prepareLangFile(lang: string): [filename: string, json: string] { + const ewTranslationsPath = EW_I18N_BASE_PATH + lang + ".json"; + const scTranslationsPath = SC_I18N_BASE_PATH + lang + ".json"; let translations: Translations = {}; - [path].forEach(function (f) { + [ewTranslationsPath, scTranslationsPath].forEach(function (f) { if (fs.existsSync(f)) { try { translations = _.merge(translations, JSON.parse(fs.readFileSync(f).toString())); @@ -99,7 +107,8 @@ function genLangList(langFileMap: Record): void { * and regenerating languages.json with the new filename */ function watchLanguage(lang: string, dest: string, langFileMap: Record): void { - const path = I18N_BASE_PATH + lang + ".json"; + const ewTranslationsPath = EW_I18N_BASE_PATH + lang + ".json"; + const scTranslationsPath = SC_I18N_BASE_PATH + lang + ".json"; // XXX: Use a debounce because for some reason if we read the language // file immediately after the FS event is received, the file contents @@ -110,14 +119,14 @@ function watchLanguage(lang: string, dest: string, langFileMap: Record { - const [filename, json] = prepareLangFile(lang, dest); + const [filename, json] = prepareLangFile(lang); genLangFile(dest, filename, json); langFileMap[lang] = filename; genLangList(langFileMap); }, 500); }; - [path].forEach(function (f) { + [ewTranslationsPath, scTranslationsPath].forEach(function (f) { chokidar .watch(f, { ignoreInitial: true }) .on("ready", () => { @@ -132,7 +141,7 @@ function watchLanguage(lang: string, dest: string, langFileMap: Record>((m, l) => { - const [filename, json] = prepareLangFile(l, I18N_DEST); + const [filename, json] = prepareLangFile(l); if (!watch) { genLangFile(I18N_DEST, filename, json); } diff --git a/sonar-project.properties b/sonar-project.properties index 3706897f63..c1d3f59842 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -21,6 +21,7 @@ sonar.coverage.exclusions=\ src/components/views/dialogs/devtools/**/*,\ src/utils/SessionLock.ts,\ src/**/*.d.ts,\ - src/vector/mobile_guide/**/* \ - packages/shared-components/src/test/**/* + src/vector/mobile_guide/**/*,\ + packages/shared-components/src/test/**/*,\ + packages/shared-components/src/**/*.stories.tsx sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 9d43a13ca4..19bc221444 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -80,7 +80,7 @@ declare global { function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number; interface Window { - mxSendRageshake: (text: string, withLogs?: boolean) => void; + mxSendRageshake: (text: string, withLogs?: boolean) => Promise; matrixLogger: typeof logger; matrixChat?: MatrixChat; mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise; diff --git a/src/@types/i18n.d.ts b/src/@types/i18n.d.ts new file mode 100644 index 0000000000..04531d8954 --- /dev/null +++ b/src/@types/i18n.d.ts @@ -0,0 +1,14 @@ +/* +Copyright 2025 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type TranslationKey as _TranslationKey } from "matrix-web-i18n"; + +import type Translations from "../i18n/strings/en_EN.json"; + +declare global { + type TranslationKey = _TranslationKey; +} diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index f9904eaef1..e9c723b75d 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -43,7 +43,6 @@ import SdkConfig from "./SdkConfig"; import PlatformPeg from "./PlatformPeg"; import { recordClientInformation, removeClientInformation } from "./utils/device/clientInformation"; import SettingsStore, { type CallbackFn } from "./settings/SettingsStore"; -import { UIFeature } from "./settings/UIFeature"; import { isBulkUnverifiedDeviceReminderSnoozed } from "./utils/device/snoozeBulkUnverifiedDeviceReminder"; import { getUserDeviceIds } from "./utils/crypto/deviceInfo"; import { asyncSomeParallel } from "./utils/arrays.ts"; @@ -125,7 +124,6 @@ export default class DeviceListener extends TypedEventEmitter 0 && - isCurrentDeviceTrusted && - this.enableBulkUnverifiedSessionsReminder && - !isBulkUnverifiedSessionsReminderSnoozed - ) { + if (oldUnverifiedDeviceIds.size > 0 && isCurrentDeviceTrusted && !isBulkUnverifiedSessionsReminderSnoozed) { showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds); } else { hideBulkUnverifiedSessionsToast(); @@ -733,16 +720,6 @@ export default class DeviceListener extends TypedEventEmitter => { - if (!(await this.isKeyBackupUploadActive(logger))) { - dis.dispatch({ action: Action.ReportKeyBackupNotEnabled }); - } - }; - /** * Is key backup enabled? Use a cached answer if we have one. */ diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 952d35e88d..8dcf5383eb 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -17,6 +17,12 @@ import { type ValidatedServerConfig } from "./utils/ValidatedServerConfig"; /* eslint-disable camelcase */ /* eslint @typescript-eslint/naming-convention: ["error", { "selector": "property", "format": ["snake_case"] } ] */ +/** + * Bug reports are enabled but must only be locally + * downloadable. + */ +export const BugReportEndpointURLLocal = "local"; + // see element-web config.md for non-developer docs export interface IConfigOptions { // dev note: while true that this is arbitrary JSON, it's valuable to enforce that all @@ -98,7 +104,10 @@ export interface IConfigOptions { show_labs_settings: boolean; features?: Record; // - bug_report_endpoint_url?: string; // omission disables bug reporting + /** + * Bug report endpoint URL. "local" means the logs should not be uploaded. + */ + bug_report_endpoint_url?: typeof BugReportEndpointURLLocal | string; // omission disables bug reporting uisi_autorageshake_app?: string; // defaults to "element-auto-uisi" sentry?: { dsn: string; @@ -120,7 +129,6 @@ export interface IConfigOptions { element_call: { guest_spa_url?: string; use_exclusively?: boolean; - participant_limit?: number; brand?: string; }; diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 746588241a..e0d5c7bd78 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -699,6 +699,10 @@ async function handleLoadSessionFailure(e: unknown, loadSessionOpts?: ILoadSessi * Also stops the old MatrixClient and clears old credentials/etc out of * storage before starting the new client. * + * This function does not work for OIDC login. + * Storage is cleared early in the process so the required data is lost. + * You must use {@link attemptDelegatedAuthLogin} followed by {@link restoreSessionFromStorage} for OIDC login. + * * @param {IMatrixClientCreds} credentials The credentials to use * * @returns {Promise} promise which resolves to the new MatrixClient once it has been started diff --git a/src/Notifier.ts b/src/Notifier.ts index aa68b386a3..6f7922fb94 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. */ import { - type MatrixEvent, + MatrixEvent, MatrixEventEvent, type Room, RoomEvent, @@ -25,7 +25,7 @@ import { } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { type PermissionChanged as PermissionChangedEvent } from "@matrix-org/analytics-events/types/typescript/PermissionChanged"; -import { type IRTCNotificationContent, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; +import { type SessionMembershipData, type IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc"; import { MatrixClientPeg } from "./MatrixClientPeg"; import { PosthogAnalytics } from "./PosthogAnalytics"; @@ -481,39 +481,100 @@ class NotifierClass extends TypedEventEmitter { + // TODO: Use the call_id to get the *correct* call. We assume there is only one call per room here. + const rtcSession = room && room.client.matrixRTC.getRoomSession(room); + if ( + rtcSession?.slotDescription?.application == "m.call" && + rtcSession.memberships.some((membership) => membership.userId === room.client.getUserId()) + ) { + // If we're already joined to the session, don't notify. + return; + } + + // XXX: Should use parseCallNotificationContent once the types are exported. + const content = ev.getContent() as IRTCNotificationContent; + const roomId = ev.getRoomId(); + const referencedMembershipEventId = ev.getRelation()?.event_id; + + // Check maximum age of a call notification event that will trigger a ringing notification + if (Date.now() - getNotificationEventSendTs(ev) > content.lifetime) { + logger.warn("Received outdated RTCNotification event."); + return; + } + if (!roomId) { + logger.warn("Could not get roomId for RTCNotification event"); + return; + } + if (!referencedMembershipEventId) { + logger.warn("Could not get referenced membership for notification"); + return; + } + if (content["m.relates_to"].rel_type !== "m.reference") { + logger.warn("Ignored RTCNotification due to invalid rel_type"); + return; + } + + let callMembership = room?.findEventById(referencedMembershipEventId); + + if (!callMembership) { + // Attempt to fetch from the homeserver, if we do not have the event locally. + // This is a rare case as obviously the referenced event for a m.call notification must + // be sent first. + try { + callMembership = new MatrixEvent(await room.client.fetchRoomEvent(roomId, referencedMembershipEventId)); + } catch (ex) { + logger.warn(`Call membership for notification could not be found`, ex); + } + } + // If the event could not be found even after requesting it from the homeserver. + if (!callMembership) { + // We will not show a call notification if there is no valid call membership. + logger.warn( + `Could not find call membership (${referencedMembershipEventId} ${roomId}) for notification event.`, + ); + return; + } + + // If we cannot determine the key, we'll accept it but assume it's empty string. + // This means if you have malformed notifications or call memberships your notifications + // will overwrite, but the solution to that is to use well-formed events. + const callId = callMembership.getContent().call_id ?? ""; + const key = getIncomingCallToastKey(callId, roomId); + + if (toaster.hasToast(key)) { + logger.debug(`Detected duplicate notification for call ${key}, ignoring`); + return; + } + + toaster.addOrReplaceToast({ + key, + priority: 100, + component: IncomingCallToast, + bodyClassName: "mx_IncomingCallToast", + props: { notificationEvent: ev }, + }); + } + + /** + * Some events require special handling such as showing in-app toasts. + * This function may either create a toast or ignore the event based + * on current app state. */ private performCustomEventHandling(ev: MatrixEvent): void { + const toaster = ToastStore.sharedInstance(); const cli = MatrixClientPeg.safeGet(); const room = cli.getRoom(ev.getRoomId()); - const thisUserHasConnectedDevice = - room && MatrixRTCSession.callMembershipsForRoom(room).some((m) => m.sender === cli.getUserId()); - if (EventType.RTCNotification === ev.getType() && !thisUserHasConnectedDevice) { - const content = ev.getContent() as IRTCNotificationContent; - const roomId = ev.getRoomId(); - const eventId = ev.getId(); - - // Check maximum age of a call notification event that will trigger a ringing notification - if (Date.now() - getNotificationEventSendTs(ev) > content.lifetime) { - logger.warn("Received outdated RTCNotification event."); - return; - } - if (!roomId) { - logger.warn("Could not get roomId for RTCNotification event"); - return; - } - if (!eventId) { - logger.warn("Could not get eventId for RTCNotification event"); - return; - } - ToastStore.sharedInstance().addOrReplaceToast({ - key: getIncomingCallToastKey(eventId, roomId), - priority: 100, - component: IncomingCallToast, - bodyClassName: "mx_IncomingCallToast", - props: { notificationEvent: ev }, - }); + if (room && EventType.RTCNotification === ev.getType()) { + // We don't need to await this. + void this.handleRTCNotification(ev, toaster, room); } } } diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index d5254d523d..2db2cf2158 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -13,16 +13,38 @@ import { PushRuleActionName, PushRuleKind, TweakName, + EventStatus, } from "matrix-js-sdk/src/matrix"; -import type { IPushRule, Room, MatrixClient } from "matrix-js-sdk/src/matrix"; +import type { IPushRule, Room, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { NotificationLevel } from "./stores/notifications/NotificationLevel"; -import { getUnsentMessages } from "./components/structures/RoomStatusBar"; import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread"; import { EffectiveMembership, getEffectiveMembership, isKnockDenied } from "./utils/membership"; import SettingsStore from "./settings/SettingsStore"; import { getMarkedUnreadState } from "./utils/notifications"; +/** + * Gets all pending events in a room that have a status of `EventStatus.NOT_SENT` + * and belong to the current thread, if specified. + * @param room The room to check. + * @param threadId The thread to check. If not specified, no thread filtering is performed. + * @returns An array of unsent matrix events. + */ +export function getUnsentMessages(room: Room, threadId?: string): MatrixEvent[] { + if (!room) { + return []; + } + return room.getPendingEvents().filter(function (ev) { + if (ev.status !== EventStatus.NOT_SENT) { + return false; + } + if (threadId && threadId !== ev.threadRootId) { + return false; + } + return true; + }); +} + export enum RoomNotifState { AllMessagesLoud = "all_messages_loud", AllMessages = "all_messages", diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 71ac6736c9..ab921bedc5 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -30,7 +30,6 @@ export const DEFAULTS: DeepReadonly = { }, element_call: { use_exclusively: false, - participant_limit: 8, brand: "Element Call", }, diff --git a/src/Searching.ts b/src/Searching.ts index d507bd10ef..28f67522ad 100644 --- a/src/Searching.ts +++ b/src/Searching.ts @@ -175,6 +175,26 @@ async function localSearch( throw new Error("Local search failed"); } + // Fix state_key: null issue - Seshat includes "state_key": null for non-state events, + // which causes matrix-js-sdk to incorrectly treat them as state events + if (localResult.results) { + for (const searchResult of localResult.results) { + const event = searchResult.result as unknown as Record; + if (event?.state_key === null) delete event.state_key; + // Also fix context events + if (searchResult.context) { + for (const ctxEvent of searchResult.context.events_before || []) { + const ev = ctxEvent as unknown as Record; + if (ev?.state_key === null) delete ev.state_key; + } + for (const ctxEvent of searchResult.context.events_after || []) { + const ev = ctxEvent as unknown as Record; + if (ev?.state_key === null) delete ev.state_key; + } + } + } + } + searchArgs.next_batch = localResult.next_batch; const result = { diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index a5be789c8a..84642626e8 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -25,7 +25,7 @@ import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDia // during the same single operation. Use `accessSecretStorage` below to scope a // single secret storage operation, as it will clear the cached keys once the // operation ends. -let secretStorageKeys: Record = {}; +let secretStorageKeys: Record> = {}; let secretStorageKeyInfo: Record = {}; let secretStorageBeingAccessed = false; @@ -50,8 +50,8 @@ export class AccessCancelledError extends Error { function makeInputToKey( keyInfo: SecretStorage.SecretStorageKeyDescription, -): (keyParams: KeyParams) => Promise { - return async ({ passphrase, recoveryKey }): Promise => { +): (keyParams: KeyParams) => Promise> { + return async ({ passphrase, recoveryKey }): Promise> => { if (passphrase) { return deriveRecoveryKeyFromPassphrase(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations); } else if (recoveryKey) { @@ -68,7 +68,7 @@ async function getSecretStorageKey( keys: Record; }, secretName: string, -): Promise<[string, Uint8Array]> { +): Promise<[string, Uint8Array]> { const cli = MatrixClientPeg.safeGet(); const defaultKeyId = await cli.secretStorage.getDefaultKeyId(); @@ -138,7 +138,7 @@ async function getSecretStorageKey( function cacheSecretStorageKey( keyId: string, keyInfo: SecretStorage.SecretStorageKeyDescription, - key: Uint8Array, + key: Uint8Array, ): void { if (secretStorageBeingAccessed) { logger.debug(`Caching 4S key ${keyId}`); diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index cf87bc79d0..0169513635 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -44,9 +44,7 @@ import { UIComponent, UIFeature } from "./settings/UIFeature"; import { CHAT_EFFECTS } from "./effects"; import LegacyCallHandler from "./LegacyCallHandler"; import { guessAndSetDMRoom } from "./Rooms"; -import { upgradeRoom } from "./utils/RoomUpgrade"; import DevtoolsDialog from "./components/views/dialogs/DevtoolsDialog"; -import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog"; import InfoDialog from "./components/views/dialogs/InfoDialog"; import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; import { shouldShowComponent } from "./customisations/helpers/UIComponents"; @@ -61,6 +59,7 @@ import { CommandCategories } from "./slash-commands/interface"; import { Command } from "./slash-commands/command"; import { goto, join } from "./slash-commands/join"; import { manuallyVerifyDevice } from "./components/views/dialogs/ManualDeviceKeyVerificationDialog"; +import upgraderoom from "./slash-commands/upgraderoom/upgraderoom"; export { CommandCategories, Command }; @@ -144,38 +143,7 @@ export const Commands = [ }, category: CommandCategories.messages, }), - new Command({ - command: "upgraderoom", - args: "", - description: _td("slash_command|upgraderoom"), - isEnabled: (cli) => !isCurrentLocalRoom(cli), - runFn: function (cli, roomId, threadId, args) { - if (args) { - const room = cli.getRoom(roomId); - if (!room?.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) { - return reject(new UserFriendlyError("slash_command|upgraderoom_permission_error")); - } - - const { finished } = Modal.createDialog( - RoomUpgradeWarningDialog, - { roomId: roomId, targetVersion: args }, - /*className=*/ undefined, - /*isPriority=*/ false, - /*isStatic=*/ true, - ); - - return success( - finished.then(async ([resp]): Promise => { - if (!resp?.continue) return; - await upgradeRoom(room, args, resp.invite); - }), - ); - } - return reject(this.getUsage()); - }, - category: CommandCategories.admin, - renderingTypes: [TimelineRenderingType.Room], - }), + upgraderoom, new Command({ command: "jumptodate", args: "", diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index 2872e1a1a2..9d4222d92f 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. */ // Import i18n.tsx instead of languageHandler to avoid circular deps -import { _td, type TranslationKey } from "@element-hq/web-shared-components"; +import { _td } from "@element-hq/web-shared-components"; import { IS_MAC, IS_ELECTRON, Key } from "../Keyboard"; import { type IBaseSetting } from "../settings/Settings"; diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index ed2fbe87a5..dc7d77dbe6 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -112,7 +112,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent): void { logger.log("CryptoSetupExtension: Created key via extension, jumping to bootstrap step"); this.recoveryKey = { privateKey: keyFromCustomisations, @@ -351,7 +351,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent
- {_t("settings|key_backup|setup_secure_backup|generate_security_key_title")}
{_t("settings|key_backup|setup_secure_backup|generate_security_key_description")}
@@ -370,7 +369,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent
- {_t("settings|key_backup|setup_secure_backup|enter_phrase_title")}
{_t("settings|key_backup|setup_secure_backup|use_phrase_only_you_know")}
@@ -694,19 +692,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // We need a clone of the buffer to avoid accidentally changing the position // on the real thing. return this.buffer.slice(0); diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index 53a5d7d537..8ea20d3cb3 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -12,7 +12,7 @@ import sanitizeHtml from "sanitize-html"; import classnames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { _t, type TranslationKey } from "../../languageHandler"; +import { _t } from "../../languageHandler"; import dis from "../../dispatcher/dispatcher"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import MatrixClientContext from "../../contexts/MatrixClientContext"; diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 5cbd2858c5..56444789c1 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX } from "react"; import { useContext, useState } from "react"; +import { ChatSolidIcon, ExploreIcon, GroupIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { getHomePageUrl } from "../../utils/pages"; @@ -117,12 +118,15 @@ const HomePage: React.FC = ({ justRegistered = false }) => { {introSection}
+ {_tDom("onboarding|send_dm")} + {_tDom("onboarding|explore_rooms")} + {_tDom("onboarding|create_room")}
diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index a4de1e4d21..7a41403410 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -117,6 +117,7 @@ export default class InteractiveAuthComponent extends React.Component { }); ret.push(
  • - + {text}
  • , diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx deleted file mode 100644 index d9889342b1..0000000000 --- a/src/components/structures/RoomStatusBar.tsx +++ /dev/null @@ -1,297 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2015-2021 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type JSX, type ReactNode } from "react"; -import { - ClientEvent, - EventStatus, - type MatrixError, - type MatrixEvent, - type Room, - RoomEvent, - type SyncState, - type SyncStateData, -} from "matrix-js-sdk/src/matrix"; -import { RestartIcon, WarningIcon, DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -import { _t, _td } from "../../languageHandler"; -import Resend from "../../Resend"; -import dis from "../../dispatcher/dispatcher"; -import { messageForResourceLimitError } from "../../utils/ErrorUtils"; -import { Action } from "../../dispatcher/actions"; -import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState"; -import AccessibleButton from "../views/elements/AccessibleButton"; -import InlineSpinner from "../views/elements/InlineSpinner"; -import MatrixClientContext from "../../contexts/MatrixClientContext"; -import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages"; -import ExternalLink from "../views/elements/ExternalLink"; - -const STATUS_BAR_HIDDEN = 0; -const STATUS_BAR_EXPANDED = 1; -const STATUS_BAR_EXPANDED_LARGE = 2; - -export function getUnsentMessages(room: Room, threadId?: string): MatrixEvent[] { - if (!room) { - return []; - } - return room.getPendingEvents().filter(function (ev) { - const isNotSent = ev.status === EventStatus.NOT_SENT; - const belongsToTheThread = threadId === ev.threadRootId; - return isNotSent && (!threadId || belongsToTheThread); - }); -} - -interface IProps { - // the room this statusbar is representing. - room: Room; - - // true if the room is being peeked at. This affects components that shouldn't - // logically be shown when peeking, such as a prompt to invite people to a room. - isPeeking?: boolean; - // callback for when the user clicks on the 'resend all' button in the - // 'unsent messages' bar - onResendAllClick?: () => void; - - // callback for when the user clicks on the 'cancel all' button in the - // 'unsent messages' bar - onCancelAllClick?: () => void; - - // callback for when the user clicks on the 'invite others' button in the - // 'you are alone' bar - onInviteClick?: () => void; - - // callback for when we do something that changes the size of the - // status bar. This is used to trigger a re-layout in the parent - // component. - onResize?: () => void; - - // callback for when the status bar can be hidden from view, as it is - // not displaying anything - onHidden?: () => void; - - // callback for when the status bar is displaying something and should - // be visible - onVisible?: () => void; -} - -interface IState { - syncState: SyncState | null; - syncStateData: SyncStateData | null; - unsentMessages: MatrixEvent[]; - isResending: boolean; -} - -export default class RoomStatusBar extends React.PureComponent { - private unmounted = false; - public static contextType = MatrixClientContext; - declare public context: React.ContextType; - - public constructor(props: IProps, context: React.ContextType) { - super(props, context); - - this.state = { - syncState: this.context.getSyncState(), - syncStateData: this.context.getSyncStateData(), - unsentMessages: getUnsentMessages(this.props.room), - isResending: false, - }; - } - - public componentDidMount(): void { - this.unmounted = false; - - const client = this.context; - client.on(ClientEvent.Sync, this.onSyncStateChange); - client.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated); - - this.checkSize(); - } - - public componentDidUpdate(): void { - this.checkSize(); - } - - public componentWillUnmount(): void { - this.unmounted = true; - // we may have entirely lost our client as we're logging out before clicking login on the guest bar... - const client = this.context; - if (client) { - client.removeListener(ClientEvent.Sync, this.onSyncStateChange); - client.removeListener(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated); - } - } - - private onSyncStateChange = (state: SyncState, prevState: SyncState | null, data?: SyncStateData): void => { - if (state === "SYNCING" && prevState === "SYNCING") { - return; - } - if (this.unmounted) return; - this.setState({ - syncState: state, - syncStateData: data ?? null, - }); - }; - - private onResendAllClick = (): void => { - Resend.resendUnsentEvents(this.props.room).then(() => { - this.setState({ isResending: false }); - }); - this.setState({ isResending: true }); - dis.fire(Action.FocusSendMessageComposer); - }; - - private onCancelAllClick = (): void => { - Resend.cancelUnsentEvents(this.props.room); - dis.fire(Action.FocusSendMessageComposer); - }; - - private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room): void => { - if (room.roomId !== this.props.room.roomId) return; - const messages = getUnsentMessages(this.props.room); - this.setState({ - unsentMessages: messages, - isResending: messages.length > 0 && this.state.isResending, - }); - }; - - // Check whether current size is greater than 0, if yes call props.onVisible - private checkSize(): void { - if (this.getSize()) { - if (this.props.onVisible) this.props.onVisible(); - } else { - if (this.props.onHidden) this.props.onHidden(); - } - } - - // We don't need the actual height - just whether it is likely to have - // changed - so we use '0' to indicate normal size, and other values to - // indicate other sizes. - private getSize(): number { - if (this.shouldShowConnectionError()) { - return STATUS_BAR_EXPANDED; - } else if (this.state.unsentMessages.length > 0 || this.state.isResending) { - return STATUS_BAR_EXPANDED_LARGE; - } - return STATUS_BAR_HIDDEN; - } - - private shouldShowConnectionError(): boolean { - // no conn bar trumps the "some not sent" msg since you can't resend without - // a connection! - // There's one situation in which we don't show this 'no connection' bar, and that's - // if it's a resource limit exceeded error: those are shown in the top bar. - const errorIsMauError = Boolean( - this.state.syncStateData && - this.state.syncStateData.error && - this.state.syncStateData.error.name === "M_RESOURCE_LIMIT_EXCEEDED", - ); - return this.state.syncState === "ERROR" && !errorIsMauError; - } - - private getUnsentMessageContent(): JSX.Element { - const unsentMessages = this.state.unsentMessages; - - let title: ReactNode; - - let consentError: MatrixError | null = null; - let resourceLimitError: MatrixError | null = null; - for (const m of unsentMessages) { - if (m.error && m.error.errcode === "M_CONSENT_NOT_GIVEN") { - consentError = m.error; - break; - } else if (m.error && m.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED") { - resourceLimitError = m.error; - break; - } - } - if (consentError) { - title = _t( - "room|status_bar|requires_consent_agreement", - {}, - { - consentLink: (sub) => ( - - {sub} - - ), - }, - ); - } else if (resourceLimitError) { - title = messageForResourceLimitError( - resourceLimitError.data.limit_type, - resourceLimitError.data.admin_contact, - { - "monthly_active_user": _td("room|status_bar|monthly_user_limit_reached"), - "hs_disabled": _td("room|status_bar|homeserver_blocked"), - "": _td("room|status_bar|exceeded_resource_limit"), - }, - ); - } else { - title = _t("room|status_bar|some_messages_not_sent"); - } - - let buttonRow = ( - <> - - - {_t("room|status_bar|delete_all")} - - - - {_t("room|status_bar|retry_all")} - - - ); - if (this.state.isResending) { - buttonRow = ( - <> - - {/* span for css */} - {_t("forward|sending")} - - ); - } - - return ( - - ); - } - - public render(): React.ReactNode { - if (this.shouldShowConnectionError()) { - return ( -
    -
    -
    - -
    -
    - {_t("room|status_bar|server_connectivity_lost_title")} -
    -
    - {_t("room|status_bar|server_connectivity_lost_description")} -
    -
    -
    -
    -
    - ); - } - - if (this.state.unsentMessages.length > 0 || this.state.isResending) { - return this.getUnsentMessageContent(); - } - - return null; - } -} diff --git a/src/components/structures/RoomStatusBarUnsentMessages.tsx b/src/components/structures/RoomStatusBarUnsentMessages.tsx deleted file mode 100644 index 0845f2550a..0000000000 --- a/src/components/structures/RoomStatusBarUnsentMessages.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type ReactElement, type ReactNode } from "react"; - -import { type StaticNotificationState } from "../../stores/notifications/StaticNotificationState"; -import NotificationBadge from "../views/rooms/NotificationBadge"; - -interface RoomStatusBarUnsentMessagesProps { - title: ReactNode; - description?: string; - notificationState: StaticNotificationState; - buttons: ReactElement; -} - -export const RoomStatusBarUnsentMessages = (props: RoomStatusBarUnsentMessagesProps): ReactElement => { - return ( -
    -
    -
    - -
    -
    -
    {props.title}
    - {props.description &&
    {props.description}
    } -
    -
    {props.buttons}
    -
    -
    - ); -}; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 997d86e0dc..7f86f5d28c 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -16,6 +16,7 @@ import React, { type ReactNode, type RefObject, type JSX, + useEffect, } from "react"; import classNames from "classnames"; import { @@ -45,7 +46,7 @@ import { debounce, throttle } from "lodash"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; import { type RoomViewProps } from "@element-hq/element-web-module-api"; -import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { RoomStatusBarView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components"; import shouldHideEvent from "../../shouldHideEvent"; import { _t } from "../../languageHandler"; @@ -92,7 +93,6 @@ import { type IOpts } from "../../createRoom"; import EditorStateTransfer from "../../utils/EditorStateTransfer"; import ErrorDialog from "../views/dialogs/ErrorDialog"; import UploadBar from "./UploadBar"; -import RoomStatusBar from "./RoomStatusBar"; import MessageComposer from "../views/rooms/MessageComposer"; import JumpToBottomButton from "../views/rooms/JumpToBottomButton"; import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; @@ -112,10 +112,8 @@ import { LocalRoom, LocalRoomState } from "../../models/LocalRoom"; import { createRoomFromLocalRoom } from "../../utils/direct-messages"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; import EncryptionEvent from "../views/messages/EncryptionEvent"; -import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; import { type ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload"; -import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages"; import { LargeLoader } from "./LargeLoader"; import { isVideoRoom } from "../../utils/video-rooms"; import { SDKContext } from "../../contexts/SDKContext"; @@ -137,6 +135,7 @@ import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInv import { type FocusMessageSearchPayload } from "../../dispatcher/payloads/FocusMessageSearchPayload.ts"; import { isRoomEncrypted } from "../../hooks/useIsEncrypted"; import { type RoomViewStore } from "../../stores/RoomViewStore.tsx"; +import { RoomStatusBarViewModel } from "../../viewmodels/room/RoomStatusBar.ts"; const DEBUG = false; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; @@ -317,33 +316,11 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { encryptionTile = ; } - const onRetryClicked = (): void => { - // eslint-disable-next-line react-compiler/react-compiler - room.state = LocalRoomState.NEW; - defaultDispatcher.dispatch({ - action: "local_room_event", - roomId: room.roomId, - }); - }; - let statusBar: ReactElement | null = null; let composer: ReactElement | null = null; if (room.isError) { - const buttons = ( - - - {_t("action|retry")} - - ); - - statusBar = ( - - ); + statusBar = ; } else { composer = ( ); } +/** + * Wrap a RoomStatusBarView and ViewModel into one component, for usage with legacy React components. + */ +function RoomStatusBarWrappedView(props: ConstructorParameters[0]): ReactElement { + const vm = useCreateAutoDisposedViewModel(() => new RoomStatusBarViewModel(props)); + useEffect(() => { + // Note: We need to tell the parent component whether the viewmodel expects to render anything + // (see onStatusBarVisible). This is ugly, but works. + if ("onVisible" in props) { + // Initial setup + if (vm.getSnapshot().state !== null) { + props.onVisible(); + } else { + props.onHidden?.(); + } + vm.subscribe(() => { + if (vm.getSnapshot().state !== null) { + props.onVisible?.(); + } else { + props.onHidden?.(); + } + }); + } + }, [vm, props]); + + return ; +} export class RoomView extends React.Component { // We cache the latest computed e2eStatus per room to show as soon as we switch rooms otherwise defaulting to @@ -766,6 +770,15 @@ export class RoomView extends React.Component { newState.search = undefined; } + if ( + room && + this.getMainSplitContentType(room) !== MainSplitContentType.Timeline && + newState.initialEventId !== this.state.initialEventId + ) { + // Ensure the right panel timeline is open to show the linked event + this.context.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline }, true, room.roomId); + } + this.setState(newState as IRoomState); // At this point, newState.roomId could be null (e.g. the alias might not // have been resolved yet) so anything called here must handle this case. @@ -1678,14 +1691,6 @@ export class RoomView extends React.Component { } } - private onInviteClick = (): void => { - // open the room inviter - defaultDispatcher.dispatch({ - action: "view_invite", - roomId: this.getRoomId(), - }); - }; - private onJoinButtonClicked = (): void => { // If the user is a ROU, allow them to transition to a PWLU if (this.context.client?.isGuest()) { @@ -2392,10 +2397,8 @@ export class RoomView extends React.Component { } else if (!this.state.search) { isStatusAreaExpanded = this.state.statusBarVisible; statusBar = ( - diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 1df95439d4..bf43369d9e 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -14,7 +14,10 @@ import { GroupIcon, PlusIcon, RoomIcon, + SettingsSolidIcon, + UserAddIcon, UserProfileSolidIcon, + VideoCallSolidIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -72,7 +75,6 @@ import MainSplit from "./MainSplit"; import RightPanel from "./RightPanel"; import SpaceHierarchy, { showRoom } from "./SpaceHierarchy"; import { type RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; -import { Icon as HashVideoIcon } from "../../../res/img/element-icons/roomlist/hash-video.svg"; import SpacePillButton from "./SpacePillButton.tsx"; interface IProps { @@ -140,7 +142,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => { {videoRoomsEnabled && ( } + icon={} onClick={async (e): Promise => { e.preventDefault(); e.stopPropagation(); @@ -231,6 +233,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { showSpaceInvite(space); }} > + {_t("action|invite")} ); @@ -254,7 +257,9 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { }} title={_t("common|settings")} placement="bottom" - /> + > + + ); } @@ -578,10 +583,8 @@ const SpaceSetupPrivateInvite: React.FC<{
    - showRoomInviteDialog(space.roomId)} - > + showRoomInviteDialog(space.roomId)}> + {_t("create_space|invite_teammates_by_username")}
    diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 7b11b92e9c..efffd49025 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX } from "react"; import classNames from "classnames"; -import { _t, type TranslationKey } from "../../languageHandler"; +import { _t } from "../../languageHandler"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { PosthogScreenTracker, type ScreenName } from "../../PosthogTrackers"; import { type NonEmptyArray } from "../../@types/common"; @@ -93,11 +93,7 @@ function TabLabel({ tab, isActive, showToolip, onClick }: ITab let tabIcon: JSX.Element | undefined; if (tab.icon) { - if (typeof tab.icon === "object") { - tabIcon = tab.icon; - } else if (typeof tab.icon === "string") { - tabIcon = ; - } + tabIcon = tab.icon; } const id = domIDForTabID(tab.id); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 665ca6f677..aeda2237e0 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -29,6 +29,7 @@ import { Thread, ThreadEvent, ReceiptType, + EventStatus, } from "matrix-js-sdk/src/matrix"; import { debounce } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; @@ -1004,6 +1005,7 @@ class TimelinePanel extends React.Component { lastReadEventIndex: number | null, ): lastReadEvent is MatrixEvent { if (!lastReadEvent) return false; + if (lastReadEvent.status === EventStatus.NOT_SENT) return false; // We want to avoid sending out read receipts when we are looking at // events in the past which are before the latest RR. diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index a84af5a504..16275d9d8e 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -8,14 +8,15 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import classNames from "classnames"; -import { Text } from "@vector-im/compound-web"; +import { IconButton, Text } from "@vector-im/compound-web"; import { type EmptyObject } from "matrix-js-sdk/src/matrix"; +import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import ToastStore, { type IToast } from "../../stores/ToastStore"; +import { _t } from "../../languageHandler"; interface IState { toasts: IToast[]; - countSeen: number; } export default class ToastContainer extends React.Component { @@ -23,7 +24,6 @@ export default class ToastContainer extends React.Component super(props); this.state = { toasts: ToastStore.sharedInstance().getToasts(), - countSeen: ToastStore.sharedInstance().getCountSeen(), }; } @@ -39,7 +39,6 @@ export default class ToastContainer extends React.Component private onToastStoreUpdate = (): void => { this.setState({ toasts: ToastStore.sharedInstance().getToasts(), - countSeen: ToastStore.sharedInstance().getCountSeen(), }); }; @@ -50,7 +49,7 @@ export default class ToastContainer extends React.Component let containerClasses; if (totalCount !== 0) { const topToast = this.state.toasts[0]; - const { title, icon, key, component, className, bodyClassName, props } = topToast; + const { title, icon, key, component, className, bodyClassName, onCloseButtonClicked, props } = topToast; const bodyClasses = classNames("mx_Toast_body", bodyClassName); const toastClasses = classNames("mx_Toast_toast", className, { mx_Toast_hasIcon: !!icon, @@ -61,20 +60,27 @@ export default class ToastContainer extends React.Component }); const content = React.createElement(component, toastProps); - let countIndicator; - if ((title && isStacked) || this.state.countSeen > 0) { - countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; - } - let titleElement; if (title) { titleElement = ( -
    - - {title} - - {countIndicator} -
    + <> +
    + + {title} + +
    + {onCloseButtonClicked && ( + + + + )} + ); } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 249a5f456b..6f9b768588 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -15,6 +15,8 @@ import { QrCodeIcon, SettingsSolidIcon, LeaveIcon, + NotificationsSolidIcon, + ThemeIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import { MatrixClientPeg } from "../../MatrixClientPeg"; @@ -50,8 +52,6 @@ import PosthogTrackers from "../../PosthogTrackers"; import { type ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; import { SDKContext } from "../../contexts/SDKContext"; import { shouldShowFeedback } from "../../utils/Feedback"; -import { Icon as DarkLightModeSvg } from "../../../res/img/element-icons/roomlist/dark-light-mode.svg"; -import { Icon as NotificationsIcon } from "../../../res/img/element-icons/notifications.svg"; interface IProps { isPanelCollapsed: boolean; @@ -337,7 +337,7 @@ export default class UserMenu extends React.Component { {homeButton} {linkNewDeviceButton} } + icon={} label={_t("notifications|enable_prompt_toast_title")} onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)} /> @@ -407,7 +407,7 @@ export default class UserMenu extends React.Component { : _t("user_menu|switch_theme_dark") } > - +
    {topSection} diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index a02a807c9b..0b0e07bbeb 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -8,12 +8,13 @@ Please see LICENSE files in the repository root for full details. import React, { type RefObject } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { EventTileBubble } from "@element-hq/web-shared-components"; import type ResizeNotifier from "../../utils/ResizeNotifier"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import RoomHeader from "../views/rooms/RoomHeader/RoomHeader.tsx"; import ScrollPanel from "./ScrollPanel"; -import EventTileBubble from "../views/messages/EventTileBubble"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; import { UnwrappedEventTile } from "../views/rooms/EventTile"; import { _t } from "../../languageHandler"; @@ -43,7 +44,8 @@ export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resize
    } + className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon" title={_t("room|waiting_for_join_title", { brand })} subtitle={_t("room|waiting_for_join_subtitle", { brand })} /> diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index ed3b3d9a20..27676bb561 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { Glass } from "@vector-im/compound-web"; +import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import { Phase, SetupEncryptionStore } from "../../../stores/SetupEncryptionStore"; @@ -92,7 +93,9 @@ export default class CompleteSecurity extends React.Component { onClick={this.onSkipClick} className="mx_CompleteSecurity_skip" aria-label={_t("encryption|verification|after_new_login|skip_verification")} - /> + > + + ); } diff --git a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx index 5f57146fe5..460495e317 100644 --- a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx +++ b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { type ReactNode } from "react"; import { Tooltip } from "@vector-im/compound-web"; -import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { CloseIcon, RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../../languageHandler"; import AccessibleButton from "../../../views/elements/AccessibleButton"; @@ -77,7 +77,9 @@ export const VerifyEmailModal: React.FC = ({ onClick={onCloseClick} className="mx_Dialog_cancelButton" aria-label={_t("dialog_close_label")} - /> + > + + ); }; diff --git a/src/components/structures/grouper/CreationGrouper.tsx b/src/components/structures/grouper/CreationGrouper.tsx index 009f5bdc26..80c9bfedcd 100644 --- a/src/components/structures/grouper/CreationGrouper.tsx +++ b/src/components/structures/grouper/CreationGrouper.tsx @@ -11,14 +11,13 @@ import { EventType, M_BEACON_INFO, type MatrixEvent } from "matrix-js-sdk/src/ma import { KnownMembership } from "matrix-js-sdk/src/types"; import { BaseGrouper } from "./BaseGrouper"; -import { type WrappedEvent } from "../MessagePanel"; +import { SeparatorKind, type WrappedEvent } from "../MessagePanel"; import type MessagePanel from "../MessagePanel"; import DMRoomMap from "../../../utils/DMRoomMap"; import { _t } from "../../../languageHandler"; import DateSeparator from "../../views/messages/DateSeparator"; import NewRoomIntro from "../../views/rooms/NewRoomIntro"; import GenericEventListSummary from "../../views/elements/GenericEventListSummary"; -import { SeparatorKind } from "../../views/messages/TimelineSeparator"; // Wrap initial room creation events into a GenericEventListSummary // Grouping only events sent by the same user that sent the `m.room.create` and only until diff --git a/src/components/structures/grouper/MainGrouper.tsx b/src/components/structures/grouper/MainGrouper.tsx index e686f1aa81..6766f0e5b7 100644 --- a/src/components/structures/grouper/MainGrouper.tsx +++ b/src/components/structures/grouper/MainGrouper.tsx @@ -10,14 +10,13 @@ import React, { type ReactNode } from "react"; import { EventType, type MatrixEvent } from "matrix-js-sdk/src/matrix"; import type MessagePanel from "../MessagePanel"; -import type { WrappedEvent } from "../MessagePanel"; +import { SeparatorKind, type WrappedEvent } from "../MessagePanel"; import { BaseGrouper } from "./BaseGrouper"; import { hasText } from "../../../TextForEvent"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import DateSeparator from "../../views/messages/DateSeparator"; import HistoryTile from "../../views/rooms/HistoryTile"; import EventListSummary from "../../views/elements/EventListSummary"; -import { SeparatorKind } from "../../views/messages/TimelineSeparator"; const groupedStateEvents = [ EventType.RoomMember, diff --git a/src/components/viewmodels/memberlist/tiles/MemberTileViewModel.tsx b/src/components/viewmodels/memberlist/tiles/MemberTileViewModel.tsx index d99c127d85..1cb29029ea 100644 --- a/src/components/viewmodels/memberlist/tiles/MemberTileViewModel.tsx +++ b/src/components/viewmodels/memberlist/tiles/MemberTileViewModel.tsx @@ -15,7 +15,7 @@ import { Action } from "../../../../dispatcher/actions"; import { asyncSome } from "../../../../utils/arrays"; import { getUserDeviceIds } from "../../../../utils/crypto/deviceInfo"; import { type RoomMember } from "../../../../models/rooms/RoomMember"; -import { _t, _td, type TranslationKey } from "../../../../languageHandler"; +import { _t, _td } from "../../../../languageHandler"; import { E2EStatus } from "../../../../utils/ShieldUtils"; interface MemberTileViewModelProps { diff --git a/src/components/viewmodels/right_panel/RoomSummaryCardViewModel.tsx b/src/components/viewmodels/right_panel/RoomSummaryCardViewModel.tsx index 143f3fca18..db3bdb51ac 100644 --- a/src/components/viewmodels/right_panel/RoomSummaryCardViewModel.tsx +++ b/src/components/viewmodels/right_panel/RoomSummaryCardViewModel.tsx @@ -5,7 +5,7 @@ Please see LICENSE files in the repository root for full details. */ import { useEffect, useRef, useState } from "react"; -import { EventType, type JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { EventType, type HistoryVisibility, type JoinRule, type Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; @@ -49,6 +49,10 @@ export interface RoomSummaryCardState { * The join rule of the room, used to display the correct badge and icon */ roomJoinRule: JoinRule; + /** + * The history visibility of the room, used to display the correct badge. + */ + historyVisibility: HistoryVisibility; /** * if it is a video room, it should not display export chat, polls, files, extensions */ @@ -158,9 +162,10 @@ export function useRoomSummaryCardViewModel( const e2eStatus = roomContext.e2eStatus; const isVideoRoom = calcIsVideoRoom(room); - const roomState = useRoomState(room); - // used to check if the room is public or not - const roomJoinRule = roomState.getJoinRule(); + const { historyVisibility, roomJoinRule } = useRoomState(room, (state) => ({ + roomJoinRule: state.getJoinRule(), + historyVisibility: state.getHistoryVisibility(), + })); const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || ""; const pinCount = usePinnedEvents(room).length; // value to check if the user can invite to the room @@ -254,6 +259,7 @@ export function useRoomSummaryCardViewModel( isRoomEncrypted, roomJoinRule, e2eStatus, + historyVisibility, isVideoRoom, alias, isFavorite, diff --git a/src/components/viewmodels/roomlist/MessagePreviewViewModel.tsx b/src/components/viewmodels/roomlist/MessagePreviewViewModel.tsx deleted file mode 100644 index 9e141c1379..0000000000 --- a/src/components/viewmodels/roomlist/MessagePreviewViewModel.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback, useEffect, useState } from "react"; - -import type { Room } from "matrix-js-sdk/src/matrix"; -import { type MessagePreview, MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; -import { useEventEmitter } from "../../../hooks/useEventEmitter"; - -interface MessagePreviewViewState { - /** - * A string representation of the message preview if available. - */ - message?: string; -} - -/** - * View model for rendering a message preview for a given room list item. - * @param room The room for which we're rendering the message preview. - * @see {@link MessagePreviewViewState} for what this view model returns. - */ -export function useMessagePreviewViewModel(room: Room): MessagePreviewViewState { - const [messagePreview, setMessagePreview] = useState(null); - - const updatePreview = useCallback(async (): Promise => { - /** - * The second argument to getPreviewForRoom is a tag id which doesn't really make - * much sense within the context of the new room list. We can pass an empty string - * to match all tags for now but we should remember to actually change the implementation - * in the store once we remove the legacy room list. - */ - const newPreview = await MessagePreviewStore.instance.getPreviewForRoom(room, ""); - setMessagePreview(newPreview); - }, [room]); - - /** - * Update when the message preview has changed for this room. - */ - useEventEmitter(MessagePreviewStore.instance, MessagePreviewStore.getPreviewChangedEventName(room), () => { - updatePreview(); - }); - - /** - * Do an initial fetch of the message preview. - */ - useEffect(() => { - updatePreview(); - }, [updatePreview]); - - return { - message: messagePreview?.text, - }; -} diff --git a/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx b/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx deleted file mode 100644 index 451a4898b7..0000000000 --- a/src/components/viewmodels/roomlist/RoomListHeaderViewModel.tsx +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback } from "react"; -import { JoinRule, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix"; - -import { useFeatureEnabled } from "../../../hooks/useSettings"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import PosthogTrackers from "../../../PosthogTrackers"; -import { Action } from "../../../dispatcher/actions"; -import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; -import { - getMetaSpaceName, - type MetaSpace, - type SpaceKey, - UPDATE_HOME_BEHAVIOUR, - UPDATE_SELECTED_SPACE, -} from "../../../stores/spaces"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; -import { - shouldShowSpaceSettings, - showCreateNewRoom, - showSpaceInvite, - showSpacePreferences, - showSpaceSettings, -} from "../../../utils/space"; -import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { createRoom, hasCreateRoomRights } from "./utils"; -import { type SortOption, useSorter } from "./useSorter"; - -/** - * Hook to get the active space and its title. - */ -function useSpace(): { activeSpace: Room | null; title: string } { - const [spaceKey, activeSpace] = useEventEmitterState<[SpaceKey, Room | null]>( - SpaceStore.instance, - UPDATE_SELECTED_SPACE, - () => [SpaceStore.instance.activeSpace, SpaceStore.instance.activeSpaceRoom], - ); - const spaceName = useTypedEventEmitterState(activeSpace ?? undefined, RoomEvent.Name, () => activeSpace?.name); - const allRoomsInHome = useEventEmitterState( - SpaceStore.instance, - UPDATE_HOME_BEHAVIOUR, - () => SpaceStore.instance.allRoomsInHome, - ); - - const title = spaceName ?? getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome); - - return { - activeSpace, - title, - }; -} - -export interface RoomListHeaderViewState { - /** - * The title of the room list - */ - title: string; - /** - * Whether to display the compose menu - * True if the user can create rooms - */ - displayComposeMenu: boolean; - /** - * Whether to display the space menu - * True if there is an active space - */ - displaySpaceMenu: boolean; - /** - * Whether the user can create rooms - */ - canCreateRoom: boolean; - /** - * Whether the user can create video rooms - */ - canCreateVideoRoom: boolean; - /** - * Whether the user can invite in the active space - */ - canInviteInSpace: boolean; - /** - * Whether the user can access space settings - */ - canAccessSpaceSettings: boolean; - /** - * Create a chat room - * @param e - The click event - */ - createChatRoom: (e: Event) => void; - /** - * Create a room - * @param e - The click event - */ - createRoom: (e: Event) => void; - /** - * Create a video room - */ - createVideoRoom: () => void; - /** - * Open the active space home - */ - openSpaceHome: () => void; - /** - * Display the space invite dialog - */ - inviteInSpace: () => void; - /** - * Open the space preferences - */ - openSpacePreferences: () => void; - /** - * Open the space settings - */ - openSpaceSettings: () => void; - /** - * Change the sort order of the room-list. - */ - sort: (option: SortOption) => void; - /** - * The currently active sort option. - */ - activeSortOption: SortOption; -} - -/** - * View model for the RoomListHeader. - */ -export function useRoomListHeaderViewModel(): RoomListHeaderViewState { - const matrixClient = useMatrixClientContext(); - const { activeSpace, title } = useSpace(); - const isSpaceRoom = Boolean(activeSpace); - - const canCreateRoom = hasCreateRoomRights(matrixClient, activeSpace); - const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms") && canCreateRoom; - const displayComposeMenu = canCreateRoom; - const displaySpaceMenu = isSpaceRoom; - const canInviteInSpace = Boolean( - activeSpace?.getJoinRule() === JoinRule.Public || activeSpace?.canInvite(matrixClient.getSafeUserId()), - ); - const canAccessSpaceSettings = Boolean(activeSpace && shouldShowSpaceSettings(activeSpace)); - - /* Actions */ - - const { activeSortOption, sort } = useSorter(); - - const createChatRoom = useCallback((e: Event) => { - defaultDispatcher.fire(Action.CreateChat); - PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e); - }, []); - - const createRoomMemoized = useCallback( - (e: Event) => { - createRoom(activeSpace); - PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e); - }, - [activeSpace], - ); - - const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms"); - const createVideoRoom = useCallback(() => { - const type = elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo; - if (activeSpace) { - showCreateNewRoom(activeSpace, type); - } else { - defaultDispatcher.dispatch({ - action: Action.CreateRoom, - type, - }); - } - }, [activeSpace, elementCallVideoRoomsEnabled]); - - const openSpaceHome = useCallback(() => { - // openSpaceHome is only available when there is an active space - if (!activeSpace) return; - defaultDispatcher.dispatch({ - action: Action.ViewRoom, - room_id: activeSpace.roomId, - metricsTrigger: undefined, - }); - }, [activeSpace]); - - const inviteInSpace = useCallback(() => { - // inviteInSpace is only available when there is an active space - if (!activeSpace) return; - showSpaceInvite(activeSpace); - }, [activeSpace]); - - const openSpacePreferences = useCallback(() => { - // openSpacePreferences is only available when there is an active space - if (!activeSpace) return; - showSpacePreferences(activeSpace); - }, [activeSpace]); - - const openSpaceSettings = useCallback(() => { - // openSpaceSettings is only available when there is an active space - if (!activeSpace) return; - showSpaceSettings(activeSpace); - }, [activeSpace]); - - return { - title, - displayComposeMenu, - displaySpaceMenu, - canCreateRoom, - canCreateVideoRoom, - canInviteInSpace, - canAccessSpaceSettings, - createChatRoom, - createRoom: createRoomMemoized, - createVideoRoom, - openSpaceHome, - inviteInSpace, - openSpacePreferences, - openSpaceSettings, - activeSortOption, - sort, - }; -} diff --git a/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx deleted file mode 100644 index 738a05b8c3..0000000000 --- a/src/components/viewmodels/roomlist/RoomListItemMenuViewModel.tsx +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback } from "react"; -import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; - -import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import { useUnreadNotifications } from "../../../hooks/useUnreadNotifications"; -import { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils"; -import DMRoomMap from "../../../utils/DMRoomMap"; -import { DefaultTagID } from "../../../stores/room-list/models"; -import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; -import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; -import { UIComponent } from "../../../settings/UIFeature"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { clearRoomNotification, setMarkedUnreadState } from "../../../utils/notifications"; -import PosthogTrackers from "../../../PosthogTrackers"; -import { tagRoom } from "../../../utils/room/tagRoom"; -import { RoomNotifState } from "../../../RoomNotifs"; -import { useNotificationState } from "../../../hooks/useRoomNotificationState"; - -export interface RoomListItemMenuViewState { - /** - * Whether the more options menu should be shown. - */ - showMoreOptionsMenu: boolean; - /** - * Whether the notification menu should be shown. - */ - showNotificationMenu: boolean; - /** - * Whether the room is a favourite room. - */ - isFavourite: boolean; - /** - * Whether the room is a low priority room. - */ - isLowPriority: boolean; - /** - * Can invite other user's in the room. - */ - canInvite: boolean; - /** - * Can copy the room link. - */ - canCopyRoomLink: boolean; - /** - * Can mark the room as read. - */ - canMarkAsRead: boolean; - /** - * Can mark the room as unread. - */ - canMarkAsUnread: boolean; - /** - * Whether the notification is set to all messages. - */ - isNotificationAllMessage: boolean; - /** - * Whether the notification is set to all messages loud. - */ - isNotificationAllMessageLoud: boolean; - /** - * Whether the notification is set to mentions and keywords only. - */ - isNotificationMentionOnly: boolean; - /** - * Whether the notification is muted. - */ - isNotificationMute: boolean; - /** - * Mark the room as read. - * @param evt - */ - markAsRead: (evt: Event) => void; - /** - * Mark the room as unread. - * @param evt - */ - markAsUnread: (evt: Event) => void; - /** - * Toggle the room as favourite. - * @param evt - */ - toggleFavorite: (evt: Event) => void; - /** - * Toggle the room as low priority. - */ - toggleLowPriority: () => void; - /** - * Invite other users in the room. - * @param evt - */ - invite: (evt: Event) => void; - /** - * Copy the room link in the clipboard. - * @param evt - */ - copyRoomLink: (evt: Event) => void; - /** - * Leave the room. - * @param evt - */ - leaveRoom: (evt: Event) => void; - /** - * Set the room notification state. - * @param state - */ - setRoomNotifState: (state: RoomNotifState) => void; -} - -export function useRoomListItemMenuViewModel(room: Room): RoomListItemMenuViewState { - const matrixClient = useMatrixClientContext(); - const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags); - const { level: notificationLevel } = useUnreadNotifications(room); - - const isDm = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId)); - const isFavourite = Boolean(roomTags[DefaultTagID.Favourite]); - const isLowPriority = Boolean(roomTags[DefaultTagID.LowPriority]); - const isArchived = Boolean(roomTags[DefaultTagID.Archived]); - - const showMoreOptionsMenu = hasAccessToOptionsMenu(room); - const showNotificationMenu = hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived); - - const canMarkAsRead = notificationLevel > NotificationLevel.None; - const canMarkAsUnread = !canMarkAsRead && !isArchived; - - const canInvite = - room.canInvite(matrixClient.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers); - const canCopyRoomLink = !isDm; - - const [roomNotifState, setRoomNotifState] = useNotificationState(room); - const isNotificationAllMessage = roomNotifState === RoomNotifState.AllMessages; - const isNotificationAllMessageLoud = roomNotifState === RoomNotifState.AllMessagesLoud; - const isNotificationMentionOnly = roomNotifState === RoomNotifState.MentionsOnly; - const isNotificationMute = roomNotifState === RoomNotifState.Mute; - - // Actions - - const markAsRead = useCallback( - async (evt: Event): Promise => { - await clearRoomNotification(room, matrixClient); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkRead", evt); - }, - [room, matrixClient], - ); - - const markAsUnread = useCallback( - async (evt: Event): Promise => { - await setMarkedUnreadState(room, matrixClient, true); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuMarkUnread", evt); - }, - [room, matrixClient], - ); - - const toggleFavorite = useCallback( - (evt: Event): void => { - tagRoom(room, DefaultTagID.Favourite); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt); - }, - [room], - ); - - const toggleLowPriority = useCallback((): void => tagRoom(room, DefaultTagID.LowPriority), [room]); - - const invite = useCallback( - (evt: Event): void => { - dispatcher.dispatch({ - action: "view_invite", - roomId: room.roomId, - }); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem", evt); - }, - [room], - ); - - const copyRoomLink = useCallback( - (evt: Event): void => { - dispatcher.dispatch({ - action: "copy_room", - room_id: room.roomId, - }); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", evt); - }, - [room], - ); - - const leaveRoom = useCallback( - (evt: Event): void => { - dispatcher.dispatch({ - action: isArchived ? "forget_room" : "leave_room", - room_id: room.roomId, - }); - PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", evt); - }, - [room, isArchived], - ); - - return { - showMoreOptionsMenu, - showNotificationMenu, - isFavourite, - isLowPriority, - canInvite, - canCopyRoomLink, - canMarkAsRead, - canMarkAsUnread, - isNotificationAllMessage, - isNotificationAllMessageLoud, - isNotificationMentionOnly, - isNotificationMute, - markAsRead, - markAsUnread, - toggleFavorite, - toggleLowPriority, - invite, - copyRoomLink, - leaveRoom, - setRoomNotifState, - }; -} diff --git a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx b/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx deleted file mode 100644 index 30576e2dc2..0000000000 --- a/src/components/viewmodels/roomlist/RoomListItemViewModel.tsx +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback, useEffect, useMemo, useState } from "react"; -import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; -import { CallType } from "matrix-js-sdk/src/webrtc/call"; - -import dispatcher from "../../../dispatcher/dispatcher"; -import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { Action } from "../../../dispatcher/actions"; -import { hasAccessToNotificationMenu, hasAccessToOptionsMenu } from "./utils"; -import { _t } from "../../../languageHandler"; -import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState"; -import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; -import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter"; -import { DefaultTagID } from "../../../stores/room-list/models"; -import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall"; -import { CallEvent, type ConnectionState } from "../../../models/Call"; -import { NotificationStateEvents } from "../../../stores/notifications/NotificationState"; -import DMRoomMap from "../../../utils/DMRoomMap"; -import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; -import { useMessagePreviewToggle } from "./useMessagePreviewToggle"; - -export interface RoomListItemViewState { - /** - * The name of the room. - */ - name: string; - /** - * Whether the context menu should be shown. - */ - showContextMenu: boolean; - /** - * Whether the hover menu should be shown. - */ - showHoverMenu: boolean; - /** - * Open the room having given roomId. - */ - openRoom: () => void; - /** - * The a11y label for the room list item. - */ - a11yLabel: string; - /** - * The notification state of the room. - */ - notificationState: RoomNotificationState; - /** - * Whether the room should be bolded. - */ - isBold: boolean; - /** - * Whether the room is a video room - */ - isVideoRoom: boolean; - /** - * The connection state of the call. - * `null` if there is no call in the room. - */ - callConnectionState: ConnectionState | null; - /** - * Whether there are participants in the call. - */ - hasParticipantInCall: boolean; - /** - * Whether the call is a voice or video call. - */ - callType: CallType | undefined; - /** - * Pre-rendered and translated preview for the latest message in the room, or undefined - * if no preview should be shown. - */ - messagePreview: string | undefined; - /** - * Whether the notification decoration should be shown. - */ - showNotificationDecoration: boolean; -} - -/** - * View model for the room list item - * @see {@link RoomListItemViewState} for more information about what this view model returns. - */ -export function useRoomListItemViewModel(room: Room): RoomListItemViewState { - const matrixClient = useMatrixClientContext(); - const roomTags = useEventEmitterState(room, RoomEvent.Tags, () => room.tags); - const isArchived = Boolean(roomTags[DefaultTagID.Archived]); - const name = useEventEmitterState(room, RoomEvent.Name, () => room.name); - - const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]); - - const [a11yLabel, setA11yLabel] = useState(getA11yLabel(name, notificationState)); - const [{ isBold, invited, hasVisibleNotification }, setNotificationValues] = useState( - getNotificationValues(notificationState), - ); - useEffect(() => { - setA11yLabel(getA11yLabel(name, notificationState)); - }, [name, notificationState]); - - // Listen to changes in the notification state and update the values - useTypedEventEmitter(notificationState, NotificationStateEvents.Update, () => { - setA11yLabel(getA11yLabel(name, notificationState)); - setNotificationValues(getNotificationValues(notificationState)); - }); - - // If the notification reference change due to room change, update the values - useEffect(() => { - setNotificationValues(getNotificationValues(notificationState)); - }, [notificationState]); - - // We don't want to show the menus if - // - there is an invitation for this room - // - the user doesn't have access to notification and more options menus - const showContextMenu = !invited && hasAccessToOptionsMenu(room); - const showHoverMenu = - !invited && (showContextMenu || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived)); - - const messagePreview = useRoomMessagePreview(room); - - // Video room - const isVideoRoom = room.isElementVideoRoom() || room.isCallRoom(); - // EC video call or video room - const call = useCall(room.roomId); - const connectionState = useConnectionState(call); - const participantCount = useParticipantCount(call); - const callConnectionState = call ? connectionState : null; - - const showNotificationDecoration = hasVisibleNotification || participantCount > 0; - - // Actions - - const openRoom = useCallback((): void => { - dispatcher.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: "RoomList", - }); - }, [room]); - - const [callType, setCallType] = useState(CallType.Video); - useTypedEventEmitter(call ?? undefined, CallEvent.CallTypeChanged, setCallType); - - return { - name, - notificationState, - showContextMenu, - showHoverMenu, - openRoom, - a11yLabel, - isBold, - isVideoRoom, - callConnectionState, - hasParticipantInCall: participantCount > 0, - messagePreview, - showNotificationDecoration, - callType: call ? callType : undefined, - }; -} - -/** - * Calculate the values from the notification state - * @param notificationState - */ -function getNotificationValues(notificationState: RoomNotificationState): { - computeA11yLabel: (name: string) => string; - isBold: boolean; - invited: boolean; - hasVisibleNotification: boolean; -} { - const invited = notificationState.invited; - const computeA11yLabel = (name: string): string => getA11yLabel(name, notificationState); - const isBold = notificationState.hasAnyNotificationOrActivity; - - const hasVisibleNotification = notificationState.hasAnyNotificationOrActivity || notificationState.muted; - - return { - computeA11yLabel, - isBold, - invited, - hasVisibleNotification, - }; -} - -/** - * Get the a11y label for the room list item - * @param roomName - * @param notificationState - */ -function getA11yLabel(roomName: string, notificationState: RoomNotificationState): string { - if (notificationState.isUnsentMessage) { - return _t("a11y|room_messsage_not_sent", { - roomName, - }); - } else if (notificationState.invited) { - return _t("a11y|room_n_unread_invite", { - roomName, - }); - } else if (notificationState.isMention) { - return _t("a11y|room_n_unread_messages_mentions", { - roomName, - count: notificationState.count, - }); - } else if (notificationState.hasUnreadCount) { - return _t("a11y|room_n_unread_messages", { - roomName, - count: notificationState.count, - }); - } else { - return _t("room_list|room|open_room", { roomName }); - } -} - -function useRoomMessagePreview(room: Room): string | undefined { - const { shouldShowMessagePreview } = useMessagePreviewToggle(); - const [previewText, setPreviewText] = useState(undefined); - - const updatePreview = useCallback(async () => { - if (!shouldShowMessagePreview) { - setPreviewText(undefined); - return; - } - - const roomIsDM = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId)); - // For the tag, we only care about whether the room is a DM or not as we don't show - // display names in previewsd for DMs, so anything else we just say is 'untagged' - // (even though it could actually be have other tags: we don't care about them). - const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom( - room, - roomIsDM ? DefaultTagID.DM : DefaultTagID.Untagged, - ); - setPreviewText(messagePreview?.text); - }, [room, shouldShowMessagePreview]); - - // MessagePreviewStore and the other AsyncStores need to be converted to TypedEventEmitter - useEventEmitter(MessagePreviewStore.instance, MessagePreviewStore.getPreviewChangedEventName(room), () => { - updatePreview(); - }); - - useEffect(() => { - updatePreview(); - }, [updatePreview]); - - return previewText; -} diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx deleted file mode 100644 index a48d973b23..0000000000 --- a/src/components/viewmodels/roomlist/RoomListViewModel.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { useCallback } from "react"; - -import type { Room } from "matrix-js-sdk/src/matrix"; -import { type PrimaryFilter, useFilteredRooms } from "./useFilteredRooms"; -import { createRoom as createRoomFunc, hasCreateRoomRights } from "./utils"; -import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useStickyRoomList } from "./useStickyRoomList"; -import { useRoomListNavigation } from "./useRoomListNavigation"; -import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3"; - -export interface RoomListViewState { - /** - * Whether the list of rooms is being loaded. - */ - isLoadingRooms: boolean; - - /** - * The room results to be displayed (along with the spaceId and filter keys at the time of query) - */ - roomsResult: RoomsResult; - - /** - * Create a chat room - * @param e - The click event - */ - createChatRoom: () => void; - - /** - * Whether the user can create a room in the current space - */ - canCreateRoom: boolean; - - /** - * Create a room - * @param e - The click event - */ - createRoom: () => void; - - /** - * A list of objects that provide the view enough information - * to render primary room filters. - */ - primaryFilters: PrimaryFilter[]; - - /** - * The currently active primary filter. - * If no primary filter is active, this will be undefined. - */ - activePrimaryFilter?: PrimaryFilter; - - /** - * The index of the active room in the room list. - */ - activeIndex: number | undefined; -} - -/** - * View model for the new room list - * @see {@link RoomListViewState} for more information about what this view model returns. - */ -export function useRoomListViewModel(): RoomListViewState { - const matrixClient = useMatrixClientContext(); - const { isLoadingRooms, primaryFilters, activePrimaryFilter, roomsResult: filteredRooms } = useFilteredRooms(); - const { activeIndex, roomsResult } = useStickyRoomList(filteredRooms); - - useRoomListNavigation(roomsResult.rooms); - - const currentSpace = useEventEmitterState( - SpaceStore.instance, - UPDATE_SELECTED_SPACE, - () => SpaceStore.instance.activeSpaceRoom, - ); - const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace); - - const createChatRoom = useCallback(() => dispatcher.fire(Action.CreateChat), []); - const createRoom = useCallback(() => createRoomFunc(currentSpace), [currentSpace]); - - return { - isLoadingRooms, - roomsResult, - canCreateRoom, - createRoom, - createChatRoom, - primaryFilters, - activePrimaryFilter, - activeIndex, - }; -} diff --git a/src/components/viewmodels/roomlist/useFilteredRooms.tsx b/src/components/viewmodels/roomlist/useFilteredRooms.tsx deleted file mode 100644 index 4e311f39db..0000000000 --- a/src/components/viewmodels/roomlist/useFilteredRooms.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { useCallback, useEffect, useMemo, useState } from "react"; - -import { FilterKey } from "../../../stores/room-list-v3/skip-list/filters"; -import { _t, _td, type TranslationKey } from "../../../languageHandler"; -import RoomListStoreV3, { - LISTS_LOADED_EVENT, - LISTS_UPDATE_EVENT, - type RoomsResult, -} from "../../../stores/room-list-v3/RoomListStoreV3"; -import { useEventEmitter } from "../../../hooks/useEventEmitter"; - -/** - * Provides information about a primary filter. - * A primary filter is a commonly used filter that is given - * more precedence in the UI. For eg, primary filters may be - * rendered as pills above the room list. - */ -export interface PrimaryFilter { - // A function to toggle this filter on and off. - toggle: () => void; - // Whether this filter is currently applied - active: boolean; - // Text that can be used in the UI to represent this filter. - name: string; - // The key of the filter - key: FilterKey; -} - -interface FilteredRooms { - primaryFilters: PrimaryFilter[]; - isLoadingRooms: boolean; - roomsResult: RoomsResult; - /** - * The currently active primary filter. - * If no primary filter is active, this will be undefined. - */ - activePrimaryFilter?: PrimaryFilter; -} - -const filterKeyToNameMap: Map = new Map([ - [FilterKey.UnreadFilter, _td("room_list|filters|unread")], - [FilterKey.PeopleFilter, _td("room_list|filters|people")], - [FilterKey.RoomsFilter, _td("room_list|filters|rooms")], - [FilterKey.FavouriteFilter, _td("room_list|filters|favourite")], - [FilterKey.MentionsFilter, _td("room_list|filters|mentions")], - [FilterKey.InvitesFilter, _td("room_list|filters|invites")], - [FilterKey.LowPriorityFilter, _td("room_list|filters|low_priority")], -]); - -/** - * Track available filters and provide a filtered list of rooms. - */ -export function useFilteredRooms(): FilteredRooms { - /** - * Primary filter refers to the pill based filters - * rendered above the room list. - */ - const [primaryFilter, setPrimaryFilter] = useState(); - - const [roomsResult, setRoomsResult] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace()); - const [isLoadingRooms, setIsLoadingRooms] = useState(() => RoomListStoreV3.instance.isLoadingRooms); - - const updateRoomsFromStore = useCallback((filters: FilterKey[] = []): void => { - const newRooms = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filters); - setRoomsResult(newRooms); - }, []); - - const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] => - array.filter((f) => f !== undefined) as FilterKey[]; - - const getAppliedFilters = useCallback((): FilterKey[] => { - return filterUndefined([primaryFilter]); - }, [primaryFilter]); - - useEffect(() => { - // Update the rooms state when the primary filter changes - const filters = getAppliedFilters(); - updateRoomsFromStore(filters); - }, [getAppliedFilters, updateRoomsFromStore]); - - useEventEmitter(RoomListStoreV3.instance, LISTS_UPDATE_EVENT, () => { - const filters = getAppliedFilters(); - updateRoomsFromStore(filters); - }); - - useEventEmitter(RoomListStoreV3.instance, LISTS_LOADED_EVENT, () => { - setIsLoadingRooms(false); - }); - - /** - * This tells the view which primary filters are available, how to toggle them - * and whether a given primary filter is active. @see {@link PrimaryFilter} - */ - const primaryFilters = useMemo(() => { - const createPrimaryFilter = (key: FilterKey, name: string): PrimaryFilter => { - return { - toggle: () => { - setPrimaryFilter((currentFilter) => { - const filter = currentFilter === key ? undefined : key; - updateRoomsFromStore(filterUndefined([filter])); - return filter; - }); - }, - active: primaryFilter === key, - name, - key, - }; - }; - const filters: PrimaryFilter[] = []; - for (const [key, name] of filterKeyToNameMap.entries()) { - filters.push(createPrimaryFilter(key, _t(name))); - } - return filters; - }, [primaryFilter, updateRoomsFromStore]); - - const activePrimaryFilter = useMemo(() => primaryFilters.find((filter) => filter.active), [primaryFilters]); - - return { - isLoadingRooms, - primaryFilters, - activePrimaryFilter, - roomsResult, - }; -} diff --git a/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx b/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx deleted file mode 100644 index efb58b3e04..0000000000 --- a/src/components/viewmodels/roomlist/useMessagePreviewToggle.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ -import { useCallback } from "react"; - -import SettingsStore from "../../../settings/SettingsStore"; -import { SettingLevel } from "../../../settings/SettingLevel"; -import { useSettingValue } from "../../../hooks/useSettings"; - -interface MessagePreviewToggleState { - shouldShowMessagePreview: boolean; - toggleMessagePreview: () => void; -} - -/** - * This hook: - * - Provides a state that tracks whether message previews are turned on or off. - * - Provides a function to toggle message previews. - */ -export function useMessagePreviewToggle(): MessagePreviewToggleState { - const shouldShowMessagePreview = useSettingValue("RoomList.showMessagePreview"); - - const toggleMessagePreview = useCallback((): void => { - const toggled = !shouldShowMessagePreview; - SettingsStore.setValue("RoomList.showMessagePreview", null, SettingLevel.DEVICE, toggled); - }, [shouldShowMessagePreview]); - - return { toggleMessagePreview, shouldShowMessagePreview }; -} diff --git a/src/components/viewmodels/roomlist/useRoomListNavigation.ts b/src/components/viewmodels/roomlist/useRoomListNavigation.ts deleted file mode 100644 index 5ef979e79c..0000000000 --- a/src/components/viewmodels/roomlist/useRoomListNavigation.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { type Room } from "matrix-js-sdk/src/matrix"; - -import dispatcher from "../../../dispatcher/dispatcher"; -import { useDispatcher } from "../../../hooks/useDispatcher"; -import { Action } from "../../../dispatcher/actions"; -import { type ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload"; -import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; - -/** - * Hook to navigate the room list using keyboard shortcuts. - * It listens to the ViewRoomDelta action and updates the room list accordingly. - * @param rooms - */ -export function useRoomListNavigation(rooms: Room[]): void { - useDispatcher(dispatcher, (payload) => { - if (payload.action !== Action.ViewRoomDelta) return; - const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); - if (!roomId) return; - - const { delta, unread } = payload as ViewRoomDeltaPayload; - const filteredRooms = unread - ? // Filter the rooms to only include unread ones and the active room - rooms.filter((room) => { - const state = RoomNotificationStateStore.instance.getRoomState(room); - return room.roomId === roomId || state.isUnread; - }) - : rooms; - - const currentIndex = filteredRooms.findIndex((room) => room.roomId === roomId); - if (currentIndex === -1) return; - - // Get the next/previous new room according to the delta - // Use slice to loop on the list - // If delta is -1 at the start of the list, it will go to the end - // If delta is 1 at the end of the list, it will go to the start - const [newRoom] = filteredRooms.slice((currentIndex + delta) % filteredRooms.length); - if (!newRoom) return; - - dispatcher.dispatch({ - action: Action.ViewRoom, - room_id: newRoom.roomId, - show_room_tile: true, // to make sure the room gets scrolled into view - metricsTrigger: "WebKeyboardShortcut", - metricsViaKeyboard: true, - }); - }); -} diff --git a/src/components/viewmodels/roomlist/useSorter.ts b/src/components/viewmodels/roomlist/useSorter.ts deleted file mode 100644 index c7a880d430..0000000000 --- a/src/components/viewmodels/roomlist/useSorter.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2025 New Vector Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ -import { useState } from "react"; - -import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3"; -import { SortingAlgorithm } from "../../../stores/room-list-v3/skip-list/sorters"; -import SettingsStore from "../../../settings/SettingsStore"; - -/** - * Sorting options made available to the view. - */ -export const enum SortOption { - Activity = SortingAlgorithm.Recency, - AToZ = SortingAlgorithm.Alphabetic, -} - -/** - * {@link SortOption} holds almost the same information as - * {@link SortingAlgorithm}. This is done intentionally to - * prevent the view from having a dependence on the - * model (which is the store in this case). - */ -const sortingAlgorithmToSortingOption = { - [SortingAlgorithm.Alphabetic]: SortOption.AToZ, - [SortingAlgorithm.Recency]: SortOption.Activity, -}; - -const sortOptionToSortingAlgorithm = { - [SortOption.AToZ]: SortingAlgorithm.Alphabetic, - [SortOption.Activity]: SortingAlgorithm.Recency, -}; - -interface SortState { - sort: (option: SortOption) => void; - activeSortOption: SortOption; -} - -/** - * This hook does two things: - * - Provides a way to track the currently active sort option. - * - Provides a function to resort the room list. - */ -export function useSorter(): SortState { - const [activeSortingAlgorithm, setActiveSortingAlgorithm] = useState(() => - SettingsStore.getValue("RoomList.preferredSorting"), - ); - - const sort = (option: SortOption): void => { - const sortingAlgorithm = sortOptionToSortingAlgorithm[option]; - RoomListStoreV3.instance.resort(sortingAlgorithm); - setActiveSortingAlgorithm(sortingAlgorithm); - }; - - return { - sort, - activeSortOption: sortingAlgorithmToSortingOption[activeSortingAlgorithm!], - }; -} diff --git a/src/components/viewmodels/roomlist/useStickyRoomList.tsx b/src/components/viewmodels/roomlist/useStickyRoomList.tsx deleted file mode 100644 index 355e09a292..0000000000 --- a/src/components/viewmodels/roomlist/useStickyRoomList.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback, useEffect, useRef, useState } from "react"; - -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { useDispatcher } from "../../../hooks/useDispatcher"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import type { Room } from "matrix-js-sdk/src/matrix"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; -import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3"; - -function getIndexByRoomId(rooms: Room[], roomId: string): number | undefined { - const index = rooms.findIndex((room) => room.roomId === roomId); - return index === -1 ? undefined : index; -} - -function getRoomsWithStickyRoom( - rooms: Room[], - oldIndex: number | undefined, - newIndex: number | undefined, - isRoomChange: boolean, -): { newRooms: Room[]; newIndex: number | undefined } { - const updated = { newIndex, newRooms: rooms }; - if (isRoomChange) { - /* - * When opening another room, the index should obviously change. - */ - return updated; - } - if (newIndex === undefined || oldIndex === undefined) { - /* - * If oldIndex is undefined, then there was no active room before. - * So nothing to do in regards to sticky room. - * Similarly, if newIndex is undefined, there's no active room anymore. - */ - return updated; - } - if (newIndex === oldIndex) { - /* - * If the index hasn't changed, we have nothing to do. - */ - return updated; - } - if (oldIndex > rooms.length - 1) { - /* - * If the old index falls out of the bounds of the rooms array - * (usually because rooms were removed), we can no longer place - * the active room in the same old index. - */ - return updated; - } - - /* - * Making the active room sticky is as simple as removing it from - * its new index and placing it in the old index. - */ - const newRooms = [...rooms]; - const [newRoom] = newRooms.splice(newIndex, 1); - newRooms.splice(oldIndex, 0, newRoom); - - return { newIndex: oldIndex, newRooms }; -} - -export interface StickyRoomListResult { - /** - * The rooms result with the active sticky room applied - */ - roomsResult: RoomsResult; - /** - * Index of the active room in the room list. - */ - activeIndex: number | undefined; -} - -/** - * - Provides a list of rooms such that the active room is sticky i.e the active room is kept - * in the same index even when the order of rooms in the list changes. - * - Provides the index of the active room. - * @param rooms list of rooms - * @see {@link StickyRoomListResult} details what this hook returns.. - */ -export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResult { - const [listState, setListState] = useState({ - activeIndex: getIndexByRoomId(roomsResult.rooms, SdkContextClass.instance.roomViewStore.getRoomId()!), - roomsResult: roomsResult, - }); - - const currentSpaceRef = useRef(SpaceStore.instance.activeSpace); - - const updateRoomsAndIndex = useCallback( - (newRoomId: string | null, isRoomChange: boolean = false) => { - setListState((current) => { - const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId(); - const newActiveIndex = getIndexByRoomId(roomsResult.rooms, activeRoomId!); - const oldIndex = current.activeIndex; - const { newIndex, newRooms } = getRoomsWithStickyRoom( - roomsResult.rooms, - oldIndex, - newActiveIndex, - isRoomChange, - ); - return { activeIndex: newIndex, roomsResult: { ...roomsResult, rooms: newRooms } }; - }); - }, - [roomsResult], - ); - - // Re-calculate the index when the active room has changed. - useDispatcher(dispatcher, (payload) => { - if (payload.action === Action.ActiveRoomChanged) updateRoomsAndIndex(payload.newRoomId, true); - }); - - // Re-calculate the index when the list of rooms has changed. - useEffect(() => { - let newRoomId: string | null = null; - let isRoomChange = false; - if (currentSpaceRef.current !== roomsResult.spaceId) { - /* - If the space has changed, we check if we can immediately set the active - index to the last opened room in that space. Otherwise, we might see a - flicker because of the delay between the space change event and - active room change dispatch. - */ - newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(roomsResult.spaceId); - isRoomChange = true; - currentSpaceRef.current = roomsResult.spaceId; - } - updateRoomsAndIndex(newRoomId, isRoomChange); - }, [roomsResult, updateRoomsAndIndex]); - - return listState; -} diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx index fb420ed459..a8388f7ad9 100644 --- a/src/components/views/auth/EmailField.tsx +++ b/src/components/views/auth/EmailField.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React, { type ComponentProps, PureComponent, type Ref } from "react"; import Field, { type IInputProps } from "../elements/Field"; -import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation"; import * as Email from "../../../email"; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 6f1e74b63d..49443bdd2e 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -951,7 +951,7 @@ export class FallbackAuthEntry extends React.Component { - public static LOGIN_TYPE = CustomAuthType.MasCrossSigningReset; + public static readonly LOGIN_TYPE = AuthType.OAuth; + public static readonly UNSTABLE_LOGIN_TYPE = CustomAuthType.MasCrossSigningReset; private onGoToAccountClick = (): void => { if (!this.props.stageParams?.url) return; @@ -1017,6 +1018,7 @@ export interface IStageComponent extends React.ComponentClass { id?: string; diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index 938e559fd7..d5cd27e401 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -12,7 +12,7 @@ import classNames from "classnames"; import type { ZxcvbnResult } from "@zxcvbn-ts/core"; import SdkConfig from "../../../SdkConfig"; import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation"; -import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import Field, { type IInputProps } from "../elements/Field"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 321c0501dc..ac7206ae59 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -19,6 +19,7 @@ import { } from "matrix-js-sdk/src/matrix"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { Tooltip } from "@vector-im/compound-web"; +import { PublicIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import RoomAvatar from "./RoomAvatar"; import NotificationBadge from "../rooms/NotificationBadge"; @@ -45,6 +46,7 @@ interface IProps { tooltipProps?: { tabIndex?: number; }; + className?: string; } interface IState { @@ -185,7 +187,8 @@ export default class DecoratedRoomAvatar extends React.PureComponent + > + {this.state.icon === Icon.Globe ? : null} +
    ); } - const classes = classNames("mx_DecoratedRoomAvatar", { - mx_DecoratedRoomAvatar_cutout: icon, - }); + const classes = classNames( + "mx_DecoratedRoomAvatar", + { + mx_DecoratedRoomAvatar_cutout: icon, + }, + className, + ); return (
    diff --git a/src/components/views/beacon/RoomCallBanner.tsx b/src/components/views/beacon/RoomCallBanner.tsx index 25109a0a5a..e8b6895720 100644 --- a/src/components/views/beacon/RoomCallBanner.tsx +++ b/src/components/views/beacon/RoomCallBanner.tsx @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import React, { useCallback } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import { VideoCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; @@ -66,7 +67,10 @@ const RoomCallBannerInner: React.FC = ({ roomId, call }) => return (
    - {_t("voip|video_call")} + + + {_t("voip|video_call")} +
    diff --git a/src/components/views/composer/HistoryVisibleBanner.tsx b/src/components/views/composer/HistoryVisibleBanner.tsx deleted file mode 100644 index 85a6bd7fb9..0000000000 --- a/src/components/views/composer/HistoryVisibleBanner.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { HistoryVisibleBannerView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components"; -import React from "react"; -import { type Room } from "matrix-js-sdk/src/matrix"; - -import { HistoryVisibleBannerViewModel } from "../../../viewmodels/composer/HistoryVisibleBannerViewModel"; - -/** Wrapper around {@link HistoryVisibleBannerViewModel} for the creation of an auto-disposed view model. */ -export const HistoryVisibleBanner: React.FC<{ - /** The room instance associated with this banner view model. */ - room: Room; - - /** Whether the current user can send messages in the room. */ - canSendMessages: boolean; - - /** - * If not null, specifies the ID of the thread currently being viewed in the thread timeline side view, - * where the banner view is displayed as a child of the message composer. - */ - threadId: string | null; -}> = (props) => { - const vm = useCreateAutoDisposedViewModel(() => new HistoryVisibleBannerViewModel(props)); - return ; -}; diff --git a/src/components/views/context_menus/DeviceContextMenu.tsx b/src/components/views/context_menus/DeviceContextMenu.tsx index 7560b4a7d5..e780449f12 100644 --- a/src/components/views/context_menus/DeviceContextMenu.tsx +++ b/src/components/views/context_menus/DeviceContextMenu.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useState } from "react"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler"; import IconizedContextMenu, { IconizedContextMenuOptionList, IconizedContextMenuRadio } from "./IconizedContextMenu"; import { type IProps as IContextMenuProps } from "../../structures/ContextMenu"; -import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; const SECTION_NAMES: Record = { [MediaDeviceKindEnum.AudioInput]: _td("voip|input_devices"), diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 004c86ff72..b3cc6a21fe 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { createRef } from "react"; import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; import ContextMenu, { type IProps as IContextMenuProps } from "../../structures/ContextMenu"; @@ -65,7 +66,9 @@ export default class DialpadContextMenu extends React.Component
    - + + +
    if (relatedEventId && SettingsStore.getValue("developerMode")) { jumpToRelatedEventButton = ( } + icon={} label={_t("timeline|context_menu|view_related_event")} onClick={() => this.onJumpToRelatedEventClick(relatedEventId)} /> @@ -691,7 +690,7 @@ export default class MessageContextMenu extends React.Component if (isThreadRootEvent) { viewInRoomButton = ( } + icon={} label={_t("timeline|mab|view_in_room")} onClick={this.viewInRoom} /> diff --git a/src/components/views/context_menus/RoomNotificationContextMenu.tsx b/src/components/views/context_menus/RoomNotificationContextMenu.tsx index 9ad381736a..bcf240dddd 100644 --- a/src/components/views/context_menus/RoomNotificationContextMenu.tsx +++ b/src/components/views/context_menus/RoomNotificationContextMenu.tsx @@ -20,10 +20,6 @@ import IconizedContextMenu, { IconizedContextMenuRadio, } from "../context_menus/IconizedContextMenu"; import { type ButtonEvent } from "../elements/AccessibleButton"; -import { Icon as NotificationsIcon } from "../../../../res/img/element-icons/notifications.svg"; -import { Icon as NotificationsDefaultIcon } from "../../../../res/img/element-icons/roomlist/notifications-default.svg"; -import { Icon as NotificationsDmIcon } from "../../../../res/img/element-icons/roomlist/notifications-dm.svg"; -import { Icon as NotificationsOffIcon } from "../../../../res/img/element-icons/roomlist/notifications-off.svg"; interface IProps extends IContextMenuProps { room: Room; @@ -50,7 +46,6 @@ export const RoomNotificationContextMenu: React.FC = ({ room, onFinished } onClick={wrapHandler(() => setNotificationState(RoomNotifState.AllMessages))} /> ); @@ -59,7 +54,6 @@ export const RoomNotificationContextMenu: React.FC = ({ room, onFinished } onClick={wrapHandler(() => setNotificationState(RoomNotifState.AllMessagesLoud))} /> ); @@ -68,7 +62,6 @@ export const RoomNotificationContextMenu: React.FC = ({ room, onFinished } onClick={wrapHandler(() => setNotificationState(RoomNotifState.MentionsOnly))} /> ); @@ -77,7 +70,6 @@ export const RoomNotificationContextMenu: React.FC = ({ room, onFinished } onClick={wrapHandler(() => setNotificationState(RoomNotifState.Mute))} /> ); diff --git a/src/components/views/context_menus/ThreadListContextMenu.tsx b/src/components/views/context_menus/ThreadListContextMenu.tsx index 11292f2a4f..2b3ee57653 100644 --- a/src/components/views/context_menus/ThreadListContextMenu.tsx +++ b/src/components/views/context_menus/ThreadListContextMenu.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { useCallback, useEffect } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { LinkIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { LinkIcon, OverflowHorizontalIcon, VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { type ButtonEvent } from "../elements/AccessibleButton"; import dis from "../../../dispatcher/dispatcher"; @@ -21,7 +21,6 @@ import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOpti import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { Icon as ViewInRoomIcon } from "../../../../res/img/element-icons/view-in-room.svg"; export interface ThreadListContextMenuProps { mxEvent: MatrixEvent; @@ -90,7 +89,9 @@ const ThreadListContextMenu: React.FC = ({ isExpanded={menuDisplayed} ref={button} data-testid="threadlist-dropdown-button" - /> + > + + {menuDisplayed && ( = ({ viewInRoom(e)} label={_t("timeline|mab|view_in_room")} - icon={} + icon={} /> )} {permalinkCreator && ( diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index ff74e25d38..4ba300370f 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -84,26 +84,6 @@ const showMoveButtons = (app: IWidget, room: Room | undefined, showUnpin: boolea return [widgetIndex > 0, widgetIndex < pinnedWidgets.length - 1]; }; -export const showContextMenu = ( - cli: MatrixClient, - room: Room | undefined, - app: IWidget, - userWidget: boolean, - showUnpin: boolean, - onDeleteClick: (() => void) | undefined, -): boolean => { - const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, room?.roomId); - const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app)); - return ( - showStreamAudioStreamButton(app) || - showEditButton(app, canModify) || - showRevokeButton(cli, room?.roomId, app, userWidget) || - showDeleteButton(canModify, onDeleteClick) || - showSnapshotButton(widgetMessaging) || - showMoveButtons(app, room, showUnpin).some(Boolean) - ); -}; - export const WidgetContextMenu: React.FC = ({ onFinished, app, diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index c247c3aea9..d60e3edab6 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -12,9 +12,9 @@ import { type Room, EventType } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { sleep } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; -import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { CheckIcon, ErrorIcon, RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; -import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; @@ -241,6 +241,7 @@ export const AddExistingToSpace: React.FC = ({ + {_t("action|retry")} @@ -424,6 +425,7 @@ export const SubspaceSelector: React.FC = ({ title, spac
    {space.name || getDisplayAliasForRoom(space) || space.roomId} + {space === value ? : null}
    ); }) as NonEmptyArray diff --git a/src/components/views/dialogs/AnalyticsLearnMoreDialog.tsx b/src/components/views/dialogs/AnalyticsLearnMoreDialog.tsx index d80f03b7d4..77ef5d522f 100644 --- a/src/components/views/dialogs/AnalyticsLearnMoreDialog.tsx +++ b/src/components/views/dialogs/AnalyticsLearnMoreDialog.tsx @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; +import { CheckCircleIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import BaseDialog from "./BaseDialog"; import { _t } from "../../../languageHandler"; @@ -72,9 +73,18 @@ export const AnalyticsLearnMoreDialog: React.FC = ({ {_t("analytics|pseudonymous_usage_data", { analyticsOwner })}
      -
    • {_t("analytics|bullet_1", {}, { Bold: (sub) => {sub} })}
    • -
    • {_t("analytics|bullet_2", {}, { Bold: (sub) => {sub} })}
    • -
    • {_t("analytics|disable_prompt")}
    • +
    • + + {_t("analytics|bullet_1", {}, { Bold: (sub) => {sub} })} +
    • +
    • + + {_t("analytics|bullet_2", {}, { Bold: (sub) => {sub} })} +
    • +
    • + + {_t("analytics|disable_prompt")} +
    {privacyPolicyLink}
    diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 8eba4d3fc6..4c08c80560 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -13,6 +13,7 @@ import FocusLock from "react-focus-lock"; import classNames from "classnames"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { I18nContext } from "@element-hq/web-shared-components"; +import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import AccessibleButton from "../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -134,7 +135,9 @@ export default class BaseDialog extends React.Component { title={_t("action|close")} aria-label={_t("dialog_close_label")} placement="bottom" - /> + > + + ); } diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 83e6da8dfe..2977570957 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -1,4 +1,5 @@ /* +Copyright 2026 Element Creations Ltd. Copyright 2024 New Vector Ltd. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 The Matrix.org Foundation C.I.C. @@ -10,7 +11,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { type JSX, type ReactNode } from "react"; -import { Link } from "@vector-im/compound-web"; +import { Link, Text } from "@vector-im/compound-web"; import SdkConfig from "../../../SdkConfig"; import Modal from "../../../Modal"; @@ -26,6 +27,7 @@ import { sendSentryReport } from "../../../sentry"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { getBrowserSupport } from "../../../SupportedBrowser"; +import { BugReportEndpointURLLocal } from "../../../IConfigOptions"; export interface BugReportDialogProps { onFinished: (success: boolean) => void; @@ -48,6 +50,7 @@ interface IState { export default class BugReportDialog extends React.Component { private unmounted: boolean; private issueRef: React.RefObject; + private readonly isLocalOnly: boolean; public constructor(props: BugReportDialogProps) { super(props); @@ -65,6 +68,8 @@ export default class BugReportDialog extends React.Component support === false)) || !getBrowserSupport() ) { - warning = ( -

    - {_t("bug_reporting|unsupported_browser")} -

    - ); + warning = {_t("bug_reporting|unsupported_browser")}; } return (
    {warning} -

    {_t("bug_reporting|description")}

    -

    - - {_t( - "bug_reporting|before_submitting", - {}, - { - a: (sub) => ( - - {sub} - - ), - }, - )} - -

    + {_t("bug_reporting|description")} + {this.isLocalOnly ? ( + <>{this.state.downloadProgress && {this.state.downloadProgress} ...} + ) : ( + <> + + {_t( + "bug_reporting|before_submitting", + {}, + { + a: (sub) => ( + + {sub} + + ), + }, + )} + -
    - - {_t("bug_reporting|download_logs")} - - {this.state.downloadProgress && {this.state.downloadProgress} ...} -
    +
    + + {_t("bug_reporting|download_logs")} + + {this.state.downloadProgress && {this.state.downloadProgress} ...} +
    - - - {progress} - {error} + + + {progress} + {error} + + )}
    ); diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 5ac0478012..10986095ea 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, useState } from "react"; import { Form } from "@vector-im/compound-web"; -import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import BaseDialog from "./BaseDialog"; import { TimelineEventEditor } from "./devtools/Event"; diff --git a/src/components/views/dialogs/FeedbackDialog.tsx b/src/components/views/dialogs/FeedbackDialog.tsx index 1775e9667a..40814d615a 100644 --- a/src/components/views/dialogs/FeedbackDialog.tsx +++ b/src/components/views/dialogs/FeedbackDialog.tsx @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { type JSX, useEffect, useRef, useState } from "react"; -import { ChatSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { ChatSolidIcon, BugIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import QuestionDialog from "./QuestionDialog"; import { _t } from "../../../languageHandler"; @@ -20,7 +20,6 @@ import { submitFeedback } from "../../../rageshake/submit-rageshake"; import { useStateToggle } from "../../../hooks/useStateToggle"; import StyledCheckbox from "../elements/StyledCheckbox"; import ExternalLink from "../elements/ExternalLink"; -import { Icon as BugIcon } from "../../../../res/img/feather-customised/bug.svg"; interface IProps { feature?: string; @@ -46,6 +45,7 @@ const FeedbackDialog: React.FC = (props: IProps) => { const onFinished = (sendFeedback: boolean): void => { if (hasFeedback && sendFeedback) { const label = props.feature ? `${props.feature}-feedback` : "feedback"; + // TODO: Handle rejection. submitFeedback(label, comment, canContact); Modal.createDialog(InfoDialog, { diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 6b4f737fa0..19c84bc108 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -23,6 +23,7 @@ import { type TimelineEvents, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; +import { CheckCircleIcon, CircleIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; @@ -127,12 +128,12 @@ const Entry: React.FC> = ({ room, type, content, matrixClient: className = "mx_ForwardList_sending"; disabled = true; title = _t("forward|sending"); - icon =
    ; + icon = ; } else if (sendState === SendState.Sent) { className = "mx_ForwardList_sent"; disabled = true; title = _t("forward|sent"); - icon =
    ; + icon = ; } else { className = "mx_ForwardList_sendFailed"; disabled = true; diff --git a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx index 3967de7c4d..8df5b8dbb4 100644 --- a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx +++ b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx @@ -38,7 +38,7 @@ const GenericFeatureFeedbackDialog: React.FC = ({ const sendFeedback = async (ok: boolean): Promise => { if (!ok) return onFinished(false); - + // TODO: Handle rejection. submitFeedback(rageshakeLabel, comment, canContact, rageshakeData); onFinished(true); diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 5e9e660967..10e4a3e428 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -18,6 +18,8 @@ import { SettingsIcon, VoiceCallIcon, NotificationsIcon, + AdvancedSettingsIcon, + TreeIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import TabbedView, { Tab } from "../../structures/TabbedView"; @@ -40,8 +42,6 @@ import { type NonEmptyArray } from "../../../@types/common"; import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab"; import ErrorBoundary from "../elements/ErrorBoundary"; import { PeopleRoomSettingsTab } from "../settings/tabs/room/PeopleRoomSettingsTab"; -import { Icon as AdvancedIcon } from "../../../../res/img/element-icons/room/settings/advanced.svg"; -import { Icon as BridgeIcon } from "../../../../res/img/feather-customised/bridge.svg"; export const enum RoomSettingsTab { General = "ROOM_GENERAL_TAB", @@ -201,7 +201,7 @@ class RoomSettingsDialog extends React.Component { new Tab( RoomSettingsTab.Bridges, _td("room_settings|bridges|title"), - , + , , "RoomSettingsBridges", ), @@ -222,7 +222,7 @@ class RoomSettingsDialog extends React.Component { new Tab( RoomSettingsTab.Advanced, _td("common|advanced"), - , + , this.props.onFinished(true)} diff --git a/src/components/views/dialogs/ScrollableBaseModal.tsx b/src/components/views/dialogs/ScrollableBaseModal.tsx index a379b90d5f..9f4ad52dd3 100644 --- a/src/components/views/dialogs/ScrollableBaseModal.tsx +++ b/src/components/views/dialogs/ScrollableBaseModal.tsx @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import React, { type FormEvent } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import FocusLock from "react-focus-lock"; +import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -91,7 +92,9 @@ export default abstract class ScrollableBaseModal< onClick={this.onCancel} className="mx_CompoundDialog_cancelButton" aria-label={_t("dialog_close_label")} - /> + > + +
    {this.renderContent()}
    diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index 336dee0c87..7566cbb9c8 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -8,7 +8,12 @@ Please see LICENSE files in the repository root for full details. import React, { useMemo } from "react"; import { type Room, type MatrixClient } from "matrix-js-sdk/src/matrix"; -import { AdminIcon, SettingsSolidIcon, VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { + AdminIcon, + AdvancedSettingsIcon, + SettingsSolidIcon, + VisibilityOnIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t, _td } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; @@ -23,7 +28,6 @@ import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsT import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab"; import { Action } from "../../../dispatcher/actions"; import { type NonEmptyArray } from "../../../@types/common"; -import { Icon as AdvancedIcon } from "../../../../res/img/element-icons/room/settings/advanced.svg"; export enum SpaceSettingsTab { General = "SPACE_GENERAL_TAB", @@ -69,7 +73,7 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin ? new Tab( SpaceSettingsTab.Advanced, _td("common|advanced"), - , + , , ) : null, diff --git a/src/components/views/dialogs/TextInputDialog.tsx b/src/components/views/dialogs/TextInputDialog.tsx index 1a86a40d14..ed076bc472 100644 --- a/src/components/views/dialogs/TextInputDialog.tsx +++ b/src/components/views/dialogs/TextInputDialog.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React, { type ChangeEvent, createRef } from "react"; import Field from "../elements/Field"; -import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import { type IFieldState, type IValidationResult } from "../elements/Validation"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; diff --git a/src/components/views/dialogs/devtools/AccountData.tsx b/src/components/views/dialogs/devtools/AccountData.tsx index d4dfe039fd..6900a7097e 100644 --- a/src/components/views/dialogs/devtools/AccountData.tsx +++ b/src/components/views/dialogs/devtools/AccountData.tsx @@ -14,7 +14,7 @@ import BaseTool, { DevtoolsContext, type IDevtoolsProps } from "./BaseTool"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import { EventEditor, EventViewer, eventTypeField, type IEditorProps, stringify } from "./Event"; import FilteredList from "./FilteredList"; -import { _td, type TranslationKey } from "../../../../languageHandler"; +import { _td } from "../../../../languageHandler"; export const AccountDataEventEditor: React.FC = ({ mxEvent, onBack }) => { const cli = useContext(MatrixClientContext); diff --git a/src/components/views/dialogs/devtools/BaseTool.tsx b/src/components/views/dialogs/devtools/BaseTool.tsx index 8a923e2623..555d5b31ff 100644 --- a/src/components/views/dialogs/devtools/BaseTool.tsx +++ b/src/components/views/dialogs/devtools/BaseTool.tsx @@ -11,7 +11,7 @@ import React, { createContext, type ReactNode, useState } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; -import { _t, type TranslationKey } from "../../../../languageHandler"; +import { _t } from "../../../../languageHandler"; import { type XOR } from "../../../../@types/common"; import { type Tool } from "../DevtoolsDialog"; diff --git a/src/components/views/dialogs/devtools/Event.tsx b/src/components/views/dialogs/devtools/Event.tsx index 00669cd614..63712b28d9 100644 --- a/src/components/views/dialogs/devtools/Event.tsx +++ b/src/components/views/dialogs/devtools/Event.tsx @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import React, { type ChangeEvent, type ReactNode, useContext, useMemo, useRef, useState } from "react"; import { type IContent, type MatrixEvent, type TimelineEvents } from "matrix-js-sdk/src/matrix"; -import { _t, _td, type TranslationKey } from "../../../../languageHandler"; +import { _t, _td } from "../../../../languageHandler"; import Field from "../../elements/Field"; import BaseTool, { DevtoolsContext, type IDevtoolsProps } from "./BaseTool"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index f1f81a2bf2..1b241f4c23 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -10,9 +10,10 @@ import { Button, PasswordInput } from "@vector-im/compound-web"; import LockSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid"; import { debounce } from "lodash"; import classNames from "classnames"; -import React, { type ChangeEvent, type FormEvent } from "react"; +import React, { type ChangeEvent, type FormEvent, type ReactNode } from "react"; import { type SecretStorage } from "matrix-js-sdk/src/matrix"; import { Flex } from "@element-hq/web-shared-components"; +import { ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../../languageHandler"; import { EncryptionCard } from "../../settings/encryption/EncryptionCard"; @@ -142,27 +143,29 @@ export default class AccessSecretStorageDialog extends React.PureComponent + + {_t("encryption|access_secret_storage_dialog|key_validation_text|wrong_security_key")} + + ); + classes = "mx_AccessSecretStorageDialog_recoveryKeyFeedback--invalid"; } return ( - - {validationText} + + {content} ); } diff --git a/src/components/views/dialogs/spotlight/RoomResultContextMenus.tsx b/src/components/views/dialogs/spotlight/RoomResultContextMenus.tsx index 8bf5c1e22b..c11bd777e2 100644 --- a/src/components/views/dialogs/spotlight/RoomResultContextMenus.tsx +++ b/src/components/views/dialogs/spotlight/RoomResultContextMenus.tsx @@ -7,8 +7,12 @@ Please see LICENSE files in the repository root for full details. */ import { type Room } from "matrix-js-sdk/src/matrix"; -import React, { type JSX, Fragment, useState, type ReactNode } from "react"; -import { OverflowHorizontalIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import React, { Fragment, type JSX, type ReactNode, useState } from "react"; +import { + NotificationsOffSolidIcon, + OverflowHorizontalIcon, + NotificationsSolidIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { ContextMenuTooltipButton } from "../../../../accessibility/context_menu/ContextMenuTooltipButton"; import { useNotificationState } from "../../../../hooks/useRoomNotificationState"; @@ -21,23 +25,18 @@ import { type ButtonEvent } from "../../elements/AccessibleButton"; import { contextMenuBelow } from "../../rooms/RoomTile"; import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../../settings/UIFeature"; -import { Icon as NotificationsIcon } from "../../../../../res/img/element-icons/notifications.svg"; -import { Icon as NotificationsDefaultIcon } from "../../../../../res/img/element-icons/roomlist/notifications-default.svg"; -import { Icon as NotificationsDmIcon } from "../../../../../res/img/element-icons/roomlist/notifications-dm.svg"; -import { Icon as NotificationsOffIcon } from "../../../../../res/img/element-icons/roomlist/notifications-off.svg"; interface Props { room: Room; } export function getNotificationIcon(state: RoomNotifState): ReactNode { - const icons: Record = { - [RoomNotifState.AllMessages]: , - [RoomNotifState.AllMessagesLoud]: , - [RoomNotifState.MentionsOnly]: , - [RoomNotifState.Mute]: , - }; - return icons[state]; + switch (state) { + case RoomNotifState.Mute: + return ; + default: + return ; + } } export function RoomResultContextMenus({ room }: Props): JSX.Element { diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index f4e33ce155..204c404fde 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -7,22 +7,21 @@ Please see LICENSE files in the repository root for full details. */ import { type WebSearch as WebSearchEvent } from "@matrix-org/analytics-events/types/typescript/WebSearch"; -import classNames from "classnames"; import { capitalize, sum } from "lodash"; import { + type HierarchyRoom, type IPublicRoomsChunkRoom, + JoinRule, type MatrixClient, + type Room, RoomMember, RoomType, - type Room, - type HierarchyRoom, - JoinRule, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { normalize } from "matrix-js-sdk/src/utils"; import React, { - type JSX, type ChangeEvent, + type JSX, useCallback, useContext, useEffect, @@ -31,6 +30,17 @@ import React, { useState, } from "react"; import sanitizeHtml from "sanitize-html"; +import { + ChatIcon, + RoomIcon, + SpaceIcon, + UserProfileIcon, + FavouriteIcon, + HomeIcon, + GroupIcon, + CloseIcon, + LinkIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; import { @@ -62,7 +72,7 @@ import { type RoomNotificationState } from "../../../../stores/notifications/Roo import { RoomNotificationStateStore } from "../../../../stores/notifications/RoomNotificationStateStore"; import { RecentAlgorithm } from "../../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import { SdkContextClass } from "../../../../contexts/SDKContext"; -import { getMetaSpaceName } from "../../../../stores/spaces"; +import { getMetaSpaceName, MetaSpace } from "../../../../stores/spaces"; import SpaceStore from "../../../../stores/spaces/SpaceStore"; import { DirectoryMember, type Member, startDmOnFirstMessage } from "../../../../utils/direct-messages"; import DMRoomMap from "../../../../utils/DMRoomMap"; @@ -131,6 +141,30 @@ function filterToLabel(filter: Filter): string { } } +function filterToIcon(filter: Filter): JSX.Element { + switch (filter) { + case Filter.People: + return ; + case Filter.PublicRooms: + return ; + case Filter.PublicSpaces: + return ; + } +} + +function metaspaceToIcon(key: MetaSpace): JSX.Element | undefined { + switch (key) { + case MetaSpace.Home: + return ; + case MetaSpace.Favourites: + return ; + case MetaSpace.People: + return ; + case MetaSpace.Orphans: + return ; + } +} + interface IBaseResult { section: Section; filter: Filter[]; @@ -396,14 +430,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n ...SpaceStore.instance.enabledMetaSpaces.map((spaceKey) => ({ section: Section.Spaces, filter: [] as Filter[], - avatar: ( -
    - ), + avatar:
    {metaspaceToIcon(spaceKey)}
    , name: getMetaSpaceName(spaceKey, SpaceStore.instance.allRoomsInHome), onClick() { SpaceStore.instance.setActiveSpace(spaceKey); @@ -584,34 +611,30 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n {filter !== Filter.PublicSpaces && supportsSpaceFiltering && ( )} {filter !== Filter.PublicRooms && ( )} {filter !== Filter.People && ( - )} {filter === null && ( )} @@ -919,7 +943,6 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n
    @@ -1003,9 +1029,9 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n

    {_t("spotlight_dialog|group_chat_section_title")}

    @@ -1244,13 +1270,8 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n >
    {filter !== null && ( -
    +
    + {filterToIcon(filter)} {filterToLabel(filter)} = ({ initialText = "", initialFilter = n })} className="mx_SpotlightDialog_filter--close" onClick={() => setFilter(null)} - /> + > + +
    )} { @@ -276,14 +276,6 @@ export default class AppTile extends React.Component { error: null, menuDisplayed: false, requiresClient: this.determineInitialRequiresClientState(), - hasContextMenuOptions: showContextMenu( - this.context, - this.props.room, - newProps.app, - newProps.userWidget, - !newProps.userWidget, - newProps.onDeleteClick, - ), }; } @@ -768,21 +760,6 @@ export default class AppTile extends React.Component { } appTileClasses = classNames(appTileClasses); - let contextMenu; - if (this.state.menuDisplayed) { - contextMenu = ( - - ); - } - const layoutButtons: ReactNode[] = []; if (this.props.showLayoutButtons) { const isMaximised = @@ -838,24 +815,33 @@ export default class AppTile extends React.Component { )} - {this.state.hasContextMenuOptions && ( - - - - )} + + + + + } + app={this.props.app} + onFinished={this.closeContextMenu} + showUnpin={!this.props.userWidget} + userWidget={this.props.userWidget} + onEditClick={this.props.onEditClick} + onDeleteClick={this.props.onDeleteClick} + menuDisplayed={this.state.menuDisplayed} + /> +
    )} {appTileBody}
    - - {contextMenu} ); } diff --git a/src/components/views/elements/AppWarning.tsx b/src/components/views/elements/AppWarning.tsx index 9f8a26e6cd..0e500c5bb1 100644 --- a/src/components/views/elements/AppWarning.tsx +++ b/src/components/views/elements/AppWarning.tsx @@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; - -import WarningSvg from "../../../../res/img/warning.svg"; +import { WarningIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; interface IProps { errorMsg?: string; @@ -18,7 +17,7 @@ const AppWarning: React.FC = (props) => { return (
    - +
    {props.errorMsg || "Error"} diff --git a/src/components/views/elements/BugReportDialogButton.tsx b/src/components/views/elements/BugReportDialogButton.tsx new file mode 100644 index 0000000000..284501e7b9 --- /dev/null +++ b/src/components/views/elements/BugReportDialogButton.tsx @@ -0,0 +1,43 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { useCallback } from "react"; +import { Button } from "@vector-im/compound-web"; + +import SdkConfig from "../../../SdkConfig"; +import { _t } from "../../../languageHandler"; +import Modal from "../../../Modal"; +import BugReportDialog, { type BugReportDialogProps } from "../dialogs/BugReportDialog"; +import { BugReportEndpointURLLocal } from "../../../IConfigOptions"; + +/** + * Renders a button to open the BugReportDialog *if* the configuration + * supports it. + */ +export function BugReportDialogButton({ + label, + error, +}: Pick): React.ReactElement | null { + const bugReportUrl = SdkConfig.get().bug_report_endpoint_url; + const onClick = useCallback(() => { + Modal.createDialog(BugReportDialog, { + label, + error, + }); + }, [label, error]); + + if (!bugReportUrl) { + return null; + } + return ( + + ); +} diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 636e37ccd0..fd20db7ef6 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import classNames from "classnames"; -import { _t, _td, type TranslationKey } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import BaseDialog from "..//dialogs/BaseDialog"; import DialogButtons from "./DialogButtons"; import AccessibleButton from "./AccessibleButton"; diff --git a/src/components/views/elements/DialPadBackspaceButton.tsx b/src/components/views/elements/DialPadBackspaceButton.tsx index 2a3b7df5e5..322d5b71db 100644 --- a/src/components/views/elements/DialPadBackspaceButton.tsx +++ b/src/components/views/elements/DialPadBackspaceButton.tsx @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; +import { BackspaceSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import AccessibleButton, { type ButtonEvent } from "./AccessibleButton"; @@ -24,7 +25,9 @@ export default class DialPadBackspaceButton extends React.PureComponent className="mx_DialPadBackspaceButton" onClick={this.props.onBackspacePress} aria-label={_t("keyboard|backspace")} - /> + > + +
    ); } diff --git a/src/components/views/elements/EditableItemList.tsx b/src/components/views/elements/EditableItemList.tsx index c5679a543b..28d4b9dc1c 100644 --- a/src/components/views/elements/EditableItemList.tsx +++ b/src/components/views/elements/EditableItemList.tsx @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { type JSX, type ChangeEvent } from "react"; +import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import Field from "./Field"; @@ -74,12 +75,14 @@ export class EditableItem extends React.Component { return (
    -
    + > + + {this.props.value}
    ); diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx index 7fd2ba9712..ae5e3f93bf 100644 --- a/src/components/views/elements/ErrorBoundary.tsx +++ b/src/components/views/elements/ErrorBoundary.tsx @@ -12,10 +12,9 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import PlatformPeg from "../../../PlatformPeg"; -import Modal from "../../../Modal"; import SdkConfig from "../../../SdkConfig"; -import BugReportDialog from "../dialogs/BugReportDialog"; import AccessibleButton from "./AccessibleButton"; +import { BugReportDialogButton } from "./BugReportDialogButton"; interface Props { children: ReactNode; @@ -60,13 +59,6 @@ export default class ErrorBoundary extends React.PureComponent { }); }; - private onBugReport = (): void => { - Modal.createDialog(BugReportDialog, { - label: "react-soft-crash", - error: this.state.error, - }); - }; - public render(): ReactNode { if (this.state.error) { const newIssueUrl = SdkConfig.get().feedback.new_issue_url; @@ -95,9 +87,7 @@ export default class ErrorBoundary extends React.PureComponent {   {_t("bug_reporting|description")}

    - - {_t("bug_reporting|submit_debug_logs")} - + ); } diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 2d57d1772b..71e6d91f68 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -19,6 +19,7 @@ import React, { import classNames from "classnames"; import { debounce } from "lodash"; import { Tooltip } from "@vector-im/compound-web"; +import { ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { type IFieldState, type IValidationResult } from "./Validation"; @@ -326,6 +327,11 @@ export default class Field extends React.PureComponent { mx_Field_invalid: hasValidationFlag ? !forceValidity : onValidate && this.state.valid === false, }); + let inputDecoration: JSX.Element | undefined; + if (this.props.element === "select") { + inputDecoration = ; + } + return (
    {prefixContainer} @@ -340,6 +346,7 @@ export default class Field extends React.PureComponent { {fieldInput} + {inputDecoration} {postfixContainer}
    ); diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index e88a1c066e..ac13f04bc3 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -11,6 +11,15 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, createRef, type CSSProperties, useEffect } from "react"; import FocusLock from "react-focus-lock"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { + CloseIcon, + DownloadIcon, + OverflowHorizontalIcon, + RotateLeftIcon, + RotateRightIcon, + ZoomInIcon, + ZoomOutIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import MemberAvatar from "../avatars/MemberAvatar"; @@ -501,25 +510,12 @@ export default class ImageView extends React.Component { onClick={this.onOpenContextMenu} ref={this.contextMenuButton} isExpanded={this.state.contextMenuDisplayed} - /> + > + + ); } - const zoomOutButton = ( - - ); - const zoomInButton = ( - - ); - let title: JSX.Element | undefined; if (this.props.mxEvent?.getContent()) { title = ( @@ -544,18 +540,34 @@ export default class ImageView extends React.Component { {info} {title}
    - {zoomOutButton} - {zoomInButton} + + + + + + + > + + + > + + { className="mx_ImageView_button mx_ImageView_button_close" title={_t("action|close")} onClick={this.props.onFinished} - /> + > + + {this.renderContextMenu()}
    @@ -612,10 +626,12 @@ export const DownloadButton: React.FC = ({ url, fileName, m return ( + > + + ); }; diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 41a3bc9dfb..f72c28f26e 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -18,6 +18,7 @@ import { type TimelineEvents, } from "matrix-js-sdk/src/matrix"; import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; +import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import ScrollableBaseModal, { type IScrollableBaseState } from "../dialogs/ScrollableBaseModal"; import QuestionDialog from "../dialogs/QuestionDialog"; @@ -233,7 +234,9 @@ export default class PollCreateDialog extends ScrollableBaseModal this.onOptionRemove(i)} className="mx_PollCreateDialog_removeOption" disabled={this.state.busy} - /> + > + +
    ))} { } interface IState { - levelRoleMap: Partial>; // List of power levels to show in the drop-down options: number[]; @@ -47,7 +47,7 @@ interface IState { custom?: boolean; } -export default class PowerSelector extends React.Component, IState> { +export default class PowerSelector extends React.PureComponent, IState> { public static defaultProps: Partial> = { maxValue: Infinity, usersDefault: 0, @@ -58,7 +58,6 @@ export default class PowerSelector extends React.C super(props); this.state = { - levelRoleMap: {}, // List of power levels to show in the drop-down options: [], @@ -85,6 +84,7 @@ export default class PowerSelector extends React.C private initStateFromProps(): void { // This needs to be done now because levelRoleMap has translated strings const levelRoleMap = Roles.levelRoleMap(this.props.usersDefault); + const options = Object.keys(levelRoleMap) .filter((level) => { return ( @@ -93,11 +93,13 @@ export default class PowerSelector extends React.C }) .map((level) => parseInt(level)); + if (arrayHasDiff(options, this.state.options)) { + this.setState({ options }); + } + const isCustom = levelRoleMap[this.props.value] === undefined; this.setState({ - levelRoleMap, - options, custom: isCustom, customValue: this.props.value, selectValue: isCustom ? CUSTOM_VALUE : this.props.value, diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index d81ff992ee..f082947162 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -106,7 +106,13 @@ export default class ReplyChain extends React.Component { if (el) { const code: HTMLElement | null = el.querySelector("code"); const isCodeEllipsisShown = code ? code.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS : false; - const isElipsisShown = el.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS || isCodeEllipsisShown; + const isElipsisShown = + isCodeEllipsisShown || + el.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS || + // Check whether the body fits into it's scroll container + el.clientHeight !== el.scrollHeight || + // Do the same for its children as the scroll container may be on them instead + [...el.children].some((child) => child.clientHeight !== child.scrollHeight); if (isElipsisShown) { this.props.setQuoteExpanded(false); } diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx index 430f205377..1debb50645 100644 --- a/src/components/views/elements/ServerPicker.tsx +++ b/src/components/views/elements/ServerPicker.tsx @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; +import { InfoIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import AccessibleButton from "./AccessibleButton"; import { type ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; @@ -86,11 +87,9 @@ const ServerPicker: React.FC = ({ title, dialogTitle, serverConfig, onSe

    {title || _t("common|homeserver")}

    {!disableCustomUrls ? ( - + + + ) : null} {serverName} diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx index 54bee73b4a..e56787044f 100644 --- a/src/components/views/elements/StyledRadioButton.tsx +++ b/src/components/views/elements/StyledRadioButton.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type Ref } from "react"; +import React, { type JSX, type Ref } from "react"; import classnames from "classnames"; interface IProps extends React.InputHTMLAttributes { @@ -16,6 +16,11 @@ interface IProps extends React.InputHTMLAttributes { // If false, they'll be in a div. Putting interactive components that have labels // themselves in labels can cause strange bugs like https://github.com/vector-im/element-web/issues/18031 childrenInLabel?: boolean; + + /** + * If provided will override the default dot icon drawn for checked state + */ + icon?: JSX.Element; } export default class StyledRadioButton extends React.PureComponent { @@ -25,7 +30,7 @@ export default class StyledRadioButton extends React.PureComponent { }; public render(): React.ReactNode { - const { children, className, disabled, outlined, childrenInLabel, inputRef, ...otherProps } = this.props; + const { children, className, disabled, outlined, childrenInLabel, inputRef, icon, ...otherProps } = this.props; const _className = classnames("mx_StyledRadioButton", className, { mx_StyledRadioButton_disabled: disabled, mx_StyledRadioButton_enabled: !disabled, @@ -42,9 +47,9 @@ export default class StyledRadioButton extends React.PureComponent { disabled={disabled} {...otherProps} /> - {/* Used to render the radio button circle */}
    -
    + {/* Empty div is used to render the radio button circle */} +
    {icon}
    ); diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx index 21e5c97c33..17f33d9346 100644 --- a/src/components/views/emojipicker/Category.tsx +++ b/src/components/views/emojipicker/Category.tsx @@ -22,6 +22,8 @@ export type CategoryKey = keyof typeof DATA_BY_CATEGORY | "recent"; export interface ICategory { id: CategoryKey; name: string; + // Emoji to show in the header for this category + emoji: string; enabled: boolean; // Whether the category is currently visible visible: boolean; diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 9ba2445102..192a40b782 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -81,19 +81,16 @@ class EmojiPicker extends React.Component { const hasRecentlyUsed = this.recentlyUsed.length > 0; - const categoryConfig: Array<{ - id: CategoryKey; - name: string; - }> = [ - { id: "recent", name: _t("emoji|category_frequently_used") }, - { id: "people", name: _t("emoji|category_smileys_people") }, - { id: "nature", name: _t("emoji|category_animals_nature") }, - { id: "foods", name: _t("emoji|category_food_drink") }, - { id: "activity", name: _t("emoji|category_activities") }, - { id: "places", name: _t("emoji|category_travel_places") }, - { id: "objects", name: _t("emoji|category_objects") }, - { id: "symbols", name: _t("emoji|category_symbols") }, - { id: "flags", name: _t("emoji|category_flags") }, + const categoryConfig: Pick[] = [ + { id: "recent", name: _t("emoji|category_frequently_used"), emoji: "🕒" }, + { id: "people", name: _t("emoji|category_smileys_people"), emoji: "😀" }, + { id: "nature", name: _t("emoji|category_animals_nature"), emoji: "🐕" }, + { id: "foods", name: _t("emoji|category_food_drink"), emoji: "🍎" }, + { id: "activity", name: _t("emoji|category_activities"), emoji: "⚽️" }, + { id: "places", name: _t("emoji|category_travel_places"), emoji: "🚗" }, + { id: "objects", name: _t("emoji|category_objects"), emoji: "💡" }, + { id: "symbols", name: _t("emoji|category_symbols"), emoji: "⁉️" }, + { id: "flags", name: _t("emoji|category_flags"), emoji: "🏁" }, ]; this.categories = categoryConfig.map((config) => { @@ -109,8 +106,7 @@ class EmojiPicker extends React.Component { firstVisible = !hasRecentlyUsed; } return { - id: config.id, - name: config.name, + ...config, enabled: isEnabled, visible: isVisible, firstVisible: firstVisible, diff --git a/src/components/views/emojipicker/Header.tsx b/src/components/views/emojipicker/Header.tsx index ed2c6b9f21..5b57c065b5 100644 --- a/src/components/views/emojipicker/Header.tsx +++ b/src/components/views/emojipicker/Header.tsx @@ -84,7 +84,7 @@ class Header extends React.PureComponent { onKeyDown={this.onKeyDown} > {this.props.categories.map((category) => { - const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, { + const classes = classNames("mx_EmojiPicker_anchor", { mx_EmojiPicker_anchor_visible: category.visible, }); // Properties of this button are also modified by EmojiPicker's updateVisibility in DOM. @@ -100,7 +100,9 @@ class Header extends React.PureComponent { tabIndex={category.firstVisible ? 0 : -1} // roving aria-selected={category.visible} aria-controls={`mx_EmojiPicker_category_${category.id}`} - /> + > + {category.emoji} + ); })} diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx index 007fa2251d..4f6ede9c06 100644 --- a/src/components/views/emojipicker/Search.tsx +++ b/src/components/views/emojipicker/Search.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { type JSX } from "react"; +import { CloseIcon, SearchIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; @@ -52,12 +53,18 @@ class Search extends React.PureComponent { rightButton = ( ); } else { - rightButton = ; + rightButton = ( + + + + ); } return ( diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index f8efd8381e..b7eb0be699 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -7,16 +7,12 @@ Please see LICENSE files in the repository root for full details. */ import React, { type Ref, useCallback, useContext, useMemo, type JSX } from "react"; +import { VideoCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import type { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix"; import { ConnectionState, type ElementCall } from "../../../models/Call"; import { _t } from "../../../languageHandler"; -import { - useCall, - useConnectionState, - useJoinCallButtonDisabledTooltip, - useParticipatingMembers, -} from "../../../hooks/useCall"; +import { useCall, useConnectionState, useParticipatingMembers } from "../../../hooks/useCall"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; @@ -102,7 +98,6 @@ interface ActiveLoadedCallEventProps { const ActiveLoadedCallEvent = ({ mxEvent, call, ref }: ActiveLoadedCallEventProps): JSX.Element => { const connectionState = useConnectionState(call); const participatingMembers = useParticipatingMembers(call); - const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call); const connect = useCallback( (ev: ButtonEvent) => { @@ -146,7 +141,6 @@ const ActiveLoadedCallEvent = ({ mxEvent, call, ref }: ActiveLoadedCallEventProp participatingMembers={participatingMembers} buttonText={buttonText} buttonKind={buttonKind} - buttonDisabledTooltip={joinCallButtonDisabledTooltip ?? undefined} onButtonClick={onButtonClick} /> ); @@ -173,7 +167,10 @@ export const CallEvent = ({ mxEvent, ref }: CallEventProps): JSX.Element => {
    - {_t("timeline|m.call|video_call_ended")} + + + {_t("timeline|m.call|video_call_ended")} +
    diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 7d49042533..061cc76204 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -12,6 +12,7 @@ import { Direction, ConnectionError, MatrixError, HTTPError } from "matrix-js-sd import { logger } from "matrix-js-sdk/src/logger"; import { capitalize } from "lodash"; import { ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { TimelineSeparator } from "@element-hq/web-shared-components"; import { _t, getUserLanguage } from "../../../languageHandler"; import { formatFullDateNoDay, formatFullDateNoTime, getDaysArray } from "../../../DateUtils"; @@ -32,7 +33,6 @@ import IconizedContextMenu, { } from "../context_menus/IconizedContextMenu"; import JumpToDatePicker from "./JumpToDatePicker"; import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import TimelineSeparator from "./TimelineSeparator"; import RoomContext from "../../../contexts/RoomContext"; interface IProps { @@ -335,6 +335,10 @@ export default class DateSeparator extends React.Component { ); } - return {dateHeaderContent}; + return ( + + {dateHeaderContent} + + ); } } diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx deleted file mode 100644 index f75a7c48f8..0000000000 --- a/src/components/views/messages/DecryptionFailureBody.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022-2024 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import classNames from "classnames"; -import React, { type JSX, useContext } from "react"; -import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; -import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -import { _t } from "../../../languageHandler"; -import { type IBodyProps } from "./IBodyProps"; -import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; - -function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | JSX.Element { - switch (mxEvent.decryptionFailureReason) { - case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE: - return _t("timeline|decryption_failure|blocked"); - - case DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP: - return _t("timeline|decryption_failure|historical_event_no_key_backup"); - - case DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED: - if (isVerified === false) { - // The user seems to have a key backup, so prompt them to verify in the hope that doing so will - // mean we can restore from backup and we'll get the key for this message. - return _t("timeline|decryption_failure|historical_event_unverified_device"); - } - // otherwise, use the default. - break; - - case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED: - // TODO: event should be hidden instead of showing this error. - // To be revisited as part of https://github.com/element-hq/element-meta/issues/2449 - return _t("timeline|decryption_failure|historical_event_user_not_joined"); - - case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: - return ( - - - {_t("timeline|decryption_failure|sender_identity_previously_verified")} - - ); - - case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE: - // TODO: event should be hidden instead of showing this error. - // To be revisited as part of https://github.com/element-hq/element-meta/issues/2449 - return ( - - - {_t("timeline|decryption_failure|sender_unsigned_device")} - - ); - } - return _t("timeline|decryption_failure|unable_to_decrypt"); -} - -/** Get an extra CSS class, specific to the decryption failure reason */ -function errorClassName(mxEvent: MatrixEvent): string | null { - switch (mxEvent.decryptionFailureReason) { - case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: - case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE: - return "mx_DecryptionFailureSenderTrustRequirement"; - - default: - return null; - } -} - -// A placeholder element for messages that could not be decrypted -export const DecryptionFailureBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { - const verificationState = useContext(LocalDeviceVerificationStateContext); - const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", errorClassName(mxEvent)); - - return ( -
    - {getErrorMessage(mxEvent, verificationState)} -
    - ); -}; diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index cf8b00db39..a8467e0519 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -6,12 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, type Ref, type ReactNode } from "react"; +import React, { type JSX, type ReactNode } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { ErrorSolidIcon, LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { EventTileBubble } from "@element-hq/web-shared-components"; import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types"; import { _t } from "../../../languageHandler"; -import EventTileBubble from "./EventTileBubble"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import DMRoomMap from "../../../utils/DMRoomMap"; import { objectHasDiff } from "../../../utils/objects"; @@ -22,7 +23,7 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts"; interface IProps { mxEvent: MatrixEvent; timestamp?: JSX.Element; - ref?: Ref; + ref?: React.RefObject; } const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => { @@ -58,33 +59,39 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => { return ( } + className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon" title={stateEncrypted ? _t("common|state_encryption_enabled") : _t("common|encryption_enabled")} subtitle={subtitle} - timestamp={timestamp} - /> + > + {timestamp} + ); } if (isRoomEncrypted) { return ( } + className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon" title={_t("common|encryption_enabled")} subtitle={_t("timeline|m.room.encryption|disable_attempt")} - timestamp={timestamp} - /> + > + {timestamp} + ); } return ( } + className="mx_EventTileBubble mx_cryptoEvent" title={_t("timeline|m.room.encryption|disabled")} subtitle={_t("timeline|m.room.encryption|unsupported")} ref={ref} - timestamp={timestamp} - /> + > + {timestamp} + ); }; diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx deleted file mode 100644 index 4569115c0d..0000000000 --- a/src/components/views/messages/EventTileBubble.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2020 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type JSX, type ReactNode, type Ref } from "react"; -import classNames from "classnames"; - -interface IProps { - className: string; - title: string; - timestamp?: JSX.Element; - subtitle?: ReactNode; - children?: JSX.Element; - ref?: Ref; -} - -const EventTileBubble = ({ className, title, timestamp, subtitle, children, ref }: IProps): JSX.Element => { - return ( -
    -
    {title}
    - {subtitle &&
    {subtitle}
    } - {children} - {timestamp} -
    - ); -}; - -export default EventTileBubble; diff --git a/src/components/views/messages/HiddenBody.tsx b/src/components/views/messages/HiddenBody.tsx index 20410017be..aeca8e7ea7 100644 --- a/src/components/views/messages/HiddenBody.tsx +++ b/src/components/views/messages/HiddenBody.tsx @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { type JSX } from "react"; +import { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import { type IBodyProps } from "./IBodyProps"; @@ -34,6 +35,7 @@ const HiddenBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { return ( + {text} ); diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index 7f196cfe82..23a35dd164 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -25,7 +25,7 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => { const [dateValue, setDateValue] = useState(dateInputDefaultValue); const [onFocus, isActive, refCallback] = useRovingTabIndex(); - const onDateValueInput = (ev: React.ChangeEvent): void => setDateValue(ev.target.value); + const onDateValueInput = (ev: React.InputEvent): void => setDateValue(ev.currentTarget.value); const onJumpToDateSubmit = (ev: FormEvent): void => { ev.preventDefault(); onDatePicked(dateValue); diff --git a/src/components/views/messages/LegacyCallEvent.tsx b/src/components/views/messages/LegacyCallEvent.tsx index f43953fb6e..2941705b0b 100644 --- a/src/components/views/messages/LegacyCallEvent.tsx +++ b/src/components/views/messages/LegacyCallEvent.tsx @@ -6,11 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, createRef } from "react"; +import React, { createRef, type JSX } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { CallErrorCode, CallState } from "matrix-js-sdk/src/webrtc/call"; import classNames from "classnames"; import { Clock } from "@element-hq/web-shared-components"; +import { + EndCallIcon, + VideoCallDeclinedSolidIcon, + VideoCallMissedSolidIcon, + VideoCallSolidIcon, + VoiceCallMissedSolidIcon, + VoiceCallSolidIcon, + VolumeOffSolidIcon, + VolumeOnSolidIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import MemberAvatar from "../avatars/MemberAvatar"; @@ -35,6 +45,17 @@ interface IState { length: number; } +export function getCallStateIcon(isVoice: boolean, state: undefined | "missed" | "declined"): JSX.Element { + let icon = isVoice ? : ; + if (state === "missed") { + icon = isVoice ? : ; + } else if (state === "declined") { + icon = isVoice ? : ; + } + + return
    {icon}
    ; +} + export default class LegacyCallEvent extends React.PureComponent { private wrapperElement = createRef(); private resizeObserver?: ResizeObserver; @@ -89,28 +110,25 @@ export default class LegacyCallEvent extends React.PureComponent private renderCallBackButton(text: string): JSX.Element { return ( - {text} + {this.props.callEventGrouper.isVoice ? : } + {text} ); } private renderSilenceIcon(): JSX.Element { - const silenceClass = classNames({ - mx_LegacyCallEvent_iconButton: true, - mx_LegacyCallEvent_unSilence: this.state.silenced, - mx_LegacyCallEvent_silence: !this.state.silenced, - }); - return ( + > + {this.state.silenced ? : } + ); } @@ -125,18 +143,20 @@ export default class LegacyCallEvent extends React.PureComponent
    {silenceIcon} - {_t("action|decline")} + + {_t("action|decline")} - {_t("action|accept")} + {this.props.callEventGrouper.isVoice ? : } + {_t("action|accept")} {this.props.timestamp}
    @@ -268,15 +288,23 @@ export default class LegacyCallEvent extends React.PureComponent mx_LegacyCallEvent_voice: isVoice, mx_LegacyCallEvent_video: !isVoice, mx_LegacyCallEvent_narrow: this.state.narrow, - mx_LegacyCallEvent_missed: this.props.callEventGrouper.callWasMissed, - mx_LegacyCallEvent_noAnswer: callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout, - mx_LegacyCallEvent_rejected: callState === CallState.Ended && this.props.callEventGrouper.gotRejected, }); + let silenceIcon; if (this.state.narrow && this.state.callState === CallState.Ringing) { silenceIcon = this.renderSilenceIcon(); } + let iconState: Parameters[1] = undefined; + if (this.props.callEventGrouper.callWasMissed) { + iconState = "missed"; + } else if ( + callState === CallState.Ended && + (hangupReason === CallErrorCode.InviteTimeout || this.props.callEventGrouper.gotRejected) + ) { + iconState = "declined"; + } + return (
    @@ -286,7 +314,7 @@ export default class LegacyCallEvent extends React.PureComponent
    {sender}
    -
    + {getCallStateIcon(!!isVoice, iconState)} {callType}
    diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 1c55385d2c..b6efbed1cf 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -9,8 +9,14 @@ Please see LICENSE files in the repository root for full details. import React, { type AllHTMLAttributes, createRef } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { type MediaEventContent } from "matrix-js-sdk/src/types"; +import { MsgType } from "matrix-js-sdk/src/matrix"; import { Button } from "@vector-im/compound-web"; -import { DownloadIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { + AttachmentIcon, + DownloadIcon, + VideoCallSolidIcon, + VolumeOnSolidIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; @@ -190,9 +196,17 @@ export default class MFileBody extends React.Component { let placeholder: React.ReactNode = null; if (showGenericPlaceholder) { + let icon = ; + // MFileBody is not generally used for Audio/Video but can be as part of ReplyTile + if (this.content.msgtype === MsgType.Audio) { + icon = ; + } else if (this.content.msgtype === MsgType.Video) { + icon = ; + } + placeholder = ( - + {icon} {presentableTextForFile(this.content, _t("common|attachment"), true, true)} diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 788a93c813..b9c8552978 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -15,6 +15,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { ClientEvent } from "matrix-js-sdk/src/matrix"; import { type ImageContent } from "matrix-js-sdk/src/types"; import { Tooltip } from "@vector-im/compound-web"; +import { ImageErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import MFileBody from "./MFileBody"; import Modal from "../../../Modal"; @@ -674,7 +675,11 @@ export class MImageBodyInner extends React.Component { errorText = _t("timeline|m.image|error_downloading"); } - return {errorText}; + return ( + + {errorText} + + ); } let contentUrl = this.state.contentUrl; diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 4bd85f2176..a51fe6065e 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -8,10 +8,11 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { VideoCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { EventTileBubble } from "@element-hq/web-shared-components"; import { _t } from "../../../languageHandler"; import WidgetStore from "../../../stores/WidgetStore"; -import EventTileBubble from "./EventTileBubble"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; @@ -41,30 +42,36 @@ export default class MJitsiWidgetEvent extends React.PureComponent { // removed return ( } + className="mx_EventTileBubble mx_MJitsiWidgetEvent" title={_t("timeline|m.widget|jitsi_ended", { senderName })} - timestamp={this.props.timestamp} - /> + > + {this.props.timestamp} + ); } else if (prevUrl) { // modified return ( } + className="mx_EventTileBubble mx_MJitsiWidgetEvent" title={_t("timeline|m.widget|jitsi_updated", { senderName })} subtitle={joinCopy} - timestamp={this.props.timestamp} - /> + > + {this.props.timestamp} + ); } else { // assume added return ( } + className="mx_EventTileBubble mx_MJitsiWidgetEvent" title={_t("timeline|m.widget|jitsi_started", { senderName })} subtitle={joinCopy} - timestamp={this.props.timestamp} - /> + > + {this.props.timestamp} + ); } } diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index 8f1f8a6962..022766868e 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -8,10 +8,11 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { EventTileBubble } from "@element-hq/web-shared-components"; import { _t } from "../../../languageHandler"; import { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver"; -import EventTileBubble from "./EventTileBubble"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; interface Props { @@ -73,12 +74,12 @@ const MKeyVerificationRequest: React.FC = ({ mxEvent, timestamp }) => { return ( } + className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon" title={title} subtitle={subtitle} - timestamp={timestamp} > - <> + {timestamp} ); }; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 22f29e963f..80b0939a71 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -32,10 +32,10 @@ import { ThreadsIcon, EditIcon, ReactionAddIcon, + ExpandIcon, + CollapseIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; -import { Icon as ExpandMessageIcon } from "../../../../res/img/element-icons/expand-message.svg"; -import { Icon as CollapseMessageIcon } from "../../../../res/img/element-icons/collapse-message.svg"; import { _t } from "../../../languageHandler"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; @@ -572,7 +572,7 @@ export default class MessageActionBar extends React.PureComponent - {this.props.isQuoteExpanded ? : } + {this.props.isQuoteExpanded ? : } , ); } diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 6d124c88a8..6bb28fd188 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import mime from "mime"; -import React, { createRef } from "react"; +import React, { type JSX, createRef, useContext, useEffect } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { EventType, @@ -18,7 +18,9 @@ import { M_POLL_START, type IContent, } from "matrix-js-sdk/src/matrix"; +import { useCreateAutoDisposedViewModel, DecryptionFailureBodyView } from "@element-hq/web-shared-components"; +import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; import SettingsStore from "../../../settings/SettingsStore"; import { Mjolnir } from "../../../mjolnir/Mjolnir"; import RedactedBody from "./RedactedBody"; @@ -36,8 +38,8 @@ import MPollBody from "./MPollBody"; import MLocationBody from "./MLocationBody"; import MjolnirBody from "./MjolnirBody"; import MBeaconBody from "./MBeaconBody"; -import { DecryptionFailureBody } from "./DecryptionFailureBody"; import { type GetRelationsForEvent, type IEventTileOps } from "../rooms/EventTile"; +import { DecryptionFailureBodyViewModel } from "../../../viewmodels/message-body/DecryptionFailureBodyViewModel"; // onMessageAllowed is handled internally interface IProps extends Omit { @@ -248,7 +250,7 @@ export default class MessageEvent extends React.Component implements IMe if (!this.props.mxEvent.isRedacted()) { // only resolve BodyType if event is not redacted if (this.props.mxEvent.isDecryptionFailure()) { - BodyType = DecryptionFailureBody; + BodyType = DecryptionFailureBodyWrapper; } else if (type && this.evTypes.has(type)) { BodyType = this.evTypes.get(type)!; } else if (msgtype && this.bodyTypes.has(msgtype)) { @@ -328,3 +330,22 @@ const CaptionBody: React.FunctionComponent
    ); + +/** + * Bridge decryption-failure events into the view model using current local verification state. + * This wrapper can be removed after MessageEvent has been changed to a function component. + */ +function DecryptionFailureBodyWrapper({ mxEvent, ref }: IBodyProps): JSX.Element { + const verificationState = useContext(LocalDeviceVerificationStateContext); + const vm = useCreateAutoDisposedViewModel( + () => + new DecryptionFailureBodyViewModel({ + decryptionFailureCode: mxEvent.decryptionFailureReason, + verificationState, + }), + ); + useEffect(() => { + vm.setVerificationState(verificationState); + }, [verificationState, vm]); + return ; +} diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index 32f37f4c24..4ac68ede3e 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -11,6 +11,7 @@ import classNames from "classnames"; import { type MatrixEvent, MatrixEventEvent, type Relations, RelationsEvent } from "matrix-js-sdk/src/matrix"; import { uniqBy } from "lodash"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; +import { ReactionAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import { isContentActionable } from "../../../utils/EventUtils"; @@ -54,7 +55,9 @@ const ReactButton: React.FC = ({ mxEvent, reactions }) => { }} isExpanded={menuDisplayed} ref={button} - /> + > + + {contextMenu} diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 8320237b25..9147d7c1fc 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -9,12 +9,13 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import classNames from "classnames"; import { EventType, type MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; +import { ReactionsRowButtonTooltipView } from "@element-hq/web-shared-components"; import { mediaFromMxc } from "../../../customisations/Media"; import { _t } from "../../../languageHandler"; import { formatList } from "../../../utils/FormattingUtils"; import dis from "../../../dispatcher/dispatcher"; -import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip"; +import { ReactionsRowButtonTooltipViewModel } from "../../../viewmodels/message-body/ReactionsRowButtonTooltipViewModel"; import AccessibleButton from "../elements/AccessibleButton"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { REACTION_SHORTCODE_KEY } from "./ReactionsRow"; @@ -40,6 +41,41 @@ export default class ReactionsRowButton extends React.PureComponent { public static contextType = MatrixClientContext; declare public context: React.ContextType; + private reactionsRowButtonTooltipViewModel: ReactionsRowButtonTooltipViewModel; + + public constructor(props: IProps, context: React.ContextType) { + super(props, context); + this.reactionsRowButtonTooltipViewModel = new ReactionsRowButtonTooltipViewModel({ + client: context, + mxEvent: props.mxEvent, + content: props.content, + reactionEvents: props.reactionEvents, + customReactionImagesEnabled: props.customReactionImagesEnabled, + }); + } + + public componentDidUpdate(prevProps: IProps): void { + if ( + prevProps.mxEvent !== this.props.mxEvent || + prevProps.content !== this.props.content || + prevProps.reactionEvents !== this.props.reactionEvents || + prevProps.customReactionImagesEnabled !== this.props.customReactionImagesEnabled + ) { + // View model bails out if derived snapshot hasn't changed. + this.reactionsRowButtonTooltipViewModel.setProps({ + client: this.context, + mxEvent: this.props.mxEvent, + content: this.props.content, + reactionEvents: this.props.reactionEvents, + customReactionImagesEnabled: this.props.customReactionImagesEnabled, + }); + } + } + + public componentWillUnmount(): void { + this.reactionsRowButtonTooltipViewModel.dispose(); + } + public onClick = (): void => { const { mxEvent, myReactionEvent, content } = this.props; if (myReactionEvent) { @@ -110,12 +146,7 @@ export default class ReactionsRowButton extends React.PureComponent { } return ( - + { {count} - + ); } } diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.tsx b/src/components/views/messages/ReactionsRowButtonTooltip.tsx deleted file mode 100644 index f40002deff..0000000000 --- a/src/components/views/messages/ReactionsRowButtonTooltip.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019-2021 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type PropsWithChildren } from "react"; -import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { Tooltip } from "@vector-im/compound-web"; - -import { unicodeToShortcode } from "../../../HtmlUtils"; -import { _t } from "../../../languageHandler"; -import { formatList } from "../../../utils/FormattingUtils"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { REACTION_SHORTCODE_KEY } from "./ReactionsRow"; -interface IProps { - // The event we're displaying reactions for - mxEvent: MatrixEvent; - // The reaction content / key / emoji - content: string; - // A list of Matrix reaction events for this key - reactionEvents: MatrixEvent[]; - // Whether to render custom image reactions - customReactionImagesEnabled?: boolean; -} - -export default class ReactionsRowButtonTooltip extends React.PureComponent> { - public static contextType = MatrixClientContext; - declare public context: React.ContextType; - - public render(): React.ReactNode { - const { content, reactionEvents, mxEvent, children } = this.props; - - const room = this.context.getRoom(mxEvent.getRoomId()); - if (room) { - const senders: string[] = []; - let customReactionName: string | undefined; - for (const reactionEvent of reactionEvents) { - const member = room.getMember(reactionEvent.getSender()!); - const name = member?.name ?? reactionEvent.getSender()!; - senders.push(name); - customReactionName = - (this.props.customReactionImagesEnabled && - REACTION_SHORTCODE_KEY.findIn(reactionEvent.getContent())) || - undefined; - } - const shortName = unicodeToShortcode(content) || customReactionName; - const formattedSenders = formatList(senders, 6); - const caption = shortName ? _t("timeline|reactions|tooltip_caption", { shortName }) : undefined; - - return ( - - {children} - - ); - } - - return children; - } -} diff --git a/src/components/views/messages/RedactedBody.tsx b/src/components/views/messages/RedactedBody.tsx index ddad4d2e4d..a3a2c475dd 100644 --- a/src/components/views/messages/RedactedBody.tsx +++ b/src/components/views/messages/RedactedBody.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { useContext, type JSX } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -34,6 +35,7 @@ const RedactedBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { return ( + {text} ); diff --git a/src/components/views/messages/RoomPredecessorTile.tsx b/src/components/views/messages/RoomPredecessorTile.tsx index e35728e4b8..a33369bd76 100644 --- a/src/components/views/messages/RoomPredecessorTile.tsx +++ b/src/components/views/messages/RoomPredecessorTile.tsx @@ -10,13 +10,14 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, useCallback } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { type MatrixEvent, type Room, type RoomState } from "matrix-js-sdk/src/matrix"; +import { ChatSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { EventTileBubble } from "@element-hq/web-shared-components"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import EventTileBubble from "./EventTileBubble"; import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { useRoomState } from "../../../hooks/useRoomState"; import SettingsStore from "../../../settings/SettingsStore"; @@ -89,26 +90,29 @@ export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) => return ( } + className="mx_EventTileBubble mx_CreateEvent" title={_t("timeline|m.room.create|continuation")} - timestamp={timestamp} > -
    - - {!!guessedLink ? ( - <> - {_t("timeline|m.room.create|unknown_predecessor_guess_server", { + <> +
    + + {!!guessedLink ? ( + <> + {_t("timeline|m.room.create|unknown_predecessor_guess_server", { + roomId: predecessor.roomId, + })} + {guessedLink} + + ) : ( + _t("timeline|m.room.create|unknown_predecessor", { roomId: predecessor.roomId, - })} - {guessedLink} - - ) : ( - _t("timeline|m.room.create|unknown_predecessor", { - roomId: predecessor.roomId, - }) - )} - -
    + }) + )} +
    +
    + {timestamp} +
    ); } @@ -128,11 +132,13 @@ export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) => return ( } + className="mx_EventTileBubble mx_CreateEvent" title={_t("timeline|m.room.create|continuation")} subtitle={link} - timestamp={timestamp} - /> + > + {timestamp} + ); function createLinkWithRoom(room: Room, roomId: string, eventId?: string): string { diff --git a/src/components/views/messages/TileErrorBoundary.tsx b/src/components/views/messages/TileErrorBoundary.tsx index 2ccac0f288..2d11371166 100644 --- a/src/components/views/messages/TileErrorBoundary.tsx +++ b/src/components/views/messages/TileErrorBoundary.tsx @@ -12,12 +12,11 @@ import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; -import SdkConfig from "../../../SdkConfig"; -import BugReportDialog from "../dialogs/BugReportDialog"; import AccessibleButton from "../elements/AccessibleButton"; import SettingsStore from "../../../settings/SettingsStore"; import ViewSource from "../../structures/ViewSource"; import { type Layout } from "../../../settings/enums/Layout"; +import { BugReportDialogButton } from "../elements/BugReportDialogButton"; interface IProps { mxEvent: MatrixEvent; @@ -42,13 +41,6 @@ export default class TileErrorBoundary extends React.Component { return { error }; } - private onBugReport = (): void => { - Modal.createDialog(BugReportDialog, { - label: "react-soft-crash-tile", - error: this.state.error, - }); - }; - private onViewSource = (): void => { Modal.createDialog( ViewSource, @@ -69,18 +61,6 @@ export default class TileErrorBoundary extends React.Component { mx_EventTile_tileError: true, }; - let submitLogsButton; - if (SdkConfig.get().bug_report_endpoint_url) { - submitLogsButton = ( - <> -   - - {_t("bug_reporting|submit_debug_logs")} - - - ); - } - let viewSourceButton; if (mxEvent && SettingsStore.getValue("developerMode")) { viewSourceButton = ( @@ -99,7 +79,7 @@ export default class TileErrorBoundary extends React.Component { {_t("timeline|error_rendering_message")} {mxEvent && ` (${mxEvent.getType()})`} - {submitLogsButton} + {viewSourceButton}
    diff --git a/src/components/views/messages/TimelineSeparator.tsx b/src/components/views/messages/TimelineSeparator.tsx deleted file mode 100644 index 4735c8e00c..0000000000 --- a/src/components/views/messages/TimelineSeparator.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import React, { type ReactNode } from "react"; - -interface Props { - label: string; - children?: ReactNode; -} - -export const enum SeparatorKind { - None, - Date, - LateEvent, -} - -/** - * Generic timeline separator component to render within a MessagePanel - * - * @param label the accessible label string describing the separator - * @param children the children to draw within the timeline separator - */ -const TimelineSeparator: React.FC = ({ label, children }) => { - // ARIA treats
    s as separators, here we abuse them slightly so manually treat this entire thing as one - return ( -
    -
    - {children} -
    -
    - ); -}; - -export default TimelineSeparator; diff --git a/src/components/views/messages/shared/MediaProcessingError.tsx b/src/components/views/messages/shared/MediaProcessingError.tsx index 95a65b4538..13e63a2275 100644 --- a/src/components/views/messages/shared/MediaProcessingError.tsx +++ b/src/components/views/messages/shared/MediaProcessingError.tsx @@ -7,17 +7,17 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; - -import { Icon as WarningIcon } from "../../../../../res/img/warning.svg"; +import { FileErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; interface Props { className?: string; + Icon?: typeof FileErrorIcon; children: React.ReactNode; } -const MediaProcessingError: React.FC = ({ className, children }) => ( +const MediaProcessingError: React.FC = ({ className, children, Icon = FileErrorIcon }) => ( - + {children} ); diff --git a/src/components/views/polls/PollOption.tsx b/src/components/views/polls/PollOption.tsx index 277ebc3395..96c21ce60c 100644 --- a/src/components/views/polls/PollOption.tsx +++ b/src/components/views/polls/PollOption.tsx @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import React, { type ReactNode } from "react"; import classNames from "classnames"; import { type PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; +import { CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import { Icon as TrophyIcon } from "../../../../res/img/element-icons/trophy.svg"; @@ -85,6 +86,7 @@ const ActivePollOption: React.FC & { chi disabled={isEnded} aria-label={ariaLabel} onChange={() => onOptionSelected?.(answer.id)} + icon={isChecked ? : undefined} > diff --git a/src/components/views/right_panel/ExtensionsCard.tsx b/src/components/views/right_panel/ExtensionsCard.tsx index 448917290a..9f1d1d0e02 100644 --- a/src/components/views/right_panel/ExtensionsCard.tsx +++ b/src/components/views/right_panel/ExtensionsCard.tsx @@ -10,15 +10,17 @@ import React, { type JSX, useEffect, useMemo, useState } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; import { Button, Link, Separator, Text } from "@vector-im/compound-web"; -import PlusIcon from "@vector-im/compound-design-tokens/assets/web/icons/plus"; -import ExtensionsIcon from "@vector-im/compound-design-tokens/assets/web/icons/extensions"; +import { + PlusIcon, + ExtensionsIcon, + OverflowHorizontalIcon, + PinSolidIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import BaseCard from "./BaseCard"; import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils"; import { _t } from "../../../languageHandler"; -import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; -import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; -import UIStore from "../../../stores/UIStore"; +import { useContextMenu } from "../../structures/ContextMenu"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { type IApp } from "../../../stores/WidgetStore"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; @@ -29,6 +31,7 @@ import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import EmptyState from "./EmptyState"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents.ts"; import { UIComponent } from "../../../settings/UIFeature.ts"; +import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel.tsx"; interface Props { room: Room; @@ -65,21 +68,6 @@ const AppRow: React.FC = ({ app, room }) => { }; const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); - let contextMenu; - if (menuDisplayed) { - const rect = handle.current?.getBoundingClientRect(); - const rightMargin = rect?.right ?? 0; - const topMargin = rect?.top ?? 0; - contextMenu = ( - - ); - } const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top); @@ -104,7 +92,7 @@ const AppRow: React.FC = ({ app, room }) => { }); return ( -
    +
    = ({ app, room }) => { {canModifyWidget && ( - + + + } /> )} @@ -132,9 +129,9 @@ const AppRow: React.FC = ({ app, room }) => { onClick={togglePin} title={pinTitle} disabled={cannotPin} - /> - - {contextMenu} + > + +
    ); }; diff --git a/src/components/views/right_panel/RoomSummaryCardView.tsx b/src/components/views/right_panel/RoomSummaryCardView.tsx index 67eea91478..be316fdd14 100644 --- a/src/components/views/right_panel/RoomSummaryCardView.tsx +++ b/src/components/views/right_panel/RoomSummaryCardView.tsx @@ -39,7 +39,7 @@ import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error" import ErrorSolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; import { JoinRule, type Room } from "matrix-js-sdk/src/matrix"; -import { Box, Flex } from "@element-hq/web-shared-components"; +import { Box, Flex, HistoryVisibilityBadge } from "@element-hq/web-shared-components"; import BaseCard from "./BaseCard.tsx"; import { _t } from "../../../languageHandler.tsx"; @@ -165,34 +165,42 @@ const RoomSummaryCardView: React.FC = ({ {vm.alias} - + {!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && ( - + {_t("common|public_room")} )} {vm.isRoomEncrypted && vm.e2eStatus !== E2EStatus.Warning && ( - + {_t("common|encrypted")} )} {!vm.isRoomEncrypted && ( - + {_t("common|unencrypted")} )} {vm.e2eStatus === E2EStatus.Warning && ( - + {_t("common|not_trusted")} )} + + diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index 29a713fdd4..7ae9ae1b21 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -14,12 +14,11 @@ import BaseCard from "./BaseCard"; import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils"; import AppTile from "../elements/AppTile"; import { _t } from "../../../languageHandler"; -import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; -import { WidgetContextMenu } from "../context_menus/WidgetContextMenu"; +import { ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; -import UIStore from "../../../stores/UIStore"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import Heading from "../typography/Heading"; +import { WidgetContextMenu } from "../../../viewmodels/right-panel/WidgetContextMenuViewModel"; interface IProps { room: Room; @@ -46,34 +45,28 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { // Don't render anything as we are about to transition if (!app || !isRight) return null; - let contextMenu: JSX.Element | undefined; - if (menuDisplayed) { - const rect = handle.current?.getBoundingClientRect(); - const rightMargin = rect ? rect.right : 0; - const bottomMargin = rect ? rect.bottom : 0; - contextMenu = ( - - ); - } + const contextMenu: JSX.Element = ( + + } + onFinished={closeMenu} + app={app} + menuDisplayed={menuDisplayed} + /> + ); const header = (
    {WidgetUtils.getWidgetName(app)} - {contextMenu}
    ); diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index c7efbb6166..aa662bbdd8 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -416,7 +416,6 @@ export default class AliasSettings extends React.Component { {this.getLocalNonAltAliases().map((alias) => { return
    should match the snapshot 1`] = `
  • should support events with 1`] = `
  • + > + + + +
    should render dialog 1`] = ` class="mx_AccessibleButton mx_Dialog_cancelButton" role="button" tabindex="0" - /> + > + + + +
    +
    + + Use + + ↓ + + + ↑ + + to scroll + +
    +
    +
    + > + + + +
    + > + + + +
    Dog
    Cat
    -

    + > + + + +
    + > + + + +
    should render a single device - signed by owner 1`] = ` data-testid="e2e-icon" > should render a single device - unsigned 1`] = ` data-testid="e2e-icon" > should render a single device - verified by cross-signing 1`] data-testid="e2e-icon" > should render a single user 1`] = ` data-testid="e2e-icon" > should render a single user 1`] = ` data-testid="e2e-icon" > should render a single user 1`] = ` data-testid="e2e-icon" > should render a single user 1`] = ` data-testid="e2e-icon" > - Generate a Recovery Key
    @@ -83,9 +80,6 @@ exports[`CreateSecretStorageDialog handles the happy path 1`] = `
    - Enter a Security Phrase
    @@ -147,7 +141,7 @@ exports[`CreateSecretStorageDialog handles the happy path 2`] = ` class="mx_Dialog_header" >

    Save your Recovery Key diff --git a/test/unit-tests/components/views/dialogs/security/__snapshots__/ExportE2eKeysDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/security/__snapshots__/ExportE2eKeysDialog-test.tsx.snap index 25b1032e64..cfee3dd7d7 100644 --- a/test/unit-tests/components/views/dialogs/security/__snapshots__/ExportE2eKeysDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/security/__snapshots__/ExportE2eKeysDialog-test.tsx.snap @@ -102,7 +102,19 @@ exports[`ExportE2eKeysDialog renders 1`] = ` class="mx_AccessibleButton mx_Dialog_cancelButton" role="button" tabindex="0" - /> + > + + + +

    + > + + + +
    should display an error when recovery key is class="mx_AccessibleButton mx_Dialog_cancelButton" role="button" tabindex="0" - /> + > + + + +
    should not raise an error when recovery is v class="mx_AccessibleButton mx_Dialog_cancelButton" role="button" tabindex="0" - /> + > + + + +
    should render 1`] = ` class="mx_AccessibleButton mx_Dialog_cancelButton" role="button" tabindex="0" - /> + > + + + +
    should restore key backup when Recovery key class="mx_AccessibleButton mx_Dialog_cancelButton" role="button" tabindex="0" - /> + > + + + +
    should restore key backup when passphrase is class="mx_AccessibleButton mx_Dialog_cancelButton" role="button" tabindex="0" - /> + > + + + +
    should restore key backup when the key is ca class="mx_AccessibleButton mx_Dialog_cancelButton" role="button" tabindex="0" - /> + > + + + +
    should restore key backup when the key is in class="mx_AccessibleButton mx_Dialog_cancelButton" role="button" tabindex="0" - /> + > + + + +
    ", () => { + const getComponent = (props: ComponentProps = {}) => + render(); + + afterEach(() => { + SdkConfig.reset(); + jest.restoreAllMocks(); + }); + + it("renders nothing if the bug reporter is disabled", () => { + SdkConfig.put({ bug_report_endpoint_url: undefined }); + const { container } = getComponent({}); + expect(container).toBeEmptyDOMElement(); + }); + + it("renders 'submit' label if a URL is configured", () => { + SdkConfig.put({ bug_report_endpoint_url: "https://example.org" }); + const { container } = getComponent({}); + expect(container).toMatchSnapshot(); + }); + + it("renders 'download' label if 'loca' is configured", () => { + SdkConfig.put({ bug_report_endpoint_url: BugReportEndpointURLLocal }); + const { container } = getComponent({}); + expect(container).toMatchSnapshot(); + }); + + it("passes through props to dialog", async () => { + SdkConfig.put({ bug_report_endpoint_url: BugReportEndpointURLLocal }); + const spy = jest.spyOn(Modal, "createDialog"); + const { getByRole } = getComponent({ label: "a label", error: "an error" }); + await userEvent.click(getByRole("button")); + expect(spy).toHaveBeenCalledWith(BugReportDialog, { error: "an error", label: "a label" }); + }); +}); diff --git a/test/unit-tests/components/views/elements/ImageView-test.tsx b/test/unit-tests/components/views/elements/ImageView-test.tsx index 6537a3948a..4a5bd83710 100644 --- a/test/unit-tests/components/views/elements/ImageView-test.tsx +++ b/test/unit-tests/components/views/elements/ImageView-test.tsx @@ -9,7 +9,7 @@ import React from "react"; import { mocked } from "jest-mock"; import { render, fireEvent, waitFor } from "jest-matrix-react"; -import fetchMock from "fetch-mock-jest"; +import fetchMock from "@fetch-mock/jest"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import ImageView from "../../../../../src/components/views/elements/ImageView"; @@ -23,7 +23,6 @@ jest.mock("../../../../../src/utils/FileDownloader"); describe("", () => { beforeEach(() => { jest.resetAllMocks(); - fetchMock.reset(); }); it("renders correctly", () => { diff --git a/test/unit-tests/components/views/elements/ReplyChain-test.tsx b/test/unit-tests/components/views/elements/ReplyChain-test.tsx index 38f5684eb7..4f750869c6 100644 --- a/test/unit-tests/components/views/elements/ReplyChain-test.tsx +++ b/test/unit-tests/components/views/elements/ReplyChain-test.tsx @@ -1,81 +1,62 @@ /* -Copyright 2024 New Vector Ltd. -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2025 Element Creations Ltd. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ -import * as testUtils from "../../../../test-utils"; -import { getParentEventId } from "../../../../../src/utils/Reply"; +import React from "react"; +import { render, waitFor } from "jest-matrix-react"; + +import ReplyChain from "../../../../../src/components/views/elements/ReplyChain.tsx"; +import { mkEvent, stubClient, withClientContextRenderOptions } from "../../../../test-utils"; describe("ReplyChain", () => { - describe("getParentEventId", () => { - it("retrieves relation reply from unedited event", () => { - const originalEventWithRelation = testUtils.mkEvent({ - event: true, - type: "m.room.message", - content: { - "msgtype": "m.text", - "body": "> Reply to this message\n\n foo", - "m.relates_to": { - "m.in_reply_to": { - event_id: "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", - }, - }, - }, - user: "some_other_user", - room: "room_id", - }); + it("should call setQuoteExpanded if chain is longer than 2 lines", async () => { + // Jest/JSDOM won't set clientHeight/scrollHeight for us so we have to synthesise it + jest.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(100); + jest.spyOn(Element.prototype, "scrollHeight", "get").mockReturnValue(150); - expect(getParentEventId(originalEventWithRelation)).toStrictEqual( - "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", - ); + const cli = stubClient(); + const { room_id: roomId } = await cli.createRoom({}); + const room = cli.getRoom(roomId)!; + + const targetEv = mkEvent({ + event: true, + type: "m.room.message", + user: cli.getUserId()!, + room: roomId, + id: "$event1", + content: { + body: "A\nB\nC", + msgtype: "m.text", + }, }); + jest.spyOn(room, "findEventById").mockReturnValue(targetEv); - it("retrieves relation reply from original event when edited", () => { - const originalEventWithRelation = testUtils.mkEvent({ - event: true, - type: "m.room.message", - content: { - "msgtype": "m.text", - "body": "> Reply to this message\n\n foo", - "m.relates_to": { - "m.in_reply_to": { - event_id: "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", - }, + const parentEv = mkEvent({ + event: true, + type: "m.room.message", + user: cli.getUserId()!, + room: roomId, + id: "$event2", + content: { + "body": "Reply", + "msgtype": "m.text", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$event1", }, }, - user: "some_other_user", - room: "room_id", - }); - - const editEvent = testUtils.mkEvent({ - event: true, - type: "m.room.message", - content: { - "msgtype": "m.text", - "body": "> Reply to this message\n\n * foo bar", - "m.new_content": { - msgtype: "m.text", - body: "foo bar", - }, - "m.relates_to": { - rel_type: "m.replace", - event_id: originalEventWithRelation.getId(), - }, - }, - user: "some_other_user", - room: "room_id", - }); - - // The edit replaces the original event - originalEventWithRelation.makeReplaced(editEvent); - - // The relation should be pulled from the original event - expect(getParentEventId(originalEventWithRelation)).toStrictEqual( - "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", - ); + }, }); + const setQuoteExpanded = jest.fn(); + const { asFragment } = render( + , + withClientContextRenderOptions(cli), + ); + + await waitFor(() => expect(setQuoteExpanded).toHaveBeenCalledWith(false)); + expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/test/unit-tests/components/views/elements/__snapshots__/AppTile-test.tsx.snap b/test/unit-tests/components/views/elements/__snapshots__/AppTile-test.tsx.snap index 3c88a63cb6..d3dd88572f 100644 --- a/test/unit-tests/components/views/elements/__snapshots__/AppTile-test.tsx.snap +++ b/test/unit-tests/components/views/elements/__snapshots__/AppTile-test.tsx.snap @@ -26,13 +26,16 @@ exports[`AppTile destroys non-persisted right panel widget on room change 1`] = aria-haspopup="true" aria-label="Options" class="mx_AccessibleButton mx_BaseCard_header_title_button--option" + data-state="closed" + id="radix-_r_0_" role="button" tabindex="0" + type="button" />
    + +`; + +exports[` renders 'submit' label if a URL is configured 1`] = ` +
    + +
    +`; diff --git a/test/unit-tests/components/views/elements/__snapshots__/ImageView-test.tsx.snap b/test/unit-tests/components/views/elements/__snapshots__/ImageView-test.tsx.snap index 40af301ccf..34df12f2c0 100644 --- a/test/unit-tests/components/views/elements/__snapshots__/ImageView-test.tsx.snap +++ b/test/unit-tests/components/views/elements/__snapshots__/ImageView-test.tsx.snap @@ -23,40 +23,154 @@ exports[` renders correctly 1`] = `
    + > + + + + +
    + > + + + + + + + + + + + + + +
    + > + + + +
    + > + + + + + + + + + + +
    + > + + + +
    + > + + + +
    should render with byline of "this is a byline" 1` class="_inline-field-control_19upo_44" >
    should render with byline of undefined 1`] = ` class="_inline-field-control_19upo_44" >
    + > + + + +
    @@ -60,6 +72,18 @@ exports[`PollCreateDialog renders a blank poll 1`] = `

    Voters see results as soon as they have voted @@ -111,7 +135,19 @@ exports[`PollCreateDialog renders a blank poll 1`] = ` class="mx_AccessibleButton mx_PollCreateDialog_removeOption" role="button" tabindex="0" - /> + > + + + +

    + > + + + +
    + > + + + +
    @@ -239,6 +299,18 @@ exports[`PollCreateDialog renders a question and some options 1`] = `