redesigned app settings and switch to rust crypto (#1988)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Mon, 10 Feb 2025 05:49:47 +0000 (16:49 +1100)
committerGitHub <noreply@github.com>
Mon, 10 Feb 2025 05:49:47 +0000 (16:49 +1100)
* rework general settings

* account settings - WIP

* add missing key prop

* add object url hook

* extract wide modal styles

* profile settings and image editor - WIP

* add outline style to upload card

* remove file param from bind upload atom hook

* add compact variant to upload card

* add  compact upload card renderer

* add option to update profile avatar

* add option to change profile displayname

* allow displayname change based on capabilities check

* rearrange settings components into folders

* add system notification settings

* add initial page param in settings

* convert account data hook to typescript

* add push rule hook

* add notification mode hook

* add notification mode switcher component

* add all messages notification settings options

* add special messages notification settings

* add keyword notifications

* add ignored users section

* improve ignore user list strings

* add about settings

* add access token option in about settings

* add developer tools settings

* add expand button to account data dev tool option

* update folds

* fix editable active element textarea check

* do not close dialog when editable element in focus

* add text area plugins

* add text area intent handler hook

* add newline intent mod in text area

* add next line hotkey in text area intent hook

* add syntax error position dom utility function

* add account data editor

* add button to send new account data in dev tools

* improve custom emoji plugin

* add more custom emojis hooks

* add text util css

* add word break in setting tile title and description

* emojis and sticker user settings - WIP

* view image packs from settings

* emoji pack editing - WIP

* add option to edit pack meta

* change saved changes message

* add image edit and delete controls

* add option to upload pack images and apply changes

* fix state event type when updating image pack

* lazy load pack image tile img

* hide upload image button when user can not edit pack

* add option to add or remove global image packs

* upgrade to rust crypto (#2168)

* update matrix js sdk

* remove dead code

* use rust crypto

* update setPowerLevel usage

* fix types

* fix deprecated isRoomEncrypted method uses

* fix deprecated room.currentState uses

* fix deprecated import/export room keys func

* fix merge issues in image pack file

* fix remaining issues in image pack file

* start indexedDBStore

* update package lock and vite-plugin-top-level-await

* user session settings - WIP

* add useAsync hook

* add password stage uia

* add uia flow matrix error hook

* add UIA action component

* add options to delete sessions

* add sso uia stage

* fix SSO stage complete error

* encryption - WIP

* update user settings encryption terminology

* add default variant to password input

* use password input in uia password stage

* add options for local backup in user settings

* remove typo in import local backup password input label

* online backup - WIP

* fix uia sso action

* move access token settings from about to developer tools

* merge encryption tab into sessions and rename it to devices

* add device placeholder tile

* add logout dialog

* add logout button for current device

* move other devices in component

* render unverified device verification tile

* add learn more section for current device verification

* add device verification status badge

* add info card component

* add index file for password input component

* add types for secret storage

* add component to access secret storage key

* manual verification - WIP

* update matrix-js-sdk to v35

* add manual verification

* use react query for device list

* show unverified tab on sidebar

* fix device list updates

* add session key details to current device

* render restore encryption backup

* fix loading state of restore backup

* fix unverified tab settings closes after verification

* key backup tile - WIP

* fix unverified tab badge

* rename session key to device key in device tile

* improve backup restore functionality

* fix restore button enabled after layout reload during restoring backup

* update backup info on status change

* add backup disconnection failures

* add device verification using sas

* restore backup after verification

* show option to logout on startup error screen

* fix key backup hook update on decryption key cached

* add option to enable device verification

* add device verification reset dialog

* add logout button in settings drawer

* add encrypted message lost on logout

* fix backup restore never finish with 0 keys

* fix setup dialog hides when enabling device verification

* show backup details in menu

* update setup device verification body copy

* replace deprecated method

* fix displayname appear as mxid in settings

* remove old refactored codes

* fix types

196 files changed:
package-lock.json
package.json
src/app/components/ActionUIA.tsx [new file with mode: 0644]
src/app/components/BackupRestore.tsx [new file with mode: 0644]
src/app/components/CapabilitiesAndMediaConfigLoader.tsx
src/app/components/CapabilitiesLoader.tsx
src/app/components/DeviceVerification.tsx [new file with mode: 0644]
src/app/components/DeviceVerificationSetup.tsx [new file with mode: 0644]
src/app/components/DeviceVerificationStatus.ts [new file with mode: 0644]
src/app/components/LogoutDialog.tsx [new file with mode: 0644]
src/app/components/ManualVerification.tsx [new file with mode: 0644]
src/app/components/Modal500.tsx [new file with mode: 0644]
src/app/components/SecretStorage.tsx [new file with mode: 0644]
src/app/components/UIAFlowOverlay.tsx
src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
src/app/components/emoji-board/EmojiBoard.tsx
src/app/components/image-editor/ImageEditor.css.ts [new file with mode: 0644]
src/app/components/image-editor/ImageEditor.tsx [new file with mode: 0644]
src/app/components/image-editor/index.ts [new file with mode: 0644]
src/app/components/image-pack-view/ImagePackContent.tsx [new file with mode: 0644]
src/app/components/image-pack-view/ImagePackView.tsx [new file with mode: 0644]
src/app/components/image-pack-view/ImageTile.tsx [new file with mode: 0644]
src/app/components/image-pack-view/PackMeta.tsx [new file with mode: 0644]
src/app/components/image-pack-view/RoomImagePack.tsx [new file with mode: 0644]
src/app/components/image-pack-view/UsageSwitcher.tsx [new file with mode: 0644]
src/app/components/image-pack-view/UserImagePack.tsx [new file with mode: 0644]
src/app/components/image-pack-view/index.ts [new file with mode: 0644]
src/app/components/image-pack-view/style.css.ts [new file with mode: 0644]
src/app/components/info-card/InfoCard.tsx [new file with mode: 0644]
src/app/components/info-card/index.ts [new file with mode: 0644]
src/app/components/info-card/styles.css.ts [new file with mode: 0644]
src/app/components/message/content/EventContent.tsx
src/app/components/message/content/FileContent.tsx
src/app/components/message/content/ImageContent.tsx
src/app/components/message/content/style.css.ts
src/app/components/page/Page.tsx
src/app/components/page/style.css.ts
src/app/components/password-input/PasswordInput.tsx
src/app/components/password-input/index.ts [new file with mode: 0644]
src/app/components/setting-tile/SettingTile.tsx [new file with mode: 0644]
src/app/components/setting-tile/index.ts [new file with mode: 0644]
src/app/components/uia-stages/PasswordStage.tsx [new file with mode: 0644]
src/app/components/uia-stages/SSOStage.tsx [new file with mode: 0644]
src/app/components/uia-stages/index.ts
src/app/components/upload-card/CompactUploadCardRenderer.tsx [new file with mode: 0644]
src/app/components/upload-card/UploadCard.css.ts
src/app/components/upload-card/UploadCard.tsx
src/app/components/upload-card/UploadCardRenderer.tsx
src/app/components/upload-card/index.ts
src/app/features/lobby/Lobby.tsx
src/app/features/room/RoomInput.tsx
src/app/features/room/RoomTimeline.tsx
src/app/features/room/message/Message.tsx
src/app/features/settings/Settings.tsx [new file with mode: 0644]
src/app/features/settings/about/About.tsx [new file with mode: 0644]
src/app/features/settings/about/index.ts [new file with mode: 0644]
src/app/features/settings/account/Account.tsx [new file with mode: 0644]
src/app/features/settings/account/index.ts [new file with mode: 0644]
src/app/features/settings/developer-tools/AccountDataEditor.tsx [new file with mode: 0644]
src/app/features/settings/developer-tools/DevelopTools.tsx [new file with mode: 0644]
src/app/features/settings/developer-tools/index.ts [new file with mode: 0644]
src/app/features/settings/developer-tools/styles.css.ts [new file with mode: 0644]
src/app/features/settings/devices/DeviceTile.tsx [new file with mode: 0644]
src/app/features/settings/devices/Devices.tsx [new file with mode: 0644]
src/app/features/settings/devices/LocalBackup.tsx [new file with mode: 0644]
src/app/features/settings/devices/OtherDevices.tsx [new file with mode: 0644]
src/app/features/settings/devices/Verification.tsx [new file with mode: 0644]
src/app/features/settings/devices/index.ts [new file with mode: 0644]
src/app/features/settings/emojis-stickers/EmojisStickers.tsx [new file with mode: 0644]
src/app/features/settings/emojis-stickers/GlobalPacks.tsx [new file with mode: 0644]
src/app/features/settings/emojis-stickers/UserPack.tsx [new file with mode: 0644]
src/app/features/settings/emojis-stickers/index.ts [new file with mode: 0644]
src/app/features/settings/general/General.tsx [new file with mode: 0644]
src/app/features/settings/general/index.ts [new file with mode: 0644]
src/app/features/settings/index.ts [new file with mode: 0644]
src/app/features/settings/notifications/AllMessages.tsx [new file with mode: 0644]
src/app/features/settings/notifications/IgnoredUserList.tsx [new file with mode: 0644]
src/app/features/settings/notifications/KeywordMessages.tsx [new file with mode: 0644]
src/app/features/settings/notifications/NotificationModeSwitcher.tsx [new file with mode: 0644]
src/app/features/settings/notifications/Notifications.tsx [new file with mode: 0644]
src/app/features/settings/notifications/SpecialMessages.tsx [new file with mode: 0644]
src/app/features/settings/notifications/index.ts [new file with mode: 0644]
src/app/features/settings/styles.css.ts [new file with mode: 0644]
src/app/hooks/useAccountData.js [deleted file]
src/app/hooks/useAccountData.ts [new file with mode: 0644]
src/app/hooks/useAsyncCallback.ts
src/app/hooks/useCrossSigning.ts [new file with mode: 0644]
src/app/hooks/useCrossSigningStatus.js [deleted file]
src/app/hooks/useDeviceList.ts
src/app/hooks/useDeviceVerificationStatus.ts [new file with mode: 0644]
src/app/hooks/useImagePacks.ts
src/app/hooks/useKeyBackup.ts [new file with mode: 0644]
src/app/hooks/useMessageLayout.ts [new file with mode: 0644]
src/app/hooks/useMessageSpacing.ts [new file with mode: 0644]
src/app/hooks/useNotificationMode.ts [new file with mode: 0644]
src/app/hooks/useObjectURL.ts [new file with mode: 0644]
src/app/hooks/usePushRule.ts [new file with mode: 0644]
src/app/hooks/useRestoreBackupOnVerification.ts [new file with mode: 0644]
src/app/hooks/useSecretStorage.ts [new file with mode: 0644]
src/app/hooks/useTextAreaIntent.ts [new file with mode: 0644]
src/app/hooks/useTheme.ts [new file with mode: 0644]
src/app/hooks/useUIAFlows.ts
src/app/hooks/useUserProfile.ts [new file with mode: 0644]
src/app/hooks/useUserTrustStatusChange.ts [new file with mode: 0644]
src/app/hooks/useVerificationRequest.ts [new file with mode: 0644]
src/app/molecules/global-notification/GlobalNotification.jsx [deleted file]
src/app/molecules/global-notification/IgnoreUserList.jsx [deleted file]
src/app/molecules/global-notification/IgnoreUserList.scss [deleted file]
src/app/molecules/global-notification/KeywordNotification.jsx [deleted file]
src/app/molecules/global-notification/KeywordNotification.scss [deleted file]
src/app/molecules/global-notification/NotificationSelector.jsx [deleted file]
src/app/molecules/image-pack/ImagePack.jsx
src/app/molecules/import-export-e2e-room-keys/ExportE2ERoomKeys.jsx [deleted file]
src/app/molecules/import-export-e2e-room-keys/ExportE2ERoomKeys.scss [deleted file]
src/app/molecules/import-export-e2e-room-keys/ImportE2ERoomKeys.jsx [deleted file]
src/app/molecules/import-export-e2e-room-keys/ImportE2ERoomKeys.scss [deleted file]
src/app/molecules/room-aliases/RoomAliases.jsx
src/app/molecules/room-emojis/RoomEmojis.jsx
src/app/molecules/room-encryption/RoomEncryption.jsx
src/app/molecules/room-permissions/RoomPermissions.jsx
src/app/molecules/room-visibility/RoomVisibility.jsx
src/app/organisms/emoji-verification/EmojiVerification.jsx [deleted file]
src/app/organisms/emoji-verification/EmojiVerification.scss [deleted file]
src/app/organisms/profile-editor/ProfileEditor.jsx [deleted file]
src/app/organisms/profile-editor/ProfileEditor.scss [deleted file]
src/app/organisms/profile-viewer/ProfileViewer.jsx
src/app/organisms/pw/Dialogs.jsx
src/app/organisms/pw/Windows.jsx
src/app/organisms/settings/AuthRequest.jsx [deleted file]
src/app/organisms/settings/AuthRequest.scss [deleted file]
src/app/organisms/settings/CrossSigning.jsx [deleted file]
src/app/organisms/settings/CrossSigning.scss [deleted file]
src/app/organisms/settings/DeviceManage.jsx [deleted file]
src/app/organisms/settings/DeviceManage.scss [deleted file]
src/app/organisms/settings/KeyBackup.jsx [deleted file]
src/app/organisms/settings/KeyBackup.scss [deleted file]
src/app/organisms/settings/SecretStorageAccess.jsx [deleted file]
src/app/organisms/settings/SecretStorageAccess.scss [deleted file]
src/app/organisms/settings/Settings.jsx [deleted file]
src/app/organisms/settings/Settings.scss [deleted file]
src/app/pages/Router.tsx
src/app/pages/ThemeManager.tsx [new file with mode: 0644]
src/app/pages/auth/login/PasswordLoginForm.tsx
src/app/pages/auth/register/PasswordRegisterForm.tsx
src/app/pages/auth/reset-password/PasswordResetForm.tsx
src/app/pages/client/ClientRoot.tsx
src/app/pages/client/SidebarNav.tsx
src/app/pages/client/sidebar/SettingsTab.tsx [new file with mode: 0644]
src/app/pages/client/sidebar/UnverifiedTab.css.ts
src/app/pages/client/sidebar/UnverifiedTab.tsx
src/app/pages/client/sidebar/UserTab.tsx [deleted file]
src/app/pages/client/sidebar/index.ts
src/app/pages/client/space/Space.tsx
src/app/pages/paths.ts
src/app/plugins/custom-emoji.ts [deleted file]
src/app/plugins/custom-emoji/ImagePack.ts [new file with mode: 0644]
src/app/plugins/custom-emoji/PackAddress.ts [new file with mode: 0644]
src/app/plugins/custom-emoji/PackImageReader.ts [new file with mode: 0644]
src/app/plugins/custom-emoji/PackImagesReader.ts [new file with mode: 0644]
src/app/plugins/custom-emoji/PackMetaReader.ts [new file with mode: 0644]
src/app/plugins/custom-emoji/index.ts [new file with mode: 0644]
src/app/plugins/custom-emoji/types.ts [new file with mode: 0644]
src/app/plugins/custom-emoji/utils.ts [new file with mode: 0644]
src/app/plugins/react-prism/ReactPrism.tsx
src/app/plugins/text-area/Cursor.ts [new file with mode: 0644]
src/app/plugins/text-area/Operations.ts [new file with mode: 0644]
src/app/plugins/text-area/TextArea.ts [new file with mode: 0644]
src/app/plugins/text-area/TextAreaOperations.ts [new file with mode: 0644]
src/app/plugins/text-area/TextUtils.ts [new file with mode: 0644]
src/app/plugins/text-area/index.ts [new file with mode: 0644]
src/app/plugins/text-area/mods/Intent.ts [new file with mode: 0644]
src/app/plugins/text-area/mods/index.ts [new file with mode: 0644]
src/app/plugins/text-area/type.ts [new file with mode: 0644]
src/app/state/backupRestore.ts [new file with mode: 0644]
src/app/state/settings.ts
src/app/state/upload.ts
src/app/styles/Modal.css.ts [new file with mode: 0644]
src/app/styles/Text.css.ts [new file with mode: 0644]
src/app/utils/common.ts
src/app/utils/dom.ts
src/app/utils/keyboard.ts
src/app/utils/matrix-crypto.ts [new file with mode: 0644]
src/app/utils/matrix.ts
src/app/utils/mimeTypes.ts
src/app/utils/room.ts
src/client/action/navigation.js
src/client/action/room.js
src/client/action/settings.js [deleted file]
src/client/initMatrix.ts
src/client/state/cons.js
src/client/state/navigation.js
src/client/state/settings.js [deleted file]
src/colors.css.ts
src/index.tsx
src/types/matrix/accountData.ts
src/util/matrixUtil.js

index 7fb554e7e8e3d4483748712a5d3adf400d695614..523ab649d62777babf1c5e114a86ed558f3019e6 100644 (file)
@@ -33,7 +33,7 @@
         "file-saver": "2.0.5",
         "flux": "4.0.3",
         "focus-trap-react": "10.0.2",
-        "folds": "2.0.0",
+        "folds": "2.1.0",
         "formik": "2.4.6",
         "html-dom-parser": "4.0.0",
         "html-react-parser": "4.2.0",
@@ -45,7 +45,7 @@
         "jotai": "2.6.0",
         "linkify-react": "4.1.3",
         "linkifyjs": "4.1.3",
-        "matrix-js-sdk": "34.11.1",
+        "matrix-js-sdk": "35.0.0",
         "millify": "6.1.0",
         "pdfjs-dist": "4.2.67",
         "prismjs": "1.29.0",
         "vite": "5.0.13",
         "vite-plugin-pwa": "0.20.5",
         "vite-plugin-static-copy": "1.0.4",
-        "vite-plugin-top-level-await": "1.4.1"
+        "vite-plugin-top-level-await": "1.4.4"
       },
       "engines": {
         "node": ">=16.0.0"
       }
     },
     "node_modules/@ampproject/remapping": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
-      "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+      "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
       "dependencies": {
-        "@jridgewell/gen-mapping": "^0.1.0",
-        "@jridgewell/trace-mapping": "^0.3.9"
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
       },
       "engines": {
         "node": ">=6.0.0"
       "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz",
       "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "json-schema": "^0.4.0",
         "jsonpointer": "^5.0.0",
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
-      "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
-      "license": "MIT",
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+      "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
       "dependencies": {
-        "@babel/highlight": "^7.24.7",
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "js-tokens": "^4.0.0",
         "picocolors": "^1.0.0"
       },
       "engines": {
       }
     },
     "node_modules/@babel/compat-data": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
-      "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
-      "license": "MIT",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz",
+      "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/core": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
-      "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
-      "license": "MIT",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
+      "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
       "dependencies": {
         "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.24.7",
-        "@babel/generator": "^7.25.0",
-        "@babel/helper-compilation-targets": "^7.25.2",
-        "@babel/helper-module-transforms": "^7.25.2",
-        "@babel/helpers": "^7.25.0",
-        "@babel/parser": "^7.25.0",
-        "@babel/template": "^7.25.0",
-        "@babel/traverse": "^7.25.2",
-        "@babel/types": "^7.25.2",
+        "@babel/code-frame": "^7.26.0",
+        "@babel/generator": "^7.26.0",
+        "@babel/helper-compilation-targets": "^7.25.9",
+        "@babel/helper-module-transforms": "^7.26.0",
+        "@babel/helpers": "^7.26.0",
+        "@babel/parser": "^7.26.0",
+        "@babel/template": "^7.25.9",
+        "@babel/traverse": "^7.25.9",
+        "@babel/types": "^7.26.0",
         "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
         "url": "https://opencollective.com/babel"
       }
     },
+    "node_modules/@babel/core/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/@babel/generator": {
-      "version": "7.25.6",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz",
-      "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==",
-      "license": "MIT",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
+      "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
       "dependencies": {
-        "@babel/types": "^7.25.6",
+        "@babel/parser": "^7.26.5",
+        "@babel/types": "^7.26.5",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
-        "jsesc": "^2.5.1"
+        "jsesc": "^3.0.2"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
-      "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
-      "license": "MIT",
-      "dependencies": {
-        "@jridgewell/set-array": "^1.2.1",
-        "@jridgewell/sourcemap-codec": "^1.4.10",
-        "@jridgewell/trace-mapping": "^0.3.24"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
     "node_modules/@babel/helper-annotate-as-pure": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz",
-      "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz",
-      "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz",
+      "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/traverse": "^7.24.7",
-        "@babel/types": "^7.24.7"
+        "@babel/types": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-compilation-targets": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
-      "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/compat-data": "^7.25.2",
-        "@babel/helper-validator-option": "^7.24.8",
-        "browserslist": "^4.23.1",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz",
+      "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
+      "dependencies": {
+        "@babel/compat-data": "^7.26.5",
+        "@babel/helper-validator-option": "^7.25.9",
+        "browserslist": "^4.24.0",
         "lru-cache": "^5.1.1",
         "semver": "^6.3.1"
       },
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
-      "license": "ISC",
-      "dependencies": {
-        "yallist": "^3.0.2"
+    "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "bin": {
+        "semver": "bin/semver.js"
       }
     },
-    "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-      "license": "ISC"
-    },
     "node_modules/@babel/helper-create-class-features-plugin": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz",
-      "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-annotate-as-pure": "^7.24.7",
-        "@babel/helper-member-expression-to-functions": "^7.24.8",
-        "@babel/helper-optimise-call-expression": "^7.24.7",
-        "@babel/helper-replace-supers": "^7.25.0",
-        "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
-        "@babel/traverse": "^7.25.4",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz",
+      "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.25.9",
+        "@babel/helper-member-expression-to-functions": "^7.25.9",
+        "@babel/helper-optimise-call-expression": "^7.25.9",
+        "@babel/helper-replace-supers": "^7.25.9",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
+        "@babel/traverse": "^7.25.9",
         "semver": "^6.3.1"
       },
       "engines": {
         "@babel/core": "^7.0.0"
       }
     },
+    "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/@babel/helper-create-regexp-features-plugin": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz",
-      "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==",
+      "version": "7.26.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz",
+      "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-annotate-as-pure": "^7.24.7",
-        "regexpu-core": "^5.3.1",
+        "@babel/helper-annotate-as-pure": "^7.25.9",
+        "regexpu-core": "^6.2.0",
         "semver": "^6.3.1"
       },
       "engines": {
         "@babel/core": "^7.0.0"
       }
     },
+    "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/@babel/helper-define-polyfill-provider": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz",
-      "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==",
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz",
+      "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@babel/helper-compilation-targets": "^7.22.6",
         "@babel/helper-plugin-utils": "^7.22.5",
       }
     },
     "node_modules/@babel/helper-member-expression-to-functions": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz",
-      "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
+      "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/traverse": "^7.24.8",
-        "@babel/types": "^7.24.8"
+        "@babel/traverse": "^7.25.9",
+        "@babel/types": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-module-imports": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz",
-      "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==",
-      "license": "MIT",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+      "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
       "dependencies": {
-        "@babel/traverse": "^7.24.7",
-        "@babel/types": "^7.24.7"
+        "@babel/traverse": "^7.25.9",
+        "@babel/types": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-module-transforms": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz",
-      "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==",
-      "license": "MIT",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
+      "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
       "dependencies": {
-        "@babel/helper-module-imports": "^7.24.7",
-        "@babel/helper-simple-access": "^7.24.7",
-        "@babel/helper-validator-identifier": "^7.24.7",
-        "@babel/traverse": "^7.25.2"
+        "@babel/helper-module-imports": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "@babel/traverse": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-optimise-call-expression": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz",
-      "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz",
+      "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.24.7"
+        "@babel/types": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-plugin-utils": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz",
-      "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==",
-      "license": "MIT",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
+      "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-remap-async-to-generator": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz",
-      "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz",
+      "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-annotate-as-pure": "^7.24.7",
-        "@babel/helper-wrap-function": "^7.25.0",
-        "@babel/traverse": "^7.25.0"
+        "@babel/helper-annotate-as-pure": "^7.25.9",
+        "@babel/helper-wrap-function": "^7.25.9",
+        "@babel/traverse": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-replace-supers": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz",
-      "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz",
+      "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-member-expression-to-functions": "^7.24.8",
-        "@babel/helper-optimise-call-expression": "^7.24.7",
-        "@babel/traverse": "^7.25.0"
+        "@babel/helper-member-expression-to-functions": "^7.25.9",
+        "@babel/helper-optimise-call-expression": "^7.25.9",
+        "@babel/traverse": "^7.26.5"
       },
       "engines": {
         "node": ">=6.9.0"
         "@babel/core": "^7.0.0"
       }
     },
-    "node_modules/@babel/helper-simple-access": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz",
-      "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/traverse": "^7.24.7",
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
     "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz",
-      "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz",
+      "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/traverse": "^7.24.7",
-        "@babel/types": "^7.24.7"
+        "@babel/traverse": "^7.25.9",
+        "@babel/types": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
-      "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
-      "license": "MIT",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
-      "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
-      "license": "MIT",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-option": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
-      "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
-      "license": "MIT",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
+      "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-wrap-function": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz",
-      "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz",
+      "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/template": "^7.25.0",
-        "@babel/traverse": "^7.25.0",
-        "@babel/types": "^7.25.0"
+        "@babel/template": "^7.25.9",
+        "@babel/traverse": "^7.25.9",
+        "@babel/types": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.25.6",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
-      "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/template": "^7.25.0",
-        "@babel/types": "^7.25.6"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/highlight": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
-      "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
-      "license": "MIT",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
+      "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.24.7",
-        "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0",
-        "picocolors": "^1.0.0"
+        "@babel/template": "^7.25.9",
+        "@babel/types": "^7.26.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.25.6",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
-      "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
-      "license": "MIT",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz",
+      "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==",
       "dependencies": {
-        "@babel/types": "^7.25.6"
+        "@babel/types": "^7.26.5"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
       }
     },
     "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
-      "version": "7.25.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz",
-      "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz",
+      "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8",
-        "@babel/traverse": "^7.25.3"
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/traverse": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz",
-      "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz",
+      "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz",
-      "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz",
+      "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz",
-      "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz",
+      "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
-        "@babel/plugin-transform-optional-chaining": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
+        "@babel/plugin-transform-optional-chaining": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz",
-      "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz",
+      "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8",
-        "@babel/traverse": "^7.25.0"
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/traverse": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
       "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
       "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-async-generators": {
-      "version": "7.8.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
-      "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.8.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-class-properties": {
-      "version": "7.12.13",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
-      "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.12.13"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-class-static-block": {
-      "version": "7.14.5",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
-      "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.14.5"
-      },
       "engines": {
         "node": ">=6.9.0"
       },
         "@babel/core": "^7.0.0-0"
       }
     },
-    "node_modules/@babel/plugin-syntax-dynamic-import": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
-      "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.8.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-export-namespace-from": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz",
-      "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.8.3"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
     "node_modules/@babel/plugin-syntax-import-assertions": {
-      "version": "7.25.6",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz",
-      "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz",
+      "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-syntax-import-attributes": {
-      "version": "7.25.6",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz",
-      "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-import-meta": {
-      "version": "7.10.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
-      "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.10.4"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-json-strings": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
-      "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.8.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
-      "version": "7.10.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
-      "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.10.4"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
-      "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.8.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-numeric-separator": {
-      "version": "7.10.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
-      "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.10.4"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-object-rest-spread": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
-      "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.8.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-optional-catch-binding": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
-      "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.8.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-optional-chaining": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
-      "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.8.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-private-property-in-object": {
-      "version": "7.14.5",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
-      "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.14.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-syntax-top-level-await": {
-      "version": "7.14.5",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
-      "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz",
+      "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.14.5"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-syntax-typescript": {
-      "version": "7.20.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz",
-      "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz",
+      "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.19.0"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
       "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@babel/helper-create-regexp-features-plugin": "^7.18.6",
         "@babel/helper-plugin-utils": "^7.18.6"
       }
     },
     "node_modules/@babel/plugin-transform-arrow-functions": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz",
-      "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz",
+      "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-async-generator-functions": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz",
-      "integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz",
+      "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8",
-        "@babel/helper-remap-async-to-generator": "^7.25.0",
-        "@babel/plugin-syntax-async-generators": "^7.8.4",
-        "@babel/traverse": "^7.25.4"
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-remap-async-to-generator": "^7.25.9",
+        "@babel/traverse": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-async-to-generator": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz",
-      "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz",
+      "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-module-imports": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/helper-remap-async-to-generator": "^7.24.7"
+        "@babel/helper-module-imports": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-remap-async-to-generator": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-block-scoped-functions": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz",
-      "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz",
+      "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.26.5"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-block-scoping": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz",
-      "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz",
+      "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-class-properties": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz",
-      "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz",
+      "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-create-class-features-plugin": "^7.25.4",
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-create-class-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-class-static-block": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz",
-      "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz",
+      "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-create-class-features-plugin": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-class-static-block": "^7.14.5"
+        "@babel/helper-create-class-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-classes": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz",
-      "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz",
+      "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-annotate-as-pure": "^7.24.7",
-        "@babel/helper-compilation-targets": "^7.25.2",
-        "@babel/helper-plugin-utils": "^7.24.8",
-        "@babel/helper-replace-supers": "^7.25.0",
-        "@babel/traverse": "^7.25.4",
+        "@babel/helper-annotate-as-pure": "^7.25.9",
+        "@babel/helper-compilation-targets": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-replace-supers": "^7.25.9",
+        "@babel/traverse": "^7.25.9",
         "globals": "^11.1.0"
       },
       "engines": {
       }
     },
     "node_modules/@babel/plugin-transform-computed-properties": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz",
-      "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz",
+      "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/template": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/template": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-destructuring": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz",
-      "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz",
+      "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-dotall-regex": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz",
-      "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz",
+      "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-create-regexp-features-plugin": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-duplicate-keys": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz",
-      "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz",
+      "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz",
-      "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz",
+      "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-create-regexp-features-plugin": "^7.25.0",
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-dynamic-import": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz",
-      "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz",
+      "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-dynamic-import": "^7.8.3"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-exponentiation-operator": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz",
-      "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==",
+      "version": "7.26.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz",
+      "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-export-namespace-from": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz",
-      "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz",
+      "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-export-namespace-from": "^7.8.3"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-for-of": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz",
-      "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz",
+      "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-function-name": {
-      "version": "7.25.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz",
-      "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz",
+      "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-compilation-targets": "^7.24.8",
-        "@babel/helper-plugin-utils": "^7.24.8",
-        "@babel/traverse": "^7.25.1"
+        "@babel/helper-compilation-targets": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/traverse": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-json-strings": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz",
-      "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz",
+      "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-json-strings": "^7.8.3"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-literals": {
-      "version": "7.25.2",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz",
-      "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz",
+      "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-logical-assignment-operators": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz",
-      "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz",
+      "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-member-expression-literals": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz",
-      "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz",
+      "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-modules-amd": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz",
-      "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz",
+      "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-module-transforms": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-module-transforms": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-modules-commonjs": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz",
-      "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==",
+      "version": "7.26.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz",
+      "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-module-transforms": "^7.24.8",
-        "@babel/helper-plugin-utils": "^7.24.8",
-        "@babel/helper-simple-access": "^7.24.7"
+        "@babel/helper-module-transforms": "^7.26.0",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-modules-systemjs": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz",
-      "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz",
+      "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-module-transforms": "^7.25.0",
-        "@babel/helper-plugin-utils": "^7.24.8",
-        "@babel/helper-validator-identifier": "^7.24.7",
-        "@babel/traverse": "^7.25.0"
+        "@babel/helper-module-transforms": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "@babel/traverse": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-modules-umd": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz",
-      "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz",
+      "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-module-transforms": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-module-transforms": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz",
-      "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz",
+      "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-create-regexp-features-plugin": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-new-target": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz",
-      "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz",
+      "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz",
-      "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==",
+      "version": "7.26.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz",
+      "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
+        "@babel/helper-plugin-utils": "^7.26.5"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-numeric-separator": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz",
-      "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz",
+      "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-numeric-separator": "^7.10.4"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-object-rest-spread": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz",
-      "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz",
+      "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-compilation-targets": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
-        "@babel/plugin-transform-parameters": "^7.24.7"
+        "@babel/helper-compilation-targets": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/plugin-transform-parameters": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-object-super": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz",
-      "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz",
+      "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/helper-replace-supers": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-replace-supers": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-optional-catch-binding": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz",
-      "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz",
+      "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-optional-chaining": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz",
-      "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz",
+      "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8",
-        "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
-        "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-parameters": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz",
-      "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz",
+      "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-private-methods": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz",
-      "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz",
+      "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-create-class-features-plugin": "^7.25.4",
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-create-class-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-private-property-in-object": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz",
-      "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz",
+      "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-annotate-as-pure": "^7.24.7",
-        "@babel/helper-create-class-features-plugin": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
+        "@babel/helper-annotate-as-pure": "^7.25.9",
+        "@babel/helper-create-class-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-property-literals": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz",
-      "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz",
+      "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-react-jsx-self": {
-      "version": "7.23.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz",
-      "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
+      "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.22.5"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-react-jsx-source": {
-      "version": "7.23.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz",
-      "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
+      "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.22.5"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-regenerator": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz",
-      "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz",
+      "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
+        "@babel/helper-plugin-utils": "^7.25.9",
         "regenerator-transform": "^0.15.2"
       },
       "engines": {
         "@babel/core": "^7.0.0-0"
       }
     },
+    "node_modules/@babel/plugin-transform-regexp-modifiers": {
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz",
+      "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
     "node_modules/@babel/plugin-transform-reserved-words": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz",
-      "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz",
+      "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-shorthand-properties": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz",
-      "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz",
+      "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-spread": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz",
-      "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz",
+      "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7",
-        "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-sticky-regex": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz",
-      "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz",
+      "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-template-literals": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz",
-      "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz",
+      "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-typeof-symbol": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz",
-      "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz",
+      "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-unicode-escapes": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz",
-      "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz",
+      "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-unicode-property-regex": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz",
-      "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz",
+      "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-create-regexp-features-plugin": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-unicode-regex": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz",
-      "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz",
+      "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-create-regexp-features-plugin": "^7.24.7",
-        "@babel/helper-plugin-utils": "^7.24.7"
+        "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/plugin-transform-unicode-sets-regex": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz",
-      "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz",
+      "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-create-regexp-features-plugin": "^7.25.2",
-        "@babel/helper-plugin-utils": "^7.24.8"
+        "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/preset-env": {
-      "version": "7.25.4",
-      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz",
-      "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@babel/compat-data": "^7.25.4",
-        "@babel/helper-compilation-targets": "^7.25.2",
-        "@babel/helper-plugin-utils": "^7.24.8",
-        "@babel/helper-validator-option": "^7.24.8",
-        "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3",
-        "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0",
-        "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0",
-        "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7",
-        "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz",
+      "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/compat-data": "^7.26.0",
+        "@babel/helper-compilation-targets": "^7.25.9",
+        "@babel/helper-plugin-utils": "^7.25.9",
+        "@babel/helper-validator-option": "^7.25.9",
+        "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9",
+        "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9",
+        "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9",
+        "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9",
+        "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9",
         "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
-        "@babel/plugin-syntax-async-generators": "^7.8.4",
-        "@babel/plugin-syntax-class-properties": "^7.12.13",
-        "@babel/plugin-syntax-class-static-block": "^7.14.5",
-        "@babel/plugin-syntax-dynamic-import": "^7.8.3",
-        "@babel/plugin-syntax-export-namespace-from": "^7.8.3",
-        "@babel/plugin-syntax-import-assertions": "^7.24.7",
-        "@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/plugin-syntax-import-assertions": "^7.26.0",
+        "@babel/plugin-syntax-import-attributes": "^7.26.0",
         "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
-        "@babel/plugin-transform-arrow-functions": "^7.24.7",
-        "@babel/plugin-transform-async-generator-functions": "^7.25.4",
-        "@babel/plugin-transform-async-to-generator": "^7.24.7",
-        "@babel/plugin-transform-block-scoped-functions": "^7.24.7",
-        "@babel/plugin-transform-block-scoping": "^7.25.0",
-        "@babel/plugin-transform-class-properties": "^7.25.4",
-        "@babel/plugin-transform-class-static-block": "^7.24.7",
-        "@babel/plugin-transform-classes": "^7.25.4",
-        "@babel/plugin-transform-computed-properties": "^7.24.7",
-        "@babel/plugin-transform-destructuring": "^7.24.8",
-        "@babel/plugin-transform-dotall-regex": "^7.24.7",
-        "@babel/plugin-transform-duplicate-keys": "^7.24.7",
-        "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0",
-        "@babel/plugin-transform-dynamic-import": "^7.24.7",
-        "@babel/plugin-transform-exponentiation-operator": "^7.24.7",
-        "@babel/plugin-transform-export-namespace-from": "^7.24.7",
-        "@babel/plugin-transform-for-of": "^7.24.7",
-        "@babel/plugin-transform-function-name": "^7.25.1",
-        "@babel/plugin-transform-json-strings": "^7.24.7",
-        "@babel/plugin-transform-literals": "^7.25.2",
-        "@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
-        "@babel/plugin-transform-member-expression-literals": "^7.24.7",
-        "@babel/plugin-transform-modules-amd": "^7.24.7",
-        "@babel/plugin-transform-modules-commonjs": "^7.24.8",
-        "@babel/plugin-transform-modules-systemjs": "^7.25.0",
-        "@babel/plugin-transform-modules-umd": "^7.24.7",
-        "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
-        "@babel/plugin-transform-new-target": "^7.24.7",
-        "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
-        "@babel/plugin-transform-numeric-separator": "^7.24.7",
-        "@babel/plugin-transform-object-rest-spread": "^7.24.7",
-        "@babel/plugin-transform-object-super": "^7.24.7",
-        "@babel/plugin-transform-optional-catch-binding": "^7.24.7",
-        "@babel/plugin-transform-optional-chaining": "^7.24.8",
-        "@babel/plugin-transform-parameters": "^7.24.7",
-        "@babel/plugin-transform-private-methods": "^7.25.4",
-        "@babel/plugin-transform-private-property-in-object": "^7.24.7",
-        "@babel/plugin-transform-property-literals": "^7.24.7",
-        "@babel/plugin-transform-regenerator": "^7.24.7",
-        "@babel/plugin-transform-reserved-words": "^7.24.7",
-        "@babel/plugin-transform-shorthand-properties": "^7.24.7",
-        "@babel/plugin-transform-spread": "^7.24.7",
-        "@babel/plugin-transform-sticky-regex": "^7.24.7",
-        "@babel/plugin-transform-template-literals": "^7.24.7",
-        "@babel/plugin-transform-typeof-symbol": "^7.24.8",
-        "@babel/plugin-transform-unicode-escapes": "^7.24.7",
-        "@babel/plugin-transform-unicode-property-regex": "^7.24.7",
-        "@babel/plugin-transform-unicode-regex": "^7.24.7",
-        "@babel/plugin-transform-unicode-sets-regex": "^7.25.4",
+        "@babel/plugin-transform-arrow-functions": "^7.25.9",
+        "@babel/plugin-transform-async-generator-functions": "^7.25.9",
+        "@babel/plugin-transform-async-to-generator": "^7.25.9",
+        "@babel/plugin-transform-block-scoped-functions": "^7.25.9",
+        "@babel/plugin-transform-block-scoping": "^7.25.9",
+        "@babel/plugin-transform-class-properties": "^7.25.9",
+        "@babel/plugin-transform-class-static-block": "^7.26.0",
+        "@babel/plugin-transform-classes": "^7.25.9",
+        "@babel/plugin-transform-computed-properties": "^7.25.9",
+        "@babel/plugin-transform-destructuring": "^7.25.9",
+        "@babel/plugin-transform-dotall-regex": "^7.25.9",
+        "@babel/plugin-transform-duplicate-keys": "^7.25.9",
+        "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9",
+        "@babel/plugin-transform-dynamic-import": "^7.25.9",
+        "@babel/plugin-transform-exponentiation-operator": "^7.25.9",
+        "@babel/plugin-transform-export-namespace-from": "^7.25.9",
+        "@babel/plugin-transform-for-of": "^7.25.9",
+        "@babel/plugin-transform-function-name": "^7.25.9",
+        "@babel/plugin-transform-json-strings": "^7.25.9",
+        "@babel/plugin-transform-literals": "^7.25.9",
+        "@babel/plugin-transform-logical-assignment-operators": "^7.25.9",
+        "@babel/plugin-transform-member-expression-literals": "^7.25.9",
+        "@babel/plugin-transform-modules-amd": "^7.25.9",
+        "@babel/plugin-transform-modules-commonjs": "^7.25.9",
+        "@babel/plugin-transform-modules-systemjs": "^7.25.9",
+        "@babel/plugin-transform-modules-umd": "^7.25.9",
+        "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9",
+        "@babel/plugin-transform-new-target": "^7.25.9",
+        "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9",
+        "@babel/plugin-transform-numeric-separator": "^7.25.9",
+        "@babel/plugin-transform-object-rest-spread": "^7.25.9",
+        "@babel/plugin-transform-object-super": "^7.25.9",
+        "@babel/plugin-transform-optional-catch-binding": "^7.25.9",
+        "@babel/plugin-transform-optional-chaining": "^7.25.9",
+        "@babel/plugin-transform-parameters": "^7.25.9",
+        "@babel/plugin-transform-private-methods": "^7.25.9",
+        "@babel/plugin-transform-private-property-in-object": "^7.25.9",
+        "@babel/plugin-transform-property-literals": "^7.25.9",
+        "@babel/plugin-transform-regenerator": "^7.25.9",
+        "@babel/plugin-transform-regexp-modifiers": "^7.26.0",
+        "@babel/plugin-transform-reserved-words": "^7.25.9",
+        "@babel/plugin-transform-shorthand-properties": "^7.25.9",
+        "@babel/plugin-transform-spread": "^7.25.9",
+        "@babel/plugin-transform-sticky-regex": "^7.25.9",
+        "@babel/plugin-transform-template-literals": "^7.25.9",
+        "@babel/plugin-transform-typeof-symbol": "^7.25.9",
+        "@babel/plugin-transform-unicode-escapes": "^7.25.9",
+        "@babel/plugin-transform-unicode-property-regex": "^7.25.9",
+        "@babel/plugin-transform-unicode-regex": "^7.25.9",
+        "@babel/plugin-transform-unicode-sets-regex": "^7.25.9",
         "@babel/preset-modules": "0.1.6-no-external-plugins",
         "babel-plugin-polyfill-corejs2": "^0.4.10",
         "babel-plugin-polyfill-corejs3": "^0.10.6",
         "babel-plugin-polyfill-regenerator": "^0.6.1",
-        "core-js-compat": "^3.37.1",
+        "core-js-compat": "^3.38.1",
         "semver": "^6.3.1"
       },
       "engines": {
         "@babel/core": "^7.0.0-0"
       }
     },
+    "node_modules/@babel/preset-env/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/@babel/preset-modules": {
       "version": "0.1.6-no-external-plugins",
       "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
       "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@babel/helper-plugin-utils": "^7.0.0",
         "@babel/types": "^7.4.4",
         "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
       }
     },
-    "node_modules/@babel/regjsgen": {
-      "version": "0.8.0",
-      "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz",
-      "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==",
-      "dev": true,
-      "license": "MIT"
-    },
     "node_modules/@babel/runtime": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
-      "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
-      "license": "MIT",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
+      "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
       }
     },
     "node_modules/@babel/runtime-corejs3": {
-      "version": "7.20.6",
-      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.20.6.tgz",
-      "integrity": "sha512-tqeujPiuEfcH067mx+7otTQWROVMKHXEaOQcAeNV5dDdbPWvPcFA8/W9LXw2NfjNmOetqLl03dfnG2WALPlsRQ==",
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.0.tgz",
+      "integrity": "sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w==",
       "dev": true,
       "dependencies": {
-        "core-js-pure": "^3.25.1",
-        "regenerator-runtime": "^0.13.11"
+        "core-js-pure": "^3.30.2",
+        "regenerator-runtime": "^0.14.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/runtime-corejs3/node_modules/regenerator-runtime": {
-      "version": "0.13.11",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
-      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
-      "dev": true
-    },
     "node_modules/@babel/template": {
-      "version": "7.25.0",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
-      "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
-      "license": "MIT",
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
+      "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
       "dependencies": {
-        "@babel/code-frame": "^7.24.7",
-        "@babel/parser": "^7.25.0",
-        "@babel/types": "^7.25.0"
+        "@babel/code-frame": "^7.25.9",
+        "@babel/parser": "^7.25.9",
+        "@babel/types": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.25.6",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz",
-      "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/code-frame": "^7.24.7",
-        "@babel/generator": "^7.25.6",
-        "@babel/parser": "^7.25.6",
-        "@babel/template": "^7.25.0",
-        "@babel/types": "^7.25.6",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz",
+      "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==",
+      "dependencies": {
+        "@babel/code-frame": "^7.26.2",
+        "@babel/generator": "^7.26.5",
+        "@babel/parser": "^7.26.5",
+        "@babel/template": "^7.25.9",
+        "@babel/types": "^7.26.5",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.25.6",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
-      "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
-      "license": "MIT",
+      "version": "7.26.5",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz",
+      "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==",
       "dependencies": {
-        "@babel/helper-string-parser": "^7.24.8",
-        "@babel/helper-validator-identifier": "^7.24.7",
-        "to-fast-properties": "^2.0.0"
+        "@babel/helper-string-parser": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@emotion/hash": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz",
-      "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ=="
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
     },
     "node_modules/@esbuild-plugins/node-globals-polyfill": {
       "version": "0.2.3",
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "aix"
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.9.tgz",
-      "integrity": "sha512-kW5ccqWHVOOTGUkkJbtfoImtqu3kA1PFkivM+9QPFSHphPfPBlBalX9eDRqPK+wHCqKhU48/78T791qPgC9e9A==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
+      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
       "cpu": [
         "arm"
       ],
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.9.tgz",
-      "integrity": "sha512-ndIAZJUeLx4O+4AJbFQCurQW4VRUXjDsUvt1L+nP8bVELOWdmdCEOtlIweCUE6P+hU0uxYbEK2AEP0n5IVQvhg==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
+      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.9.tgz",
-      "integrity": "sha512-UbMcJB4EHrAVOnknQklREPgclNU2CPet2h+sCBCXmF2mfoYWopBn/CfTfeyOkb/JglOcdEADqAljFndMKnFtOw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
+      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.9.tgz",
-      "integrity": "sha512-d7D7/nrt4CxPul98lx4PXhyNZwTYtbdaHhOSdXlZuu5zZIznjqtMqLac8Bv+IuT6SVHiHUwrkL6ywD7mOgLW+A==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
+      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.9.tgz",
-      "integrity": "sha512-LZc+Wlz06AkJYtwWsBM3x2rSqTG8lntDuftsUNQ3fCx9ZttYtvlDcVtgb+NQ6t9s6K5No5zutN3pcjZEC2a4iQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
+      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.9.tgz",
-      "integrity": "sha512-gIj0UQZlQo93CHYouHKkpzP7AuruSaMIm1etcWIxccFEVqCN1xDr6BWlN9bM+ol/f0W9w3hx3HDuEwcJVtGneQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
+      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.9.tgz",
-      "integrity": "sha512-GNors4vaMJ7lzGOuhzNc7jvgsQZqErGA8rsW+nck8N1nYu86CvsJW2seigVrQQWOV4QzEP8Zf3gm+QCjA2hnBQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
+      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.9.tgz",
-      "integrity": "sha512-cNx1EF99c2t1Ztn0lk9N+MuwBijGF8mH6nx9GFsB3e0lpUpPkCE/yt5d+7NP9EwJf5uzqdjutgVYoH1SNqzudA==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
+      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
       "cpu": [
         "arm"
       ],
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.9.tgz",
-      "integrity": "sha512-YPxQunReYp8RQ1FvexFrOEqqf+nLbS3bKVZF5FRT2uKM7Wio7BeATqAwO02AyrdSEntt3I5fhFsujUChIa8CZg==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
+      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.9.tgz",
-      "integrity": "sha512-zb12ixDIKNwFpIqR00J88FFitVwOEwO78EiUi8wi8FXlmSc3GtUuKV/BSO+730Kglt0B47+ZrJN1BhhOxZaVrw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
+      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
       "cpu": [
         "ia32"
       ],
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.9.tgz",
-      "integrity": "sha512-X8te4NLxtHiNT6H+4Pfm5RklzItA1Qy4nfyttihGGX+Koc53Ar20ViC+myY70QJ8PDEOehinXZj/F7QK3A+MKQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
+      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
       "cpu": [
         "loong64"
       ],
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.9.tgz",
-      "integrity": "sha512-ZqyMDLt02c5smoS3enlF54ndK5zK4IpClLTxF0hHfzHJlfm4y8IAkIF8LUW0W7zxcKy7oAwI7BRDqeVvC120SA==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
+      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
       "cpu": [
         "mips64el"
       ],
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.9.tgz",
-      "integrity": "sha512-k+ca5W5LDBEF3lfDwMV6YNXwm4wEpw9krMnNvvlNz3MrKSD2Eb2c861O0MaKrZkG/buTQAP4vkavbLwgIe6xjg==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
+      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
       "cpu": [
         "ppc64"
       ],
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.9.tgz",
-      "integrity": "sha512-GuInVdogjmg9DhgkEmNipHkC+3tzkanPJzgzTC2ihsvrruLyFoR1YrTGixblNSMPudQLpiqkcwGwwe0oqfrvfA==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
+      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
       "cpu": [
         "riscv64"
       ],
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.9.tgz",
-      "integrity": "sha512-49wQ0aYkvwXonGsxc7LuuLNICMX8XtO92Iqmug5Qau0kpnV6SP34jk+jIeu4suHwAbSbRhVFtDv75yRmyfQcHw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
+      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
       "cpu": [
         "s390x"
       ],
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.9.tgz",
-      "integrity": "sha512-Nx4oKEAJ6EcQlt4dK7qJyuZUoXZG7CAeY22R7rqZijFzwFfMOD+gLP56uV7RrV86jGf8PeRY8TBsRmOcZoG42w==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
+      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.9.tgz",
-      "integrity": "sha512-d0WnpgJ+FTiMZXEQ1NOv9+0gvEhttbgKEvVqWWAtl1u9AvlspKXbodKHzQ5MLP6YV1y52Xp+p8FMYqj8ykTahg==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.9.tgz",
-      "integrity": "sha512-jccK11278dvEscHFfMk5EIPjF4wv1qGD0vps7mBV1a6TspdR36O28fgPem/SA/0pcsCPHjww5ouCLwP+JNAFlw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.9.tgz",
-      "integrity": "sha512-OetwTSsv6mIDLqN7I7I2oX9MmHGwG+AP+wKIHvq+6sIHwcPPJqRx+DJB55jy9JG13CWcdcQno/7V5MTJ5a0xfQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
+      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.9.tgz",
-      "integrity": "sha512-tKSSSK6unhxbGbHg+Cc+JhRzemkcsX0tPBvG0m5qsWbkShDK9c+/LSb13L18LWVdOQZwuA55Vbakxmt6OjBDOQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
+      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.9.tgz",
-      "integrity": "sha512-ZTQ5vhNS5gli0KK8I6/s6+LwXmNEfq1ftjnSVyyNm33dBw8zDpstqhGXYUbZSWWLvkqiRRjgxgmoncmi6Yy7Ng==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
+      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
       "cpu": [
         "ia32"
       ],
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.9.tgz",
-      "integrity": "sha512-C4ZX+YFIp6+lPrru3tpH6Gaapy8IBRHw/e7l63fzGDhn/EaiGpQgbIlT5paByyy+oMvRFQoxxyvC4LE0AjJMqQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
+      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@eslint/eslintrc": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
-      "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
+      "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
       "dev": true,
       "dependencies": {
         "ajv": "^6.12.4",
         "debug": "^4.3.2",
         "espree": "^9.4.0",
-        "globals": "^13.15.0",
+        "globals": "^13.19.0",
         "ignore": "^5.2.0",
         "import-fresh": "^3.2.1",
         "js-yaml": "^4.1.0",
       }
     },
     "node_modules/@eslint/eslintrc/node_modules/globals": {
-      "version": "13.19.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
-      "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
+      "version": "13.24.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
       "dev": true,
       "dependencies": {
         "type-fest": "^0.20.2"
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/@eslint/eslintrc/node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/@fontsource/inter": {
       "version": "4.5.14",
       "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.14.tgz",
       "integrity": "sha512-JDC9AocdPLuGsASkvWw9hS5gtHE7K9dOwL98XLrk5yjYqxy4uVnScG58NUvFMJDVJRl/7c8Wnap6PEs+7Zvj1Q=="
     },
     "node_modules/@formatjs/ecma402-abstract": {
-      "version": "1.17.2",
-      "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz",
-      "integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==",
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz",
+      "integrity": "sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==",
       "dependencies": {
-        "@formatjs/intl-localematcher": "0.4.2",
-        "tslib": "^2.4.0"
+        "@formatjs/fast-memoize": "2.2.6",
+        "@formatjs/intl-localematcher": "0.5.10",
+        "decimal.js": "10",
+        "tslib": "2"
       }
     },
-    "node_modules/@formatjs/ecma402-abstract/node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
-    },
     "node_modules/@formatjs/fast-memoize": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz",
-      "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==",
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz",
+      "integrity": "sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==",
       "dependencies": {
-        "tslib": "^2.4.0"
+        "tslib": "2"
       }
     },
-    "node_modules/@formatjs/fast-memoize/node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
-    },
     "node_modules/@formatjs/icu-messageformat-parser": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.0.tgz",
-      "integrity": "sha512-7uqC4C2RqOaBQtcjqXsSpGRYVn+ckjhNga5T/otFh6MgxRrCJQqvjfbrGLpX1Lcbxdm5WH3Z2WZqt1+Tm/cn/Q==",
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.10.0.tgz",
+      "integrity": "sha512-PDeky6nDAyHYEtmSi2X1PG9YpqE+2BRTJT7JvPix8K8JX1wBWQNao6KcPtmZpttQHUHmzMcd/rne7lFesSzUKQ==",
       "dependencies": {
-        "@formatjs/ecma402-abstract": "1.17.2",
-        "@formatjs/icu-skeleton-parser": "1.6.2",
-        "tslib": "^2.4.0"
+        "@formatjs/ecma402-abstract": "2.3.2",
+        "@formatjs/icu-skeleton-parser": "1.8.12",
+        "tslib": "2"
       }
     },
-    "node_modules/@formatjs/icu-messageformat-parser/node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
-    },
     "node_modules/@formatjs/icu-skeleton-parser": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.2.tgz",
-      "integrity": "sha512-VtB9Slo4ZL6QgtDFJ8Injvscf0xiDd4bIV93SOJTBjUF4xe2nAWOoSjLEtqIG+hlIs1sNrVKAaFo3nuTI4r5ZA==",
+      "version": "1.8.12",
+      "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.12.tgz",
+      "integrity": "sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg==",
       "dependencies": {
-        "@formatjs/ecma402-abstract": "1.17.2",
-        "tslib": "^2.4.0"
+        "@formatjs/ecma402-abstract": "2.3.2",
+        "tslib": "2"
       }
     },
-    "node_modules/@formatjs/icu-skeleton-parser/node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
-    },
     "node_modules/@formatjs/intl-localematcher": {
-      "version": "0.4.2",
-      "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
-      "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
+      "version": "0.5.10",
+      "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
+      "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
       "dependencies": {
-        "tslib": "^2.4.0"
+        "tslib": "2"
       }
     },
-    "node_modules/@formatjs/intl-localematcher/node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
-    },
     "node_modules/@humanwhocodes/config-array": {
-      "version": "0.11.7",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
-      "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==",
+      "version": "0.11.14",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
+      "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
+      "deprecated": "Use @eslint/config-array instead",
       "dev": true,
       "dependencies": {
-        "@humanwhocodes/object-schema": "^1.2.1",
-        "debug": "^4.1.1",
+        "@humanwhocodes/object-schema": "^2.0.2",
+        "debug": "^4.3.1",
         "minimatch": "^3.0.5"
       },
       "engines": {
       }
     },
     "node_modules/@humanwhocodes/object-schema": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
-      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+      "deprecated": "Use @eslint/object-schema instead",
       "dev": true
     },
     "node_modules/@internationalized/date": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.0.tgz",
-      "integrity": "sha512-nw0Q+oRkizBWMioseI8+2TeUPEyopJVz5YxoYVzR0W1v+2YytiYah7s/ot35F149q/xAg4F1gT/6eTd+tsUpFQ==",
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.7.0.tgz",
+      "integrity": "sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==",
       "dependencies": {
         "@swc/helpers": "^0.5.0"
       }
     },
     "node_modules/@internationalized/message": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.1.tgz",
-      "integrity": "sha512-ZgHxf5HAPIaR0th+w0RUD62yF6vxitjlprSxmLJ1tam7FOekqRSDELMg4Cr/DdszG5YLsp5BG3FgHgqquQZbqw==",
+      "version": "3.1.6",
+      "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.6.tgz",
+      "integrity": "sha512-JxbK3iAcTIeNr1p0WIFg/wQJjIzJt9l/2KNY/48vXV7GRGZSv3zMxJsce008fZclk2cDC8y0Ig3odceHO7EfNQ==",
       "dependencies": {
         "@swc/helpers": "^0.5.0",
         "intl-messageformat": "^10.1.0"
       }
     },
     "node_modules/@internationalized/number": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.3.0.tgz",
-      "integrity": "sha512-PuxgnKE5NJMOGKUcX1QROo8jq7sW7UWLrL5B6Rfe8BdWgU/be04cVvLyCeALD46vvbAv3d1mUvyHav/Q9a237g==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.0.tgz",
+      "integrity": "sha512-PtrRcJVy7nw++wn4W2OuePQQfTqDzfusSuY1QTtui4wa7r+rGVtR75pO8CyKvHvzyQYi3Q1uO5sY0AsB4e65Bw==",
       "dependencies": {
         "@swc/helpers": "^0.5.0"
       }
     },
     "node_modules/@internationalized/string": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.1.1.tgz",
-      "integrity": "sha512-fvSr6YRoVPgONiVIUhgCmIAlifMVCeej/snPZVzbzRPxGpHl3o1GRe+d/qh92D8KhgOciruDUH8I5mjdfdjzfA==",
+      "version": "3.2.5",
+      "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.5.tgz",
+      "integrity": "sha512-rKs71Zvl2OKOHM+mzAFMIyqR5hI1d1O6BBkMK2/lkfg3fkmVh9Eeg0awcA8W2WqYqDOv6a86DIOlFpggwLtbuw==",
       "dependencies": {
         "@swc/helpers": "^0.5.0"
       }
     },
     "node_modules/@jridgewell/gen-mapping": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
-      "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
+      "version": "0.3.8",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+      "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
       "dependencies": {
-        "@jridgewell/set-array": "^1.0.0",
-        "@jridgewell/sourcemap-codec": "^1.4.10"
+        "@jridgewell/set-array": "^1.2.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.24"
       },
       "engines": {
         "node": ">=6.0.0"
       }
     },
     "node_modules/@jridgewell/resolve-uri": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
-      "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
       "engines": {
         "node": ">=6.0.0"
       }
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
       "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
-      "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
       }
       "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
       "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25"
       }
     },
-    "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
-      "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@jridgewell/set-array": "^1.2.1",
-        "@jridgewell/sourcemap-codec": "^1.4.10",
-        "@jridgewell/trace-mapping": "^0.3.24"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
     "node_modules/@jridgewell/sourcemap-codec": {
-      "version": "1.4.14",
-      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
-      "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
     },
     "node_modules/@jridgewell/trace-mapping": {
       "version": "0.3.25",
       "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
       "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
-      "license": "MIT",
       "dependencies": {
         "@jridgewell/resolve-uri": "^3.1.0",
         "@jridgewell/sourcemap-codec": "^1.4.14"
         "node-pre-gyp": "bin/node-pre-gyp"
       }
     },
-    "node_modules/@mapbox/node-pre-gyp/node_modules/semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "optional": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz",
-      "integrity": "sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og==",
-      "license": "Apache-2.0",
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.1.0.tgz",
+      "integrity": "sha512-JPuO9RCVDklDjbFzMvZfQb7PuiFkLY72bniRSu81lRzkkrcbZtmKqBFMm9H4f2FSz+tHVkDnmsvn12I4sdJJ5A==",
       "engines": {
         "node": ">= 10"
       }
     "node_modules/@matrix-org/olm": {
       "version": "3.2.15",
       "resolved": "https://registry.npmjs.org/@matrix-org/olm/-/olm-3.2.15.tgz",
-      "integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==",
-      "license": "Apache-2.0"
+      "integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q=="
     },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       }
     },
     "node_modules/@popperjs/core": {
-      "version": "2.11.6",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
-      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/popperjs"
       }
     },
     "node_modules/@react-aria/breadcrumbs": {
-      "version": "3.5.7",
-      "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.7.tgz",
-      "integrity": "sha512-z+L1gNyWrjZ4Fs0Vo4AkwJicPpEGIestww6r8CiTlt07eo0vCReNmB3oofI6nMJOSu51yef+qqBtFyr0tqBgiw==",
-      "dependencies": {
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/link": "^3.6.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/breadcrumbs": "^3.7.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.5.20",
+      "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.20.tgz",
+      "integrity": "sha512-xqVSSDPpQuUFpJyIXMQv8L7zumk5CeGX7qTzo4XRvqm5T9qnNAX4XpYEMdktnLrQRY/OemCBScbx7SEwr0B3Kg==",
+      "dependencies": {
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/link": "^3.7.8",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/breadcrumbs": "^3.7.10",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/button": {
-      "version": "3.8.4",
-      "resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.8.4.tgz",
-      "integrity": "sha512-rTGZk5zu+lQNjfij2fwnw2PAgBgzNLi3zbMw1FL5/XwVx+lEH2toeqKLoqULtd7nSxskYuQz56VhmjUok6Qkmg==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/toggle": "^3.6.3",
-        "@react-types/button": "^3.9.0",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.11.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.11.1.tgz",
+      "integrity": "sha512-NSs2HxHSSPSuYy5bN+PMJzsCNDVsbm1fZ/nrWM2WWWHTBrx9OqyrEXZVV9ebzQCN9q0nzhwpf6D42zHIivWtJA==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/toolbar": "3.0.0-beta.12",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/toggle": "^3.8.1",
+        "@react-types/button": "^3.10.2",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/calendar": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/@react-aria/calendar/-/calendar-3.5.2.tgz",
-      "integrity": "sha512-HiyUiY0C2aoHa2252Es/Rj1fh5/tewLf6/3gUr42zKl7lq4IqG9cyW7LVRwA47ow1VGLPZSSqTcVakB7jgr7Zw==",
-      "dependencies": {
-        "@internationalized/date": "^3.5.0",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/live-announcer": "^3.3.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/calendar": "^3.4.1",
-        "@react-types/button": "^3.9.0",
-        "@react-types/calendar": "^3.4.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/calendar/-/calendar-3.7.0.tgz",
+      "integrity": "sha512-9YUbgcox7cQgvZfQtL2BLLRsIuX4mJeclk9HkFoOsAu3RGO5HNsteah8FV54W8BMjm/bNRXIPUxtjTTP+1L6jg==",
+      "dependencies": {
+        "@internationalized/date": "^3.7.0",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/live-announcer": "^3.4.1",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/calendar": "^3.7.0",
+        "@react-types/button": "^3.10.2",
+        "@react-types/calendar": "^3.6.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/checkbox": {
-      "version": "3.11.2",
-      "resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.11.2.tgz",
-      "integrity": "sha512-8cgXxpc7IMJ9buw+Rbhr1xc66zNp2ePuFpjw3uWyH7S3IJEd2f5kXUDNWLXQRADJso95UlajRlJQiG4QIObEnA==",
-      "dependencies": {
-        "@react-aria/label": "^3.7.2",
-        "@react-aria/toggle": "^3.8.2",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/checkbox": "^3.5.1",
-        "@react-stately/toggle": "^3.6.3",
-        "@react-types/checkbox": "^3.5.2",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.15.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.15.1.tgz",
+      "integrity": "sha512-ETgsMDZ0IZzRXy/OVlGkazm8T+PcMHoTvsxp0c+U82c8iqdITA+VJ615eBPOQh6OkkYIIn4cRn/e+69RmGzXng==",
+      "dependencies": {
+        "@react-aria/form": "^3.0.12",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/label": "^3.7.14",
+        "@react-aria/toggle": "^3.10.11",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/checkbox": "^3.6.11",
+        "@react-stately/form": "^3.1.1",
+        "@react-stately/toggle": "^3.8.1",
+        "@react-types/checkbox": "^3.9.1",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/combobox": {
-      "version": "3.7.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.7.1.tgz",
-      "integrity": "sha512-37no1b3sRI9mDh3MpMPWNt0Q8QdoRipnx12Vx5Uvtb0PA23hwOWDquICzs157SoJpXP49/+eH6LiA0uTsqwVuQ==",
-      "dependencies": {
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/listbox": "^3.11.1",
-        "@react-aria/live-announcer": "^3.3.1",
-        "@react-aria/menu": "^3.11.1",
-        "@react-aria/overlays": "^3.18.1",
-        "@react-aria/selection": "^3.17.1",
-        "@react-aria/textfield": "^3.12.2",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/combobox": "^3.7.1",
-        "@react-stately/layout": "^3.13.3",
-        "@react-types/button": "^3.9.0",
-        "@react-types/combobox": "^3.8.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.11.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.11.1.tgz",
+      "integrity": "sha512-TTNbGhUuqxzPcJzd6hufOxuHzX0UARkw+0bl+TuCwNPQnqrcPf20EoOZvd3MHZwGq6GCP4QV+qo0uGx83RpUvA==",
+      "dependencies": {
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/listbox": "^3.14.0",
+        "@react-aria/live-announcer": "^3.4.1",
+        "@react-aria/menu": "^3.17.0",
+        "@react-aria/overlays": "^3.25.0",
+        "@react-aria/selection": "^3.22.0",
+        "@react-aria/textfield": "^3.16.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/combobox": "^3.10.2",
+        "@react-stately/form": "^3.1.1",
+        "@react-types/button": "^3.10.2",
+        "@react-types/combobox": "^3.13.2",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/datepicker": {
-      "version": "3.8.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/datepicker/-/datepicker-3.8.1.tgz",
-      "integrity": "sha512-q2Z5DYDkic3RWzvg3oysrA2VEebuxtEfqj8PSlNFndZh/pNrA+Tvkaatdk/BoxlsZsfeLof+/tBq6yWeqTDguQ==",
-      "dependencies": {
-        "@internationalized/date": "^3.5.0",
-        "@internationalized/number": "^3.3.0",
-        "@internationalized/string": "^3.1.1",
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/label": "^3.7.2",
-        "@react-aria/spinbutton": "^3.5.4",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/datepicker": "^3.8.0",
-        "@react-types/button": "^3.9.0",
-        "@react-types/calendar": "^3.4.1",
-        "@react-types/datepicker": "^3.6.1",
-        "@react-types/dialog": "^3.5.6",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.13.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/datepicker/-/datepicker-3.13.0.tgz",
+      "integrity": "sha512-TmJan65P3Vk7VDBNW5rH9Z25cAn0vk8TEtaP3boCs8wJFE+HbEuB8EqLxBFu47khtuKTEqDP3dTlUh2Vt/f7Xw==",
+      "dependencies": {
+        "@internationalized/date": "^3.7.0",
+        "@internationalized/number": "^3.6.0",
+        "@internationalized/string": "^3.2.5",
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/form": "^3.0.12",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/label": "^3.7.14",
+        "@react-aria/spinbutton": "^3.6.11",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/datepicker": "^3.12.0",
+        "@react-stately/form": "^3.1.1",
+        "@react-types/button": "^3.10.2",
+        "@react-types/calendar": "^3.6.0",
+        "@react-types/datepicker": "^3.10.0",
+        "@react-types/dialog": "^3.5.15",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/dialog": {
-      "version": "3.5.7",
-      "resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.7.tgz",
-      "integrity": "sha512-IKeBaIQBl+WYkhytyE0eISW4ApOEvCJZuw9Xq7gjlKFBlF4X6ffo8souv12KpaznK6/fp1vtEXMmy1AfejiT8Q==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/overlays": "^3.18.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/overlays": "^3.6.3",
-        "@react-types/dialog": "^3.5.6",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.21.tgz",
+      "integrity": "sha512-tBsn9swBhcptJ9QIm0+ur0PVR799N6qmGguva3rUdd+gfitknFScyT08d7AoMr9AbXYdJ+2R9XNSZ3H3uIWQMw==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/overlays": "^3.25.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/dialog": "^3.5.15",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/dnd": {
-      "version": "3.4.3",
-      "resolved": "https://registry.npmjs.org/@react-aria/dnd/-/dnd-3.4.3.tgz",
-      "integrity": "sha512-9yiYTQvfT5EUmSsGY3vZlK1xs+xHOFDw5I+c+HyvwqiSu0AIZ4yXqpJVwbarKeZlTOQGCWtb/SOHEdMXfaXKgA==",
-      "dependencies": {
-        "@internationalized/string": "^3.1.1",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/live-announcer": "^3.3.1",
-        "@react-aria/overlays": "^3.18.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-aria/visually-hidden": "^3.8.6",
-        "@react-stately/dnd": "^3.2.5",
-        "@react-types/button": "^3.9.0",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.8.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/dnd/-/dnd-3.8.1.tgz",
+      "integrity": "sha512-FoXYQ4z33E9YBzIGRJM1B1oZep6CvEWgXvjCZGURatjr3qG7vf95mOqA5kVd9bjLL7QK4w0ujJWEBfog3WmufA==",
+      "dependencies": {
+        "@internationalized/string": "^3.2.5",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/live-announcer": "^3.4.1",
+        "@react-aria/overlays": "^3.25.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/dnd": "^3.5.1",
+        "@react-types/button": "^3.10.2",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/focus": {
-      "version": "3.14.3",
-      "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.14.3.tgz",
-      "integrity": "sha512-gvO/frZ7SxyfyHJYC+kRsUXnXct8hGHKlG1TwbkzCCXim9XIPKDgRzfNGuFfj0i8ZpR9xmsjOBUkHZny0uekFA==",
+      "version": "3.19.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.1.tgz",
+      "integrity": "sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==",
       "dependencies": {
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/shared": "^3.21.0",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0",
-        "clsx": "^1.1.1"
+        "clsx": "^2.0.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
-    "node_modules/@react-aria/grid": {
-      "version": "3.8.4",
-      "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.8.4.tgz",
-      "integrity": "sha512-UxEz98Z6yxVAOq7QSZ9OmSsvMwxJDVl7dVRwUHeqWxNprk9o5GGCLjhMv948XBUEnOvLV2qgtI7UoGzSdliUJA==",
+    "node_modules/@react-aria/form": {
+      "version": "3.0.12",
+      "resolved": "https://registry.npmjs.org/@react-aria/form/-/form-3.0.12.tgz",
+      "integrity": "sha512-8uvPYEd3GDyGt5NRJIzdWW1Ry5HLZq37vzRZKUW8alZ2upFMH3KJJG55L9GP59KiF6zBrYBebvI/YK1Ye1PE1g==",
       "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/live-announcer": "^3.3.1",
-        "@react-aria/selection": "^3.17.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/grid": "^3.8.2",
-        "@react-stately/selection": "^3.14.0",
-        "@react-stately/virtualizer": "^3.6.4",
-        "@react-types/checkbox": "^3.5.2",
-        "@react-types/grid": "^3.2.2",
-        "@react-types/shared": "^3.21.0",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/form": "^3.1.1",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
+      }
+    },
+    "node_modules/@react-aria/grid": {
+      "version": "3.11.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.11.1.tgz",
+      "integrity": "sha512-Wg8m68RtNWfkhP3Qjrrsl1q1et8QCjXPMRsYgKBahYRS0kq2MDcQ+UBdG1fiCQn/MfNImhTUGVeQX276dy1lww==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/live-announcer": "^3.4.1",
+        "@react-aria/selection": "^3.22.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/grid": "^3.10.1",
+        "@react-stately/selection": "^3.19.0",
+        "@react-types/checkbox": "^3.9.1",
+        "@react-types/grid": "^3.2.11",
+        "@react-types/shared": "^3.27.0",
+        "@swc/helpers": "^0.5.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/gridlist": {
-      "version": "3.7.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/gridlist/-/gridlist-3.7.1.tgz",
-      "integrity": "sha512-XnU8mTc/KrwHsGayQm0u5aoaDzdZ8DftKSSfyBEqLiCaibKFqMADb987SOY5+IVGEtYkxDRn1Reo52U0Fs4mxg==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/grid": "^3.8.4",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/selection": "^3.17.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/list": "^3.10.0",
-        "@react-types/checkbox": "^3.5.2",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/gridlist/-/gridlist-3.10.1.tgz",
+      "integrity": "sha512-11FlupBg5C9ehs7R6OjqMPWEOLK/4IuSrq7D1xU+Hnm7ZYI/KKcCXvNMjMmnOz/gGzOmfgVwz5PIKaY9aZarEg==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/grid": "^3.11.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/selection": "^3.22.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/list": "^3.11.2",
+        "@react-stately/tree": "^3.8.7",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/i18n": {
-      "version": "3.8.4",
-      "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.8.4.tgz",
-      "integrity": "sha512-YlTJn7YJlUxds/T5dNtme551qc118NoDQhK+IgGpzcmPQ3xSnwBAQP4Zwc7wCpAU+xEwnNcsGw+L1wJd49He/A==",
-      "dependencies": {
-        "@internationalized/date": "^3.5.0",
-        "@internationalized/message": "^3.1.1",
-        "@internationalized/number": "^3.3.0",
-        "@internationalized/string": "^3.1.1",
-        "@react-aria/ssr": "^3.8.0",
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.12.5",
+      "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.5.tgz",
+      "integrity": "sha512-ooeop2pTG94PuaHoN2OTk2hpkqVuoqgEYxRvnc1t7DVAtsskfhS/gVOTqyWGsxvwAvRi7m/CnDu6FYdeQ/bK5w==",
+      "dependencies": {
+        "@internationalized/date": "^3.7.0",
+        "@internationalized/message": "^3.1.6",
+        "@internationalized/number": "^3.6.0",
+        "@internationalized/string": "^3.2.5",
+        "@react-aria/ssr": "^3.9.7",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/interactions": {
-      "version": "3.19.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.19.1.tgz",
-      "integrity": "sha512-2QFOvq/rJfMGEezmtYcGcJmfaD16kHKcSTLFrZ8aeBK6hYFddGVZJZk+dXf+G7iNaffa8rMt6uwzVe/malJPBA==",
+      "version": "3.23.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.23.0.tgz",
+      "integrity": "sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==",
       "dependencies": {
-        "@react-aria/ssr": "^3.8.0",
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/shared": "^3.21.0",
+        "@react-aria/ssr": "^3.9.7",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/label": {
-      "version": "3.7.2",
-      "resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.7.2.tgz",
-      "integrity": "sha512-rS0xQy+4RH1+JLESzLZd9H285McjNNf2kKwBhzU0CW3akjlu7gqaMKEJhX9MlpPDIVOUc2oEObGdU3UMmqa8ew==",
+      "version": "3.7.14",
+      "resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.7.14.tgz",
+      "integrity": "sha512-EN1Md2YvcC4sMqBoggsGYUEGlTNqUfJZWzduSt29fbQp1rKU2KlybTe+TWxKq/r2fFd+4JsRXxMeJiwB3w2AQA==",
       "dependencies": {
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/label": "^3.8.1",
-        "@react-types/shared": "^3.21.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/link": {
-      "version": "3.6.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.6.1.tgz",
-      "integrity": "sha512-uVkuNHabxE11Eqeo0d1RA86EckOlfJ2Ld8uN8HnTxiLetXLZYUMBwlZfBJvT3RdwPtTG7jC3OK3BvwiyIJrtZw==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/link": "^3.5.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.7.8",
+      "resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.7.8.tgz",
+      "integrity": "sha512-oiXUPQLZmf9Q9Xehb/sG1QRxfo28NFKdh9w+unD12sHI6NdLMETl5MA4CYyTgI0dfMtTjtfrF68GCnWfc7JvXQ==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/link": "^3.5.10",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/listbox": {
-      "version": "3.11.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/listbox/-/listbox-3.11.1.tgz",
-      "integrity": "sha512-AkguQaIkqpP5oe++EZqYHowD7FfeQs+yY0QZVSsVPpNExcBug8/GcXvhSclcOxdh6ekZg4Wwcq7K0zhuTSOPzg==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/label": "^3.7.2",
-        "@react-aria/selection": "^3.17.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/list": "^3.10.0",
-        "@react-types/listbox": "^3.4.5",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.14.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/listbox/-/listbox-3.14.0.tgz",
+      "integrity": "sha512-pyVbKavh8N8iyiwOx6I3JIcICvAzFXkKSFni1yarfgngJsJV3KSyOkzLomOfN9UhbjcV4sX61/fccwJuvlurlA==",
+      "dependencies": {
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/label": "^3.7.14",
+        "@react-aria/selection": "^3.22.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/list": "^3.11.2",
+        "@react-types/listbox": "^3.5.4",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/live-announcer": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.3.1.tgz",
-      "integrity": "sha512-hsc77U7S16trM86d+peqJCOCQ7/smO1cybgdpOuzXyiwcHQw8RQ4GrXrS37P4Ux/44E9nMZkOwATQRT2aK8+Ew==",
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.4.1.tgz",
+      "integrity": "sha512-4X2mcxgqLvvkqxv2l1n00jTzUxxe0kkLiapBGH1LHX/CxA1oQcHDqv8etJ2ZOwmS/MSBBiWnv3DwYHDOF6ubig==",
       "dependencies": {
         "@swc/helpers": "^0.5.0"
       }
     },
     "node_modules/@react-aria/menu": {
-      "version": "3.11.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.11.1.tgz",
-      "integrity": "sha512-1eVVDrGnSExaL7e8IiaM9ndWTjT23rsnQGUK3p66R1Ojs8Q5rPBuJpP74rsmIpYiKOCr8WyZunjm5Fjv5KfA5Q==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/overlays": "^3.18.1",
-        "@react-aria/selection": "^3.17.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/menu": "^3.5.6",
-        "@react-stately/tree": "^3.7.3",
-        "@react-types/button": "^3.9.0",
-        "@react-types/menu": "^3.9.5",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.17.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.17.0.tgz",
+      "integrity": "sha512-aiFvSv3G1YvPC0klJQ/9quB05xIDZzJ5Lt6/CykP0UwGK5i8GCqm6/cyFLwEXsS5ooUPxS3bqmdOsgdADSSgqg==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/overlays": "^3.25.0",
+        "@react-aria/selection": "^3.22.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/menu": "^3.9.1",
+        "@react-stately/selection": "^3.19.0",
+        "@react-stately/tree": "^3.8.7",
+        "@react-types/button": "^3.10.2",
+        "@react-types/menu": "^3.9.14",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/meter": {
-      "version": "3.4.7",
-      "resolved": "https://registry.npmjs.org/@react-aria/meter/-/meter-3.4.7.tgz",
-      "integrity": "sha512-Cp4d6Pd5K6iphXMS/VZ81YxlboUi0I4WPQ+EYb4fxFBJMXVwMK6N5dnn8kwG0vpIx9m0pkFVxSZhlbrwnvW9KA==",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@react-aria/meter/-/meter-3.4.19.tgz",
+      "integrity": "sha512-IIA+gTHrNVbMuBgcqdGLEKd/ZiKM2hOUqS6uztbT15dwPJTmtfJiTWA2872PiY52p+gqPSanZuTc2TXYJa+rew==",
       "dependencies": {
-        "@react-aria/progress": "^3.4.7",
-        "@react-types/meter": "^3.3.5",
-        "@react-types/shared": "^3.21.0",
+        "@react-aria/progress": "^3.4.19",
+        "@react-types/meter": "^3.4.6",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/numberfield": {
-      "version": "3.9.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/numberfield/-/numberfield-3.9.1.tgz",
-      "integrity": "sha512-s9LM5YUzZpbOn5KldUS2JmkDNOA9obVmm8TofICH+z6RnReznp72NLPn0IwblRnocmMOIvGINT55Tz50BmbfNA==",
-      "dependencies": {
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/live-announcer": "^3.3.1",
-        "@react-aria/spinbutton": "^3.5.4",
-        "@react-aria/textfield": "^3.12.2",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/numberfield": "^3.6.2",
-        "@react-types/button": "^3.9.0",
-        "@react-types/numberfield": "^3.6.1",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/textfield": "^3.8.1",
+      "version": "3.11.10",
+      "resolved": "https://registry.npmjs.org/@react-aria/numberfield/-/numberfield-3.11.10.tgz",
+      "integrity": "sha512-bYbTfO9NbAKMFOfEGGs+lvlxk0I9L0lU3WD2PFQZWdaoBz9TCkL+vK0fJk1zsuKaVjeGsmHP9VesBPRmaP0MiA==",
+      "dependencies": {
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/spinbutton": "^3.6.11",
+        "@react-aria/textfield": "^3.16.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/form": "^3.1.1",
+        "@react-stately/numberfield": "^3.9.9",
+        "@react-types/button": "^3.10.2",
+        "@react-types/numberfield": "^3.8.8",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/overlays": {
-      "version": "3.18.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.18.1.tgz",
-      "integrity": "sha512-C74eZbTp3OA/gXy9/+4iPrZiz7g27Zy6Q1+plbg5QTLpsFLBt2Ypy9jTTANNRZfW7a5NW/Bnw9WIRjCdtTBRXw==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/ssr": "^3.8.0",
-        "@react-aria/utils": "^3.21.1",
-        "@react-aria/visually-hidden": "^3.8.6",
-        "@react-stately/overlays": "^3.6.3",
-        "@react-types/button": "^3.9.0",
-        "@react-types/overlays": "^3.8.3",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.25.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.25.0.tgz",
+      "integrity": "sha512-UEqJJ4duowrD1JvwXpPZreBuK79pbyNjNxFUVpFSskpGEJe3oCWwsSDKz7P1O7xbx5OYp+rDiY8fk/sE5rkaKw==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/ssr": "^3.9.7",
+        "@react-aria/utils": "^3.27.0",
+        "@react-aria/visually-hidden": "^3.8.19",
+        "@react-stately/overlays": "^3.6.13",
+        "@react-types/button": "^3.10.2",
+        "@react-types/overlays": "^3.8.12",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/progress": {
-      "version": "3.4.7",
-      "resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.7.tgz",
-      "integrity": "sha512-wQ+xnzt5bBdbyQ2Qx80HxaFrPZRFKge57tmJWg4qelo7tzmgb3a22tf0Ug4C3gEz/uAv0JQWOtqLKTxjsiVP7g==",
-      "dependencies": {
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/label": "^3.7.2",
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/progress": "^3.5.0",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.19.tgz",
+      "integrity": "sha512-5HHnBJHqEUuY+dYsjIZDYsENeKr49VCuxeaDZ0OSahbOlloIOB1baCo/6jLBv1O1rwrAzZ2gCCPcVGed/cjrcw==",
+      "dependencies": {
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/label": "^3.7.14",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/progress": "^3.5.9",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/radio": {
-      "version": "3.8.2",
-      "resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.8.2.tgz",
-      "integrity": "sha512-j8yyGjboTgoBEQWlnJbQVvegKiUeQEUvU/kZ7ZAdj+eAL3BqfO6FO7yt6WzK7ZIBzjGS9YbesaUa3hwIjDi3LA==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/label": "^3.7.2",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/radio": "^3.9.1",
-        "@react-types/radio": "^3.5.2",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.10.11",
+      "resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.10.11.tgz",
+      "integrity": "sha512-R150HsBFPr1jLMShI4aBM8heCa1k6h0KEvnFRfTAOBu+B9hMSZOPB+d6GQOwGPysNlbset90Kej8G15FGHjqiA==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/form": "^3.0.12",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/label": "^3.7.14",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/radio": "^3.10.10",
+        "@react-types/radio": "^3.8.6",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/searchfield": {
-      "version": "3.5.7",
-      "resolved": "https://registry.npmjs.org/@react-aria/searchfield/-/searchfield-3.5.7.tgz",
-      "integrity": "sha512-HYjB/QH3AR2E39N6eu+P/DmJMjGweg6LrO1QUbBbKJS+LDorHTN9YNKA4N89gnDDz2IPyycjxtr71hEv0I092A==",
-      "dependencies": {
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/textfield": "^3.12.2",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/searchfield": "^3.4.6",
-        "@react-types/button": "^3.9.0",
-        "@react-types/searchfield": "^3.5.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/searchfield/-/searchfield-3.8.0.tgz",
+      "integrity": "sha512-AaZuH9YIWlMyE1m7cSjHCfOuQmlWN+w8HVW32TxeGGGL1kJsYAlSYWYHUyYFIKh245kq/m5zUxAxmw5Ygmnx5w==",
+      "dependencies": {
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/textfield": "^3.16.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/searchfield": "^3.5.9",
+        "@react-types/button": "^3.10.2",
+        "@react-types/searchfield": "^3.5.11",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/select": {
-      "version": "3.13.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/select/-/select-3.13.1.tgz",
-      "integrity": "sha512-tWWOnMnrV1nlZzdO04Ntvf5GCJ6MPkg8Gwv6y0klDDjt12Qyc7J8INluW5A4eMUdtxCkWdaiEsXjyYBHT14ILQ==",
-      "dependencies": {
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/label": "^3.7.2",
-        "@react-aria/listbox": "^3.11.1",
-        "@react-aria/menu": "^3.11.1",
-        "@react-aria/selection": "^3.17.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-aria/visually-hidden": "^3.8.6",
-        "@react-stately/select": "^3.5.5",
-        "@react-types/button": "^3.9.0",
-        "@react-types/select": "^3.8.4",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.15.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/select/-/select-3.15.1.tgz",
+      "integrity": "sha512-FOtY1tuHt0YTHwOEy/sf7LEIL+Nnkho3wJmfpWQuTxsvMCF7UJdQPYPd6/jGCcCdiqW7H4iqyjUkSp6nk/XRWQ==",
+      "dependencies": {
+        "@react-aria/form": "^3.0.12",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/label": "^3.7.14",
+        "@react-aria/listbox": "^3.14.0",
+        "@react-aria/menu": "^3.17.0",
+        "@react-aria/selection": "^3.22.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-aria/visually-hidden": "^3.8.19",
+        "@react-stately/select": "^3.6.10",
+        "@react-types/button": "^3.10.2",
+        "@react-types/select": "^3.9.9",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/selection": {
-      "version": "3.17.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.17.1.tgz",
-      "integrity": "sha512-g5gkSc/M+zJiVgWbUpKN095ea0D4fxdluH9ZcXxN4AAvcrVfEJyAnMmWOIKRebN8xR0KPfNRnKB7E6jld2tbuQ==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/selection": "^3.14.0",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.22.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.22.0.tgz",
+      "integrity": "sha512-XFOrK525HX2eeWeLZcZscUAs5qsuC1ZxsInDXMjvLeAaUPtQNEhUKHj3psDAl6XDU4VV1IJo0qCmFTVqTTMZSg==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/selection": "^3.19.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/separator": {
-      "version": "3.3.7",
-      "resolved": "https://registry.npmjs.org/@react-aria/separator/-/separator-3.3.7.tgz",
-      "integrity": "sha512-5XjDhvGVmGHxxOrXLFCQhOs75v579nPTaSlrKhG/5BjTN3JrByAtuNAw8XZf3HbtiCRZnnL2bKdVbHBjmbuvDw==",
+      "version": "3.4.5",
+      "resolved": "https://registry.npmjs.org/@react-aria/separator/-/separator-3.4.5.tgz",
+      "integrity": "sha512-RQA9sKZdAEjP1Yrv0GpDdXgmXd56kXDE8atPDHEC0/A4lpYh/YFLfXcv1JW0Hlg4kBocdX2pB2INyDGhiD+yfw==",
       "dependencies": {
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/shared": "^3.21.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/slider": {
-      "version": "3.7.2",
-      "resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.7.2.tgz",
-      "integrity": "sha512-io7yJm2jS0gK1ILE9kjClh9zylKsOLbRy748CyD66LDV0ZIjj2D/uZF6BtfKq7Zhc2OsMvDB9+e2IkrszKe8uw==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/label": "^3.7.2",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/radio": "^3.9.1",
-        "@react-stately/slider": "^3.4.4",
-        "@react-types/radio": "^3.5.2",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/slider": "^3.6.2",
+      "version": "3.7.15",
+      "resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.7.15.tgz",
+      "integrity": "sha512-v9tujsuvJYRX0vE/vMYBzTT9FXbzrLsjkOrouNq+UdBIr7wRjIWTHHM0j+khb2swyCWNTbdv6Ce316Zqx2qWFg==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/label": "^3.7.14",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/slider": "^3.6.1",
+        "@react-types/shared": "^3.27.0",
+        "@react-types/slider": "^3.7.8",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/spinbutton": {
-      "version": "3.5.4",
-      "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.5.4.tgz",
-      "integrity": "sha512-W5dhUOjyBIgd8d4z526fW/HXQ+BdFceeGyvNAXoYBi/1gt3KqN/6CZgskG7OQEufxCOWc9e4A2eWNwvkQVJvWg==",
-      "dependencies": {
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/live-announcer": "^3.3.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/button": "^3.9.0",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.6.11",
+      "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.6.11.tgz",
+      "integrity": "sha512-RM+gYS9tf9Wb+GegV18n4ArK3NBKgcsak7Nx1CkEgX9BjJ0yayWUHdfEjRRvxGXl+1z1n84cJVkZ6FUlWOWEZA==",
+      "dependencies": {
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/live-announcer": "^3.4.1",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/button": "^3.10.2",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/ssr": {
-      "version": "3.8.0",
-      "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.8.0.tgz",
-      "integrity": "sha512-Y54xs483rglN5DxbwfCPHxnkvZ+gZ0LbSYmR72LyWPGft8hN/lrl1VRS1EW2SMjnkEWlj+Km2mwvA3kEHDUA0A==",
+      "version": "3.9.7",
+      "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
+      "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==",
       "dependencies": {
         "@swc/helpers": "^0.5.0"
       },
         "node": ">= 12"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/switch": {
-      "version": "3.5.6",
-      "resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.5.6.tgz",
-      "integrity": "sha512-W6H/0TFa72MJY02AatUERt5HKgaDTF8lOaTjNNmS6U6U20+//uvrVCqcBof8OMe4M60mQpkp7Bd6756CJAMX1w==",
-      "dependencies": {
-        "@react-aria/toggle": "^3.8.2",
-        "@react-stately/toggle": "^3.6.3",
-        "@react-types/switch": "^3.4.2",
+      "version": "3.6.11",
+      "resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.6.11.tgz",
+      "integrity": "sha512-paYCpH+oeL+8rgQK+cBJ+IaZ1sXSh3+50WPlg2LvLBta0QVfQhPR4juPvfXRpfHHhCjFBgF4/RGbV8q5zpl3vA==",
+      "dependencies": {
+        "@react-aria/toggle": "^3.10.11",
+        "@react-stately/toggle": "^3.8.1",
+        "@react-types/shared": "^3.27.0",
+        "@react-types/switch": "^3.5.8",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/table": {
-      "version": "3.13.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.13.1.tgz",
-      "integrity": "sha512-TBtCmJsKl3rJW/dCzA0ZxPGb8mN7ndbryLh3u+iV/+GVAVsytvAenOGrq9sLHHWXwQo5RJoO1bkUudvrZrJ5/g==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/grid": "^3.8.4",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/live-announcer": "^3.3.1",
-        "@react-aria/selection": "^3.17.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-aria/visually-hidden": "^3.8.6",
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/flags": "^3.0.0",
-        "@react-stately/table": "^3.11.2",
-        "@react-stately/virtualizer": "^3.6.4",
-        "@react-types/checkbox": "^3.5.2",
-        "@react-types/grid": "^3.2.2",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/table": "^3.9.0",
+      "version": "3.16.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.16.1.tgz",
+      "integrity": "sha512-T28TIGnKnPBunyErDBmm5jUX7AyzT7NVWBo9pDSt9wUuEnz0rVNd7p9sjmP2+u7I645feGG9klcdpCvFeqrk8A==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/grid": "^3.11.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/live-announcer": "^3.4.1",
+        "@react-aria/utils": "^3.27.0",
+        "@react-aria/visually-hidden": "^3.8.19",
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/flags": "^3.0.5",
+        "@react-stately/table": "^3.13.1",
+        "@react-types/checkbox": "^3.9.1",
+        "@react-types/grid": "^3.2.11",
+        "@react-types/shared": "^3.27.0",
+        "@react-types/table": "^3.10.4",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/tabs": {
-      "version": "3.8.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.8.1.tgz",
-      "integrity": "sha512-3kRd5rYKclmW9lllcANq0oun2d1pZq7Onma95laYfrWtPBZ3YDVKOkujGSqdfSQAFVshWBjl2Q03yyvcRiwzbQ==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/selection": "^3.17.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/list": "^3.10.0",
-        "@react-stately/tabs": "^3.6.1",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/tabs": "^3.3.3",
+      "version": "3.9.9",
+      "resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.9.9.tgz",
+      "integrity": "sha512-oXPtANs16xu6MdMGLHjGV/2Zupvyp9CJEt7ORPLv5xAzSY5hSjuQHJLZ0te3Lh/KSG5/0o3RW/W5yEqo7pBQQQ==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/selection": "^3.22.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/tabs": "^3.7.1",
+        "@react-types/shared": "^3.27.0",
+        "@react-types/tabs": "^3.3.12",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/tag": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/tag/-/tag-3.2.1.tgz",
-      "integrity": "sha512-i7Mj3IhB91sGp3NS6iNBVh25W+LR2XXpTmtn3OS4R62q3Oalw/1PKqPWqFc73Lb5IWF5rj3eh2yTf+rerWf3dw==",
-      "dependencies": {
-        "@react-aria/gridlist": "^3.7.1",
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/label": "^3.7.2",
-        "@react-aria/selection": "^3.17.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/list": "^3.10.0",
-        "@react-types/button": "^3.9.0",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.4.9",
+      "resolved": "https://registry.npmjs.org/@react-aria/tag/-/tag-3.4.9.tgz",
+      "integrity": "sha512-Vnps+zk8vYyjevv2Bc6vc9kSp9HFLKrKUDmrWMc0DfseypwJMc3Ya6F965ZVTjF9nuWrojNmvgusNu7qyXFShQ==",
+      "dependencies": {
+        "@react-aria/gridlist": "^3.10.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/label": "^3.7.14",
+        "@react-aria/selection": "^3.22.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/list": "^3.11.2",
+        "@react-types/button": "^3.10.2",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
-        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/textfield": {
-      "version": "3.12.2",
-      "resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.12.2.tgz",
-      "integrity": "sha512-wRg8LJjZV6o4S/LRFqxs5waGDTiuIa/CRN+/X37Fu7GeZFeK0IBvWjKPlXLe7gMswaFqRmTKnQCU42mzUdDK1g==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/label": "^3.7.2",
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/textfield": "^3.8.1",
+      "version": "3.16.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.16.0.tgz",
+      "integrity": "sha512-53RVpMeMDN/QoabqnYZ1lxTh1xTQ3IBYQARuayq5EGGMafyxoFHzttxUdSqkZGK/+zdSF2GfmjOYJVm2nDKuDQ==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/form": "^3.0.12",
+        "@react-aria/label": "^3.7.14",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/form": "^3.1.1",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/shared": "^3.27.0",
+        "@react-types/textfield": "^3.11.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/toggle": {
-      "version": "3.8.2",
-      "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.8.2.tgz",
-      "integrity": "sha512-0+RmlOQtyRmU+Dd9qM9od4DPpITC7jqA+n3aZn732XtCsosz5gPGbhFuLbSdWRZ42FQgqo7pZQWaDRZpJPkipA==",
+      "version": "3.10.11",
+      "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.11.tgz",
+      "integrity": "sha512-J3jO3KJiUbaYVDEpeXSBwqcyKxpi9OreiHRGiaxb6VwB+FWCj7Gb2WKajByXNyfs8jc6kX9VUFaXa7jze60oEQ==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/toggle": "^3.8.1",
+        "@react-types/checkbox": "^3.9.1",
+        "@react-types/shared": "^3.27.0",
+        "@swc/helpers": "^0.5.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
+      }
+    },
+    "node_modules/@react-aria/toolbar": {
+      "version": "3.0.0-beta.12",
+      "resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.12.tgz",
+      "integrity": "sha512-a+Be27BtM2lzEdTzm19FikPbitfW65g/JZln3kyAvgpswhU6Ljl8lztaVw4ixjG4H0nqnKvVggMy4AlWwDUaVQ==",
       "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/toggle": "^3.6.3",
-        "@react-types/checkbox": "^3.5.2",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/switch": "^3.4.2",
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/i18n": "^3.12.5",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/tooltip": {
-      "version": "3.6.4",
-      "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.6.4.tgz",
-      "integrity": "sha512-5WCOiRSugzbfEOH+Bjpuf6EsNyynqq5S1uDh/P6J8qiYDjc0xLRJ5dyLdytX7c8MK9Y0pIHi6xb0xR9jDqJXTw==",
-      "dependencies": {
-        "@react-aria/focus": "^3.14.3",
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/tooltip": "^3.4.5",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/tooltip": "^3.4.5",
+      "version": "3.7.11",
+      "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.7.11.tgz",
+      "integrity": "sha512-mhZgAWUj7bUWipDeJXaVPZdqnzoBCd/uaEbdafnvgETmov1udVqPTh9w4ZKX2Oh1wa2+OdLFrBOk+8vC6QbWag==",
+      "dependencies": {
+        "@react-aria/focus": "^3.19.1",
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-stately/tooltip": "^3.5.1",
+        "@react-types/shared": "^3.27.0",
+        "@react-types/tooltip": "^3.4.14",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/utils": {
-      "version": "3.21.1",
-      "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.21.1.tgz",
-      "integrity": "sha512-tySfyWHXOhd/b6JSrSOl7krngEXN3N6pi1hCAXObRu3+MZlaZOMDf/j18aoteaIF2Jpv8HMWUJUJtQKGmBJGRA==",
+      "version": "3.27.0",
+      "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz",
+      "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==",
       "dependencies": {
-        "@react-aria/ssr": "^3.8.0",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/shared": "^3.21.0",
+        "@react-aria/ssr": "^3.9.7",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0",
-        "clsx": "^1.1.1"
+        "clsx": "^2.0.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-aria/visually-hidden": {
-      "version": "3.8.6",
-      "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.6.tgz",
-      "integrity": "sha512-6DmS/JLbK9KgU/ClK1WjwOyvpn8HtwYn+uisMLdP7HlCm692peYOkXDR1jqYbHL4GlyLCD0JLI+/xGdVh5aR/w==",
+      "version": "3.8.19",
+      "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.19.tgz",
+      "integrity": "sha512-MZgCCyQ3sdG94J5iJz7I7Ai3IxoN0U5d/+EaUnA1mfK7jf2fSYQBqi6Eyp8sWUYzBTLw4giXB5h0RGAnWzk9hA==",
       "dependencies": {
-        "@react-aria/interactions": "^3.19.1",
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/shared": "^3.21.0",
-        "@swc/helpers": "^0.5.0",
-        "clsx": "^1.1.1"
+        "@react-aria/interactions": "^3.23.0",
+        "@react-aria/utils": "^3.27.0",
+        "@react-types/shared": "^3.27.0",
+        "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
+        "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/calendar": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.4.1.tgz",
-      "integrity": "sha512-XKCdrXNA7/ukZ842EeDZfLqYUQDv/x5RoAVkzTbp++3U/MLM1XZXsqj+5xVlQfJiWpQzM9L6ySjxzzgepJDeuw==",
-      "dependencies": {
-        "@internationalized/date": "^3.5.0",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/calendar": "^3.4.1",
-        "@react-types/datepicker": "^3.6.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.7.0.tgz",
+      "integrity": "sha512-N15zKubP2S7eWfPSJjKVlmJA7YpWzrIGx52BFhwLSQAZcV+OPcMgvOs71WtB7PLwl6DUYQGsgc0B3tcHzzvdvQ==",
+      "dependencies": {
+        "@internationalized/date": "^3.7.0",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/calendar": "^3.6.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/checkbox": {
-      "version": "3.5.1",
-      "resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.5.1.tgz",
-      "integrity": "sha512-j+EbHpZgS8J2LbysbVDK3vQAJc7YZHOjHRX20auEzVmulAFKwkRpevo/R5gEL4EpOz4bRyu+BH/jbssHXG+Ezw==",
-      "dependencies": {
-        "@react-stately/toggle": "^3.6.3",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/checkbox": "^3.5.2",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.6.11",
+      "resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.6.11.tgz",
+      "integrity": "sha512-jApdBis+Q1sXLivg+f7krcVaP/AMMMiQcVqcz5gwxlweQN+dRZ/NpL0BYaDOuGc26Mp0lcuVaET3jIZeHwtyxA==",
+      "dependencies": {
+        "@react-stately/form": "^3.1.1",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/checkbox": "^3.9.1",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/collections": {
-      "version": "3.10.2",
-      "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.10.2.tgz",
-      "integrity": "sha512-h+LzCa1gWhVRWVH8uR+ZxsKmFSx7kW3RIlcjWjhfyc59BzXCuojsOJKTTAyPVFP/3kOdJeltw8g/reV1Cw/x6Q==",
+      "version": "3.12.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.1.tgz",
+      "integrity": "sha512-8QmFBL7f+P64dEP4o35pYH61/lP0T/ziSdZAvNMrCqaM+fXcMfUp2yu1E63kADVX7WRDsFJWE3CVMeqirPH6Xg==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/combobox": {
-      "version": "3.7.1",
-      "resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.7.1.tgz",
-      "integrity": "sha512-JMKsbhCgP8HpwRjHLBmJILzyU9WzWykjXyP4QF/ifmkzGRjC/s46+Ieq+WonjVaLNGCoi6XqhYn2x2RyACSbsQ==",
-      "dependencies": {
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/list": "^3.10.0",
-        "@react-stately/menu": "^3.5.6",
-        "@react-stately/select": "^3.5.5",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/combobox": "^3.8.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.10.2",
+      "resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.10.2.tgz",
+      "integrity": "sha512-uT642Dool4tQBh+8UQjlJnTisrJVtg3LqmiP/HqLQ4O3pW0O+ImbG+2r6c9dUzlAnH4kEfmEwCp9dxkBkmFWsg==",
+      "dependencies": {
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/form": "^3.1.1",
+        "@react-stately/list": "^3.11.2",
+        "@react-stately/overlays": "^3.6.13",
+        "@react-stately/select": "^3.6.10",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/combobox": "^3.13.2",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/datepicker": {
-      "version": "3.8.0",
-      "resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.8.0.tgz",
-      "integrity": "sha512-6YDSmkrRafYCWhRHks8Z2tZavM1rqSOy8GY8VYjYMCVTFpRuhPK9TQaFv2BdzZL/vJ6OGThxqoglcEwywZVq2g==",
-      "dependencies": {
-        "@internationalized/date": "^3.5.0",
-        "@internationalized/string": "^3.1.1",
-        "@react-stately/overlays": "^3.6.3",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/datepicker": "^3.6.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.12.0.tgz",
+      "integrity": "sha512-AfJEP36d+QgQ30GfacXtYdGsJvqY2yuCJ+JrjHct+m1nYuTkMvMMnhwNBFasgDJPLCDyHzyANlWkl2kQGfsBFw==",
+      "dependencies": {
+        "@internationalized/date": "^3.7.0",
+        "@internationalized/string": "^3.2.5",
+        "@react-stately/form": "^3.1.1",
+        "@react-stately/overlays": "^3.6.13",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/datepicker": "^3.10.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/dnd": {
-      "version": "3.2.5",
-      "resolved": "https://registry.npmjs.org/@react-stately/dnd/-/dnd-3.2.5.tgz",
-      "integrity": "sha512-f9S+ycjAMEaz9HqGxkx4jsqo/ZS8kh0o97rxSKpGFKPZ02UMFWCr9lJI1p3hVGukiMahrmsNtoQXAvMcFAZyQQ==",
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/dnd/-/dnd-3.5.1.tgz",
+      "integrity": "sha512-N18wt6fka9ngJJqxfAzmdtyrk9whAnqWUxZn22CatjNQsqukI4a6KRYwZTXM9x/wm7KamhVOp+GBl85zM8GLdA==",
       "dependencies": {
-        "@react-stately/selection": "^3.14.0",
-        "@react-types/shared": "^3.21.0",
+        "@react-stately/selection": "^3.19.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/flags": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.0.0.tgz",
-      "integrity": "sha512-e3i2ItHbIa0eEwmSXAnPdD7K8syW76JjGe8ENxwFJPW/H1Pu9RJfjkCb/Mq0WSPN/TpxBb54+I9TgrGhbCoZ9w==",
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.0.5.tgz",
+      "integrity": "sha512-6wks4csxUwPCp23LgJSnkBRhrWpd9jGd64DjcCTNB2AHIFu7Ab1W59pJpUL6TW7uAxVxdNKjgn6D1hlBy8qWsA==",
       "dependencies": {
-        "@swc/helpers": "^0.4.14"
+        "@swc/helpers": "^0.5.0"
       }
     },
-    "node_modules/@react-stately/flags/node_modules/@swc/helpers": {
-      "version": "0.4.36",
-      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.36.tgz",
-      "integrity": "sha512-5lxnyLEYFskErRPenYItLRSge5DjrJngYKdVjRSrWfza9G6KkgHEXi0vUZiyUeMU5JfXH1YnvXZzSp8ul88o2Q==",
+    "node_modules/@react-stately/form": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.1.1.tgz",
+      "integrity": "sha512-qavrz5X5Mdf/Q1v/QJRxc0F8UTNEyRCNSM1we/nnF7GV64+aYSDLOtaRGmzq+09RSwo1c8ZYnIkK5CnwsPhTsQ==",
       "dependencies": {
-        "legacy-swc-helpers": "npm:@swc/helpers@=0.4.14",
-        "tslib": "^2.4.0"
-      }
-    },
-    "node_modules/@react-stately/flags/node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
-    },
-    "node_modules/@react-stately/grid": {
-      "version": "3.8.2",
-      "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.8.2.tgz",
-      "integrity": "sha512-CB5QpYjXFatuXZodj3r0vIiqTysUe6DURZdJu6RKG2Elx19n2k49fKyx7P7CTKD2sPBOMSSX4edWuTzpL8Tl+A==",
-      "dependencies": {
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/selection": "^3.14.0",
-        "@react-types/grid": "^3.2.2",
-        "@react-types/shared": "^3.21.0",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
-    "node_modules/@react-stately/layout": {
-      "version": "3.13.3",
-      "resolved": "https://registry.npmjs.org/@react-stately/layout/-/layout-3.13.3.tgz",
-      "integrity": "sha512-AZ2Sm7iSRcRsNATXg7bjbPpZIjV3z7bHAJtICWA1wHieVVSV1FFoyDyiXdDTIOxyuGeytNPaxtGfPpFZia9Wsg==",
-      "dependencies": {
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/table": "^3.11.2",
-        "@react-stately/virtualizer": "^3.6.4",
-        "@react-types/grid": "^3.2.2",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/table": "^3.9.0",
+    "node_modules/@react-stately/grid": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.10.1.tgz",
+      "integrity": "sha512-MOIy//AdxZxIXIzvWSKpvMvaPEMZGQNj+/cOsElHepv/Veh0psNURZMh2TP6Mr0+MnDTZbX+5XIeinGkWYO3JQ==",
+      "dependencies": {
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/selection": "^3.19.0",
+        "@react-types/grid": "^3.2.11",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/list": {
-      "version": "3.10.0",
-      "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.10.0.tgz",
-      "integrity": "sha512-Yspumiln2fvzoO8AND8jNAIfBu1XPaYioeeDmsB5Vrya2EvOkzEGsauQSNBJ6Vhee1fQqpnmzH1HB0jfIKUfzg==",
+      "version": "3.11.2",
+      "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.11.2.tgz",
+      "integrity": "sha512-eU2tY3aWj0SEeC7lH9AQoeAB4LL9mwS54FvTgHHoOgc1ZIwRJUaZoiuETyWQe98AL8KMgR1nrnDJ1I+CcT1Y7g==",
       "dependencies": {
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/selection": "^3.14.0",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/shared": "^3.21.0",
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/selection": "^3.19.0",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/menu": {
-      "version": "3.5.6",
-      "resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.5.6.tgz",
-      "integrity": "sha512-Cm82SVda1qP71Fcz8ohIn3JYKmKCuSUIFr1WsEo/YwDPkX0x9+ev6rmphHTsxDdkCLcYHSTQL6e2KL0wAg50zA==",
-      "dependencies": {
-        "@react-stately/overlays": "^3.6.3",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/menu": "^3.9.5",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.9.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.9.1.tgz",
+      "integrity": "sha512-WRjGGImhQlQaer/hhahGytwd1BDq3fjpTkY/04wv3cQJPJR6lkVI5nSvGFMHfCaErsA1bNyB8/T9Y5F5u4u9ng==",
+      "dependencies": {
+        "@react-stately/overlays": "^3.6.13",
+        "@react-types/menu": "^3.9.14",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/numberfield": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/@react-stately/numberfield/-/numberfield-3.6.2.tgz",
-      "integrity": "sha512-li/SO3BU3RGySRNlXhPRKr161GJyNbQe6kjnj+0BFTS/ST9nxCgxFK4llHf+S+I/shNI6+0U2nAjE85QOv4emQ==",
-      "dependencies": {
-        "@internationalized/number": "^3.3.0",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/numberfield": "^3.6.1",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.9.9",
+      "resolved": "https://registry.npmjs.org/@react-stately/numberfield/-/numberfield-3.9.9.tgz",
+      "integrity": "sha512-hZsLiGGHTHmffjFymbH1qVmA633rU2GNjMFQTuSsN4lqqaP8fgxngd5pPCoTCUFEkUgWjdHenw+ZFByw8lIE+g==",
+      "dependencies": {
+        "@internationalized/number": "^3.6.0",
+        "@react-stately/form": "^3.1.1",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/numberfield": "^3.8.8",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/overlays": {
-      "version": "3.6.3",
-      "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.3.tgz",
-      "integrity": "sha512-K3eIiYAdAGTepYqNf2pVb+lPqLoVudXwmxPhyOSZXzjgpynD6tR3E9QfWQtkMazBuU73PnNX7zkH4l87r2AmTg==",
+      "version": "3.6.13",
+      "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.13.tgz",
+      "integrity": "sha512-WsU85Gf/b+HbWsnnYw7P/Ila3wD+C37Uk/WbU4/fHgJ26IEOWsPE6wlul8j54NZ1PnLNhV9Fn+Kffi+PaJMQXQ==",
       "dependencies": {
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/overlays": "^3.8.3",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/overlays": "^3.8.12",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/radio": {
-      "version": "3.9.1",
-      "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.9.1.tgz",
-      "integrity": "sha512-DrQPHiP9pz1uQbBP/NDFdO8uOZigPbvuAWPUNK7Gq6kye5lW+RsS97IUnYJePNTSMvhiAVz/aleBt05Gr/PZmg==",
-      "dependencies": {
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/radio": "^3.5.2",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.10.10",
+      "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.10.10.tgz",
+      "integrity": "sha512-9x3bpq87uV8iYA4NaioTTWjriQSlSdp+Huqlxll0T3W3okpyraTTejE91PbIoRTUmL5qByIh2WzxYmr4QdBgAA==",
+      "dependencies": {
+        "@react-stately/form": "^3.1.1",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/radio": "^3.8.6",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/searchfield": {
-      "version": "3.4.6",
-      "resolved": "https://registry.npmjs.org/@react-stately/searchfield/-/searchfield-3.4.6.tgz",
-      "integrity": "sha512-DeVacER0MD35gzQjrYpX/e3k8rjKF82W0OooTkRjeQ2U48femZkQpmp3O+j10foQx2LLaxqt9PSW7QS0Ww1bCA==",
+      "version": "3.5.9",
+      "resolved": "https://registry.npmjs.org/@react-stately/searchfield/-/searchfield-3.5.9.tgz",
+      "integrity": "sha512-7/aO/oLJ4czKEji0taI/lbHKqPJRag9p3YmRaZ4yqjIMpKxzmJCWQcov5lzWeFhG/1hINKndYlxFnVIKV/urpg==",
       "dependencies": {
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/searchfield": "^3.5.1",
-        "@react-types/shared": "^3.21.0",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/searchfield": "^3.5.11",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/select": {
-      "version": "3.5.5",
-      "resolved": "https://registry.npmjs.org/@react-stately/select/-/select-3.5.5.tgz",
-      "integrity": "sha512-nDkvFeAZbN7dK/Ty+mk1h4LZYYaoPpkwrG49wa67DTHkCc8Zk2+UEjhKPwOK20th4vfJKHzKjVa0Dtq4DIj0rw==",
-      "dependencies": {
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/list": "^3.10.0",
-        "@react-stately/menu": "^3.5.6",
-        "@react-stately/selection": "^3.14.0",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/select": "^3.8.4",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.6.10",
+      "resolved": "https://registry.npmjs.org/@react-stately/select/-/select-3.6.10.tgz",
+      "integrity": "sha512-V7V0FCL9T+GzLjyfnJB6PUaKldFyT/8Rj6M+R9ura1A0O+s/FEOesy0pdMXFoL1l5zeUpGlCnhJrsI5HFWHfDw==",
+      "dependencies": {
+        "@react-stately/form": "^3.1.1",
+        "@react-stately/list": "^3.11.2",
+        "@react-stately/overlays": "^3.6.13",
+        "@react-types/select": "^3.9.9",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/selection": {
-      "version": "3.14.0",
-      "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.14.0.tgz",
-      "integrity": "sha512-E5rNH+gVGDJQDSnPO30ynu6jZ0Z0++VPUbM5Bu3P/bZ3+TgoTtDDvlONba3fspgSBDfdnHpsuG9eqYnDtEAyYA==",
+      "version": "3.19.0",
+      "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.19.0.tgz",
+      "integrity": "sha512-AvbUqnWjqVQC48RD39S9BpMKMLl55Zo5l/yx5JQFPl55cFwe9Tpku1KY0wzt3fXXiXWaqjDn/7Gkg1VJYy8esQ==",
       "dependencies": {
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/shared": "^3.21.0",
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/slider": {
-      "version": "3.4.4",
-      "resolved": "https://registry.npmjs.org/@react-stately/slider/-/slider-3.4.4.tgz",
-      "integrity": "sha512-tFexbtN50zSo6e1Gi8K9MBfqgOo1eemF/VvFbde3PP9nG+ODcxEIajaYDPlMUuFw5cemJuoKo3+G5NBBn2/AjQ==",
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/slider/-/slider-3.6.1.tgz",
+      "integrity": "sha512-8kij5O82Xe233vZZ6qNGqPXidnlNQiSnyF1q613c7ktFmzAyGjkIWVUapHi23T1fqm7H2Rs3RWlmwE9bo2KecA==",
       "dependencies": {
-        "@react-aria/i18n": "^3.8.4",
-        "@react-aria/utils": "^3.21.1",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/slider": "^3.6.2",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/shared": "^3.27.0",
+        "@react-types/slider": "^3.7.8",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/table": {
-      "version": "3.11.2",
-      "resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.11.2.tgz",
-      "integrity": "sha512-EVgksPAsnEoqeT+5ej4aGJdu9kAu3LCDqQfnmif2P/R1BP5eDU1Kv0N/mV/90Xp546g7kuZ1wS2if/hWDXEA5g==",
-      "dependencies": {
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/flags": "^3.0.0",
-        "@react-stately/grid": "^3.8.2",
-        "@react-stately/selection": "^3.14.0",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/grid": "^3.2.2",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/table": "^3.9.0",
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.13.1.tgz",
+      "integrity": "sha512-Im8W+F8o9EhglY5kqRa3xcMGXl8zBi6W5phGpAjXb+UGDL1tBIlAcYj733bw8g/ITCnaSz9ubsmON0HekPd6Jg==",
+      "dependencies": {
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/flags": "^3.0.5",
+        "@react-stately/grid": "^3.10.1",
+        "@react-stately/selection": "^3.19.0",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/grid": "^3.2.11",
+        "@react-types/shared": "^3.27.0",
+        "@react-types/table": "^3.10.4",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/tabs": {
-      "version": "3.6.1",
-      "resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.6.1.tgz",
-      "integrity": "sha512-akGmejEaXg2RMZuWbRZ0W1MLr515e0uV0iVZefKBlcHtD/mK9K9Bo2XxBScf0TIhaPJ6Qa2w2k2+V7RmT7r8Ag==",
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.7.1.tgz",
+      "integrity": "sha512-gr9ACyuWrYuc727h7WaHdmNw8yxVlUyQlguziR94MdeRtFGQnf3V6fNQG3kxyB77Ljko69tgDF7Nf6kfPUPAQQ==",
       "dependencies": {
-        "@react-stately/list": "^3.10.0",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/shared": "^3.21.0",
-        "@react-types/tabs": "^3.3.3",
+        "@react-stately/list": "^3.11.2",
+        "@react-types/shared": "^3.27.0",
+        "@react-types/tabs": "^3.3.12",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/toggle": {
-      "version": "3.6.3",
-      "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.6.3.tgz",
-      "integrity": "sha512-4kIMTjRjtaapFk4NVmBoFDUYfkmyqDaYAmHpRyEIHTDpBYn0xpxZL/MHv9WuLYa4MjJLRp0MeicuWiZ4ai7f6Q==",
+      "version": "3.8.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.8.1.tgz",
+      "integrity": "sha512-MVpe79ghVQiwLmVzIPhF/O/UJAUc9B+ZSylVTyJiEPi0cwhbkKGQv9thOF0ebkkRkace5lojASqUAYtSTZHQJA==",
       "dependencies": {
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/checkbox": "^3.5.2",
-        "@react-types/shared": "^3.21.0",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/checkbox": "^3.9.1",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/tooltip": {
-      "version": "3.4.5",
-      "resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.4.5.tgz",
-      "integrity": "sha512-VrwQcjnrNddSulh+Zql8P8cORRnWqSPkHPqQwD/Ly91Rva3gUIy+VwnYeThbGDxRzlUv1wfN+UQraEcrgwSZ/Q==",
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.5.1.tgz",
+      "integrity": "sha512-0aI3U5kB7Cop9OCW9/Bag04zkivFSdUcQgy/TWL4JtpXidVWmOha8txI1WySawFSjZhH83KIyPc+wKm1msfLMQ==",
       "dependencies": {
-        "@react-stately/overlays": "^3.6.3",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/tooltip": "^3.4.5",
+        "@react-stately/overlays": "^3.6.13",
+        "@react-types/tooltip": "^3.4.14",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/tree": {
-      "version": "3.7.3",
-      "resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.7.3.tgz",
-      "integrity": "sha512-wB/68qetgCYTe7OMqbTFmtWRrEqVdIH2VlACPCsMlECr3lW9TrrbrOwlHIJfLhkxWvY3kSCoKcOJ5KTiJC9LGA==",
-      "dependencies": {
-        "@react-stately/collections": "^3.10.2",
-        "@react-stately/selection": "^3.14.0",
-        "@react-stately/utils": "^3.8.0",
-        "@react-types/shared": "^3.21.0",
+      "version": "3.8.7",
+      "resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.8.7.tgz",
+      "integrity": "sha512-hpc3pyuXWeQV5ufQ02AeNQg/MYhnzZ4NOznlY5OOUoPzpLYiI3ZJubiY3Dot4jw5N/LR7CqvDLHmrHaJPmZlHg==",
+      "dependencies": {
+        "@react-stately/collections": "^3.12.1",
+        "@react-stately/selection": "^3.19.0",
+        "@react-stately/utils": "^3.10.5",
+        "@react-types/shared": "^3.27.0",
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-stately/utils": {
-      "version": "3.8.0",
-      "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.8.0.tgz",
-      "integrity": "sha512-wCIoFDbt/uwNkWIBF+xV+21k8Z8Sj5qGO3uptTcVmjYcZngOaGGyB4NkiuZhmhG70Pkv+yVrRwoC1+4oav9cCg==",
+      "version": "3.10.5",
+      "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz",
+      "integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==",
       "dependencies": {
         "@swc/helpers": "^0.5.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
-      }
-    },
-    "node_modules/@react-stately/virtualizer": {
-      "version": "3.6.4",
-      "resolved": "https://registry.npmjs.org/@react-stately/virtualizer/-/virtualizer-3.6.4.tgz",
-      "integrity": "sha512-lf3+FDRnyLyY1IhLfwA6GuE/9F3nIEc5p245NkUSN1ngKlXI5PvLHNatiVbONC3wt90abkpMK+WMhu2S/B+4lA==",
-      "dependencies": {
-        "@react-aria/utils": "^3.21.1",
-        "@react-types/shared": "^3.21.0",
-        "@swc/helpers": "^0.5.0"
-      },
-      "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/breadcrumbs": {
-      "version": "3.7.1",
-      "resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.1.tgz",
-      "integrity": "sha512-WWC5pQdWkAzJ2hkx4w7f+waDLLvuD9vowKey+bdLoEmKvdaHNLLVUQPEyFm6SQ5+E3pNBWkNx9a+0S9iW6wa+Q==",
+      "version": "3.7.10",
+      "resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.10.tgz",
+      "integrity": "sha512-5HhRxkKHfAQBoyOYzyf4HT+24HgPE/C/QerxJLNNId303LXO03yeYrbvRqhYZSlD1ACLJW9OmpPpREcw5iSqgw==",
       "dependencies": {
-        "@react-types/link": "^3.5.1",
-        "@react-types/shared": "^3.21.0"
+        "@react-types/link": "^3.5.10",
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/button": {
-      "version": "3.9.0",
-      "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.9.0.tgz",
-      "integrity": "sha512-YhbchUDB7yL88ZFA0Zqod6qOMdzCLD5yVRmhWymk0yNLvB7EB1XX4c5sRANalfZSFP0RpCTlkjB05Hzp4+xOYg==",
+      "version": "3.10.2",
+      "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.10.2.tgz",
+      "integrity": "sha512-h8SB/BLoCgoBulCpyzaoZ+miKXrolK9XC48+n1dKJXT8g4gImrficurDW6+PRTQWaRai0Q0A6bu8UibZOU4syg==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/calendar": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.4.1.tgz",
-      "integrity": "sha512-tiCkHi6IQtYcVoAESG79eUBWDXoo8NImo+Mj8WAWpo1lOA3SV1W2PpeXkoRNqtloilQ0aYcmsaJJUhciQG4ndg==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.6.0.tgz",
+      "integrity": "sha512-BtFh4BFwvsYlsaSqUOVxlqXZSlJ6u4aozgO3PwHykhpemwidlzNwm9qDZhcMWPioNF/w2cU/6EqhvEKUHDnFZg==",
       "dependencies": {
-        "@internationalized/date": "^3.5.0",
-        "@react-types/shared": "^3.21.0"
+        "@internationalized/date": "^3.7.0",
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/checkbox": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.5.2.tgz",
-      "integrity": "sha512-iRQrbY8vRRya3bt3i7sHAifhP/ozfkly1/TItkRK5MNPRNPRDKns55D8ZFkRMj4NSyKQpjVt1zzlBXrnSOxWdQ==",
+      "version": "3.9.1",
+      "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.9.1.tgz",
+      "integrity": "sha512-0x/KQcipfNM9Nvy6UMwYG25roRLvsiqf0J3woTYylNNWzF+72XT0iI5FdJkE3w2wfa0obmSoeq4WcbFREQrH/A==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/combobox": {
-      "version": "3.8.1",
-      "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.8.1.tgz",
-      "integrity": "sha512-F910tk8K5qE0TksJ9LRGcJIpaPzpsCnFxT6E9oJH3ssK4N8qZL8QfT9tIKo2XWhK9Uxb/tIZOGQwA8Cn7TyZrA==",
+      "version": "3.13.2",
+      "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.13.2.tgz",
+      "integrity": "sha512-yl2yMcM5/v3lJiNZWjpAhQ9vRW6dD55CD4rYmO2K7XvzYJaFVT4WYI/AymPYD8RqomMp7coBmBHfHW0oupk8gg==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/datepicker": {
-      "version": "3.6.1",
-      "resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.6.1.tgz",
-      "integrity": "sha512-/M+0e9hL9w98f5k4EoxeH2UfPsUPoS6fvmFsmwUZJcDiw7wP510XngnDLy9GOHj9xgqagZ20S79cxcEuTq7U6g==",
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.10.0.tgz",
+      "integrity": "sha512-Att7y4NedNH1CogMDIX9URXgMLxGbZgnFCZ8oxgFAVndWzbh3TBcc4s7uoJDPvgRMAalq+z+SrlFFeoBeJmvvg==",
       "dependencies": {
-        "@internationalized/date": "^3.5.0",
-        "@react-types/calendar": "^3.4.1",
-        "@react-types/overlays": "^3.8.3",
-        "@react-types/shared": "^3.21.0"
+        "@internationalized/date": "^3.7.0",
+        "@react-types/calendar": "^3.6.0",
+        "@react-types/overlays": "^3.8.12",
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/dialog": {
-      "version": "3.5.6",
-      "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.6.tgz",
-      "integrity": "sha512-lwwaAgoi4xe4eEJxBns+cBIRstIPTKWWddMkp51r7Teeh2uKs1Wki7N+Acb9CfT6JQTQDqtVJm6K76rcqNBVwg==",
+      "version": "3.5.15",
+      "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.15.tgz",
+      "integrity": "sha512-BX1+mV35Oa0aIlhu98OzJaSB7uiCWDPQbr0AkpFBajSSlESUoAjntN+4N+QJmj24z2v6UE9zxGQ85/U/0Le+bw==",
       "dependencies": {
-        "@react-types/overlays": "^3.8.3",
-        "@react-types/shared": "^3.21.0"
+        "@react-types/overlays": "^3.8.12",
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/grid": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.2.tgz",
-      "integrity": "sha512-R4USOpn1xfsWVGwZsakRlIdsBA10XNCnAUcRXQTn2JmzLjDCtcln6uYo9IFob080lQuvjkSw3j4zkw7Yo4Qepg==",
-      "dependencies": {
-        "@react-types/shared": "^3.21.0"
-      },
-      "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
-      }
-    },
-    "node_modules/@react-types/label": {
-      "version": "3.8.1",
-      "resolved": "https://registry.npmjs.org/@react-types/label/-/label-3.8.1.tgz",
-      "integrity": "sha512-fA6zMTF2TmfU7H8JBJi0pNd8t5Ak4gO+ZA3cZBysf8r3EmdAsgr3LLqFaGTnZzPH1Fux6c7ARI3qjVpyNiejZQ==",
+      "version": "3.2.11",
+      "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.11.tgz",
+      "integrity": "sha512-Mww9nrasppvPbsBi+uUqFnf7ya8fXN0cTVzDNG+SveD8mhW+sbtuy+gPtEpnFD2Oyi8qLuObefzt4gdekJX2Yw==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/link": {
-      "version": "3.5.1",
-      "resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.5.1.tgz",
-      "integrity": "sha512-hX2KpjB7wSuJw5Pia63+WEgEql53VfVG1Vu2cTUJDxfrgUtawwHtxB8B0K3cs3jBanq69amgAInEx0FfqYY0uQ==",
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.5.10.tgz",
+      "integrity": "sha512-IM2mbSpB0qP44Jh1Iqpevo7bQdZAr0iDyDi13OhsiUYJeWgPMHzGEnQqdBMkrfQeOTXLtZtUyOYLXE2v39bhzQ==",
       "dependencies": {
-        "@react-aria/interactions": "^3.19.1",
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/listbox": {
-      "version": "3.4.5",
-      "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.4.5.tgz",
-      "integrity": "sha512-nuRY3l8h/rBYQWTXWdZz5YJdl6QDDmXpHrnPuX7PxTwbXcwjhoMK+ZkJ0arA8Uv3MPs1OUcT6K6CInsPnG2ARQ==",
+      "version": "3.5.4",
+      "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.5.4.tgz",
+      "integrity": "sha512-5otTes0zOwRZwNtqysPD/aW4qFJSxd5znjwoWTLnzDXXOBHXPyR83IJf8ITgvIE5C0y+EFadsWR/BBO3k9Pj7g==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/menu": {
-      "version": "3.9.5",
-      "resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.9.5.tgz",
-      "integrity": "sha512-KB5lJM0p9PxwpVlHV9sRdpjh+sqINeHrJgGizy/cQI9bj26nupiEgamSD14dULNI6BFT9DkgKCsobBtE04DDKQ==",
+      "version": "3.9.14",
+      "resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.9.14.tgz",
+      "integrity": "sha512-RJW/S8IPwbRuohJ/A9HJ7W8QaAY816tm7Nv6+H/TLXG76zu2AS5vEgq+0TcCAWvJJwUdLDpJWJMlo0iIoIBtcg==",
       "dependencies": {
-        "@react-types/overlays": "^3.8.3",
-        "@react-types/shared": "^3.21.0"
+        "@react-types/overlays": "^3.8.12",
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/meter": {
-      "version": "3.3.5",
-      "resolved": "https://registry.npmjs.org/@react-types/meter/-/meter-3.3.5.tgz",
-      "integrity": "sha512-7kSP/bqkt6ANHUJLJ4OsHOPNwg9ETvWHAKXDYoCqkLYzdhFh0H/8EAW9z4Bh/io0GvR7ePds9s+32iislfSwDg==",
+      "version": "3.4.6",
+      "resolved": "https://registry.npmjs.org/@react-types/meter/-/meter-3.4.6.tgz",
+      "integrity": "sha512-YczAht1VXy3s4fR6Dq0ibGsjulGHzS/A/K4tOruSNTL6EkYH9ktHX62Xk/OhCiKHxV315EbZ136WJaCeO4BgHw==",
       "dependencies": {
-        "@react-types/progress": "^3.5.0",
-        "@react-types/shared": "^3.21.0"
+        "@react-types/progress": "^3.5.9"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/numberfield": {
-      "version": "3.6.1",
-      "resolved": "https://registry.npmjs.org/@react-types/numberfield/-/numberfield-3.6.1.tgz",
-      "integrity": "sha512-jdMCN0mQ7eZkPrCKYkkG+jSjcG2VQ5P7mR9tTaCQeQK1wo+tF/8LWD+6n6dU7hH/qlU9sxVEg3U3kJ9sgNK+Hw==",
+      "version": "3.8.8",
+      "resolved": "https://registry.npmjs.org/@react-types/numberfield/-/numberfield-3.8.8.tgz",
+      "integrity": "sha512-825JPppxDaWh0Zxb0Q+wSslgRQYOtQPCAuhszPuWEy6d2F/M+hLR+qQqvQm9+LfMbdwiTg6QK5wxdWFCp2t7jw==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/overlays": {
-      "version": "3.8.3",
-      "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.3.tgz",
-      "integrity": "sha512-TrCG2I2+V+TD0PGi3CqfnyU5jEzcelSGgYJQvVxsl5Vv3ri7naBLIsOjF9x66tPxhINLCPUtOze/WYRAexp8aw==",
+      "version": "3.8.12",
+      "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.12.tgz",
+      "integrity": "sha512-ZvR1t0YV7/6j+6OD8VozKYjvsXT92+C/2LOIKozy7YUNS5KI4MkXbRZzJvkuRECVZOmx8JXKTUzhghWJM/3QuQ==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/progress": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.5.0.tgz",
-      "integrity": "sha512-c1KLQCfYjdUdkTcPy0ZW31dc2+D86ZiZRHPNOaSYFGJjk9ItbWWi8BQTwlrw6D2l/+0d/YDdUFGaZhHMrY9mBQ==",
+      "version": "3.5.9",
+      "resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.5.9.tgz",
+      "integrity": "sha512-zFxOzx3G8XUmHgpm037Hcayls5bqzXVa182E3iM7YWTmrjxJPKZ58XL0WWBgpTd+mJD7fTpnFdAZqSmFbtDOdA==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/radio": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.5.2.tgz",
-      "integrity": "sha512-crYQ+97abd5v0Iw9X+Tt+E7KWdm5ckr4g0+Iy8byV1g6MyiBOsNtq9QT99TOzyWJPqqD8T9qZfAOk49wK7KEDg==",
+      "version": "3.8.6",
+      "resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.8.6.tgz",
+      "integrity": "sha512-woTQYdRFjPzuml4qcIf+2zmycRuM5w3fDS5vk6CQmComVUjOFPtD28zX3Z9kc9lSNzaBQz9ONZfFqkZ1gqfICA==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/searchfield": {
-      "version": "3.5.1",
-      "resolved": "https://registry.npmjs.org/@react-types/searchfield/-/searchfield-3.5.1.tgz",
-      "integrity": "sha512-+v9fo50JrZOfFzbdgJsW39hyTFv1gVH458nx82aidYJzQocFJniiAEl0ZhhRzbE8RijyjLleKIAY+klPeFmEaQ==",
+      "version": "3.5.11",
+      "resolved": "https://registry.npmjs.org/@react-types/searchfield/-/searchfield-3.5.11.tgz",
+      "integrity": "sha512-MX8d9pgvxZxmgDwI0tiDaf6ijOY8XcRj0HM8Ocfttlk7PEFJK44p51WsUC+fPX1GmZni2JpFkx/haPOSLUECdw==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0",
-        "@react-types/textfield": "^3.8.1"
+        "@react-types/shared": "^3.27.0",
+        "@react-types/textfield": "^3.11.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/select": {
-      "version": "3.8.4",
-      "resolved": "https://registry.npmjs.org/@react-types/select/-/select-3.8.4.tgz",
-      "integrity": "sha512-jHBaLiAHTcYPz52kuJpypBbR0WAA+YCZHy2HH+W8711HuTqePZCEp6QAWHK9Fw0qwSZQ052jYaWvOsgEZZ6ojQ==",
+      "version": "3.9.9",
+      "resolved": "https://registry.npmjs.org/@react-types/select/-/select-3.9.9.tgz",
+      "integrity": "sha512-/hCd0o+ztn29FKCmVec+v7t4JpOzz56o+KrG7NDq2pcRWqUR9kNwCjrPhSbJIIEDm4ubtrfPu41ysIuDvRd2Bg==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/shared": {
-      "version": "3.21.0",
-      "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.21.0.tgz",
-      "integrity": "sha512-wJA2cUF8dP4LkuNUt9Vh2kkfiQb2NLnV2pPXxVnKJZ7d4x2/7VPccN+LYPnH8m0X3+rt50cxWuPKQmjxSsCFOg==",
+      "version": "3.27.0",
+      "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.27.0.tgz",
+      "integrity": "sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==",
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/slider": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.6.2.tgz",
-      "integrity": "sha512-LSvna1gpOvBxOBI5I/CYEtkAshWYwPlxE9F/jCaxCa9Q7E9xZp1hFFGY87iQ1A3vQM5SCa5PFStwOvXO7rA55w==",
+      "version": "3.7.8",
+      "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.7.8.tgz",
+      "integrity": "sha512-utW1o9KT70hqFwu1zqMtyEWmP0kSATk4yx+Fm/peSR4iZa+BasRqH83yzir5GKc8OfqfE1kmEsSlO98/k986+w==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/switch": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.4.2.tgz",
-      "integrity": "sha512-OQWpawikWhF+ET1/kE0/JeJVr6gHjkR72p/idTsT7RUJySBcehhAscbIA8iWzVWJvdFCVF2hG7uzBAJTeDMr9A==",
+      "version": "3.5.8",
+      "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.8.tgz",
+      "integrity": "sha512-sL7jmh8llF8BxzY4HXkSU4bwU8YU6gx45P85D0AdYXgRHxU9Cp7BQPOMF4pJoQ8TTej05MymY5q7xvJVmxUTAQ==",
       "dependencies": {
-        "@react-types/checkbox": "^3.5.2",
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/table": {
-      "version": "3.9.0",
-      "resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.9.0.tgz",
-      "integrity": "sha512-WOLxZ3tzLA4gxRxvnsZhnnQDbh4Qe/johpHNk4coSOFOP5W8PbunPacXnbvdPkSx6rqrOIzCnYcZCtgk4gDQmg==",
+      "version": "3.10.4",
+      "resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.10.4.tgz",
+      "integrity": "sha512-d0tLz/whxVteqr1rophtuuxqyknHHfTKeXrCgDjt8pAyd9U8GPDbfcFSfYPUhWdELRt7aLVyQw6VblZHioVEgQ==",
       "dependencies": {
-        "@react-types/grid": "^3.2.2",
-        "@react-types/shared": "^3.21.0"
+        "@react-types/grid": "^3.2.11",
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/tabs": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.3.tgz",
-      "integrity": "sha512-Zc4g5TIwJpKS5fiT9m4dypbCr1xqtauL4wqM76fGERCAZy0FwXTH/yjzHJDYKyWFJrQNWtJ0KAhJR/ZqKDVnIw==",
+      "version": "3.3.12",
+      "resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.12.tgz",
+      "integrity": "sha512-E9O9G+wf9kaQ8UbDEDliW/oxYlJnh7oDCW1zaMOySwnG4yeCh7Wu02EOCvlQW4xvgn/i+lbEWgirf7L+yj5nRg==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/textfield": {
-      "version": "3.8.1",
-      "resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.8.1.tgz",
-      "integrity": "sha512-p8Xmew9kzJd+tCM7h9LyebZHpv7SH1IE1Nu13hLCOV5cZ/tVVVCwjNGLMv4MtUpSn++H42YLJgAW9Uif+a+RHg==",
+      "version": "3.11.0",
+      "resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.11.0.tgz",
+      "integrity": "sha512-YORBgr6wlu2xfvr4MqjKFHGpj+z8LBzk14FbWDbYnnhGnv0I10pj+m2KeOHgDNFHrfkDdDOQmMIKn1UCqeUuEg==",
       "dependencies": {
-        "@react-types/shared": "^3.21.0"
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@react-types/tooltip": {
-      "version": "3.4.5",
-      "resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.4.5.tgz",
-      "integrity": "sha512-pv87Vlu+Pn1Titw199y5aiSuXF/GHX+fBCihi9BeePqtwYm505e/Si01BNh5ejCeXXOS4JIMuXwmGGzGVdGk6Q==",
+      "version": "3.4.14",
+      "resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.4.14.tgz",
+      "integrity": "sha512-J7CeYL2yPeKIasx1rPaEefyCHGEx2DOCx+7bM3XcKGmCxvNdVQLjimNJOt8IHlUA0nFJQOjmSW/mz9P0f2/kUw==",
       "dependencies": {
-        "@react-types/overlays": "^3.8.3",
-        "@react-types/shared": "^3.21.0"
+        "@react-types/overlays": "^3.8.12",
+        "@react-types/shared": "^3.27.0"
       },
       "peerDependencies": {
-        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+        "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
     "node_modules/@remix-run/router": {
       "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
       "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@babel/helper-module-imports": "^7.10.4",
         "@rollup/pluginutils": "^3.1.0"
       "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
       "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@types/estree": "0.0.39",
         "estree-walker": "^1.0.1",
       "version": "0.0.39",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
       "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
+      "dev": true
+    },
+    "node_modules/@rollup/plugin-babel/node_modules/estree-walker": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
+      "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
+      "dev": true
+    },
+    "node_modules/@rollup/plugin-babel/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
       "dev": true,
-      "license": "MIT"
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
     },
     "node_modules/@rollup/plugin-inject": {
       "version": "5.0.3",
         }
       }
     },
-    "node_modules/@rollup/plugin-inject/node_modules/estree-walker": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
-      "dev": true
-    },
     "node_modules/@rollup/plugin-node-resolve": {
-      "version": "15.2.3",
-      "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
-      "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
+      "version": "15.3.1",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
+      "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@rollup/pluginutils": "^5.0.1",
         "@types/resolve": "1.20.2",
         "deepmerge": "^4.2.2",
-        "is-builtin-module": "^3.2.1",
         "is-module": "^1.0.0",
         "resolve": "^1.22.1"
       },
         }
       }
     },
-    "node_modules/@rollup/plugin-node-resolve/node_modules/deepmerge": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
-      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/@rollup/plugin-replace": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz",
       "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@rollup/pluginutils": "^3.1.0",
         "magic-string": "^0.25.7"
       "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
       "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@types/estree": "0.0.39",
         "estree-walker": "^1.0.1",
       "version": "0.0.39",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
       "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
+    },
+    "node_modules/@rollup/plugin-replace/node_modules/estree-walker": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
+      "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
+      "dev": true
     },
     "node_modules/@rollup/plugin-replace/node_modules/magic-string": {
       "version": "0.25.9",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
       "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "sourcemap-codec": "^1.4.8"
       }
     },
+    "node_modules/@rollup/plugin-replace/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
     "node_modules/@rollup/plugin-terser": {
       "version": "0.4.4",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
       "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "serialize-javascript": "^6.0.1",
         "smob": "^1.0.0",
       }
     },
     "node_modules/@rollup/pluginutils": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
-      "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
+      "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
       "dev": true,
       "dependencies": {
         "@types/estree": "^1.0.0",
         "estree-walker": "^2.0.2",
-        "picomatch": "^2.3.1"
+        "picomatch": "^4.0.2"
       },
       "engines": {
         "node": ">=14.0.0"
       },
       "peerDependencies": {
-        "rollup": "^1.20.0||^2.0.0||^3.0.0"
+        "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
       },
       "peerDependenciesMeta": {
         "rollup": {
         }
       }
     },
-    "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
-      "dev": true
-    },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.8.0.tgz",
-      "integrity": "sha512-zdTObFRoNENrdPpnTNnhOljYIcOX7aI7+7wyrSpPFFIOf/nRdedE6IYsjaBE7tjukphh1tMTojgJ7p3lKY8x6Q==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
+      "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==",
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.8.0.tgz",
-      "integrity": "sha512-aiItwP48BiGpMFS9Znjo/xCNQVwTQVcRKkFKsO81m8exrGjHkCBDvm9PHay2kpa8RPnZzzKcD1iQ9KaLY4fPQQ==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz",
+      "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.8.0.tgz",
-      "integrity": "sha512-zhNIS+L4ZYkYQUjIQUR6Zl0RXhbbA0huvNIWjmPc2SL0cB1h5Djkcy+RZ3/Bwszfb6vgwUvcVJYD6e6Zkpsi8g==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz",
+      "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.8.0.tgz",
-      "integrity": "sha512-A/FAHFRNQYrELrb/JHncRWzTTXB2ticiRFztP4ggIUAfa9Up1qfW8aG2w/mN9jNiZ+HB0t0u0jpJgFXG6BfRTA==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz",
+      "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
       ]
     },
-    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.8.0.tgz",
-      "integrity": "sha512-JsidBnh3p2IJJA4/2xOF2puAYqbaczB3elZDT0qHxn362EIoIkq7hrR43Xa8RisgI6/WPfvb2umbGsuvf7E37A==",
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz",
+      "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==",
       "cpu": [
-        "arm"
+        "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
-        "linux"
+        "freebsd"
       ]
     },
-    "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.8.0.tgz",
-      "integrity": "sha512-hBNCnqw3EVCkaPB0Oqd24bv8SklETptQWcJz06kb9OtiShn9jK1VuTgi7o4zPSt6rNGWQOTDEAccbk0OqJmS+g==",
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz",
+      "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==",
       "cpu": [
-        "arm64"
+        "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
-        "linux"
+        "freebsd"
       ]
     },
-    "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.8.0.tgz",
-      "integrity": "sha512-Fw9ChYfJPdltvi9ALJ9wzdCdxGw4wtq4t1qY028b2O7GwB5qLNSGtqMsAel1lfWTZvf4b6/+4HKp0GlSYg0ahA==",
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz",
+      "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==",
       "cpu": [
-        "arm64"
+        "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
       ]
     },
-    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.8.0.tgz",
-      "integrity": "sha512-BH5xIh7tOzS9yBi8dFrCTG8Z6iNIGWGltd3IpTSKp6+pNWWO6qy8eKoRxOtwFbMrid5NZaidLYN6rHh9aB8bEw==",
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz",
+      "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==",
       "cpu": [
-        "riscv64"
+        "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
       ]
     },
-    "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.8.0.tgz",
-      "integrity": "sha512-PmvAj8k6EuWiyLbkNpd6BLv5XeYFpqWuRvRNRl80xVfpGXK/z6KYXmAgbI4ogz7uFiJxCnYcqyvZVD0dgFog7Q==",
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz",
+      "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==",
       "cpu": [
-        "x64"
+        "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
       ]
     },
-    "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.8.0.tgz",
-      "integrity": "sha512-mdxnlW2QUzXwY+95TuxZ+CurrhgrPAMveDWI97EQlA9bfhR8tw3Pt7SUlc/eSlCNxlWktpmT//EAA8UfCHOyXg==",
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz",
+      "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==",
       "cpu": [
-        "x64"
+        "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
       ]
     },
-    "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.8.0.tgz",
-      "integrity": "sha512-ge7saUz38aesM4MA7Cad8CHo0Fyd1+qTaqoIo+Jtk+ipBi4ATSrHWov9/S4u5pbEQmLjgUjB7BJt+MiKG2kzmA==",
+    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz",
+      "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz",
+      "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz",
+      "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz",
+      "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==",
+      "cpu": [
+        "s390x"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz",
+      "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz",
+      "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz",
+      "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.8.0.tgz",
-      "integrity": "sha512-p9E3PZlzurhlsN5h9g7zIP1DnqKXJe8ZUkFwAazqSvHuWfihlIISPxG9hCHCoA+dOOspL/c7ty1eeEVFTE0UTw==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz",
+      "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==",
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.8.0.tgz",
-      "integrity": "sha512-kb4/auKXkYKqlUYTE8s40FcJIj5soOyRLHKd4ugR0dCq0G2EfcF54eYcfQiGkHzjidZ40daB4ulsFdtqNKZtBg==",
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz",
+      "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
       "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
       "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==",
       "dev": true,
-      "license": "Apache-2.0",
       "dependencies": {
         "ejs": "^3.1.6",
         "json5": "^2.2.0",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
       "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "sourcemap-codec": "^1.4.8"
       }
     },
     "node_modules/@swc/core": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.5.tgz",
-      "integrity": "sha512-M8O22EEgdSONLd+7KRrXj8pn+RdAZZ7ISnPjE9KCQQlI0kkFNEquWR+uFdlFxQfwlyCe/Zb6uGXGDvtcov4IMg==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.7.tgz",
+      "integrity": "sha512-py91kjI1jV5D5W/Q+PurBdGsdU5TFbrzamP7zSCqLdMcHkKi3rQEM5jkQcZr0MXXSJTaayLxS3MWYTBIkzPDrg==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
-        "@swc/counter": "^0.1.2",
-        "@swc/types": "^0.1.5"
+        "@swc/counter": "^0.1.3",
+        "@swc/types": "^0.1.17"
       },
       "engines": {
         "node": ">=10"
         "url": "https://opencollective.com/swc"
       },
       "optionalDependencies": {
-        "@swc/core-darwin-arm64": "1.5.5",
-        "@swc/core-darwin-x64": "1.5.5",
-        "@swc/core-linux-arm-gnueabihf": "1.5.5",
-        "@swc/core-linux-arm64-gnu": "1.5.5",
-        "@swc/core-linux-arm64-musl": "1.5.5",
-        "@swc/core-linux-x64-gnu": "1.5.5",
-        "@swc/core-linux-x64-musl": "1.5.5",
-        "@swc/core-win32-arm64-msvc": "1.5.5",
-        "@swc/core-win32-ia32-msvc": "1.5.5",
-        "@swc/core-win32-x64-msvc": "1.5.5"
+        "@swc/core-darwin-arm64": "1.10.7",
+        "@swc/core-darwin-x64": "1.10.7",
+        "@swc/core-linux-arm-gnueabihf": "1.10.7",
+        "@swc/core-linux-arm64-gnu": "1.10.7",
+        "@swc/core-linux-arm64-musl": "1.10.7",
+        "@swc/core-linux-x64-gnu": "1.10.7",
+        "@swc/core-linux-x64-musl": "1.10.7",
+        "@swc/core-win32-arm64-msvc": "1.10.7",
+        "@swc/core-win32-ia32-msvc": "1.10.7",
+        "@swc/core-win32-x64-msvc": "1.10.7"
       },
       "peerDependencies": {
-        "@swc/helpers": "^0.5.0"
+        "@swc/helpers": "*"
       },
       "peerDependenciesMeta": {
         "@swc/helpers": {
       }
     },
     "node_modules/@swc/core-darwin-arm64": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.5.tgz",
-      "integrity": "sha512-Ol5ZwZYdTOZsv2NwjcT/qVVALKzVFeh+IJ4GNarr3P99+38Dkwi81OqCI1o/WaDXQYKAQC/V+CzMbkEuJJfq9Q==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.7.tgz",
+      "integrity": "sha512-SI0OFg987P6hcyT0Dbng3YRISPS9uhLX1dzW4qRrfqQdb0i75lPJ2YWe9CN47HBazrIA5COuTzrD2Dc0TcVsSQ==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@swc/core-darwin-x64": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.5.tgz",
-      "integrity": "sha512-XHWpKBIPKYLgh5/lV2PYjO84lkzf5JR51kjiloyz2Pa9HIV8tHoAP8bYdJwm4nUp2I7KcEh3pPH0AVu5LpxMKw==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.7.tgz",
+      "integrity": "sha512-RFIAmWVicD/l3RzxgHW0R/G1ya/6nyMspE2cAeDcTbjHi0I5qgdhBWd6ieXOaqwEwiCd0Mot1g2VZrLGoBLsjQ==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@swc/core-linux-arm-gnueabihf": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.5.tgz",
-      "integrity": "sha512-vtoWNCWAe+CNSqtqIwFnIH48qgPPlUZKoQ4EVFeMM+7/kDi6SeNxoh5TierJs5bKAWxD49VkPvRoWFCk6V62mA==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.7.tgz",
+      "integrity": "sha512-QP8vz7yELWfop5mM5foN6KkLylVO7ZUgWSF2cA0owwIaziactB2hCPZY5QU690coJouk9KmdFsPWDnaCFUP8tg==",
       "cpu": [
         "arm"
       ],
       }
     },
     "node_modules/@swc/core-linux-arm64-gnu": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.5.tgz",
-      "integrity": "sha512-L4l7M78U6h/rCAxId+y5Vu+1KfDRF6dJZtitFcaT293guiUQFwJv8gLxI4Jh5wFtZ0fYd0QaCuvh2Ip79CzGMg==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.7.tgz",
+      "integrity": "sha512-NgUDBGQcOeLNR+EOpmUvSDIP/F7i/OVOKxst4wOvT5FTxhnkWrW+StJGKj+DcUVSK5eWOYboSXr1y+Hlywwokw==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@swc/core-linux-arm64-musl": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.5.tgz",
-      "integrity": "sha512-DkzJc13ukXa7oJpyn24BjIgsiOybYrc+IxjsQyfNlDrrs1QXP4elStcpkD02SsIuSyHjZV8Hw2HFBMQB3OHPrA==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.7.tgz",
+      "integrity": "sha512-gp5Un3EbeSThBIh6oac5ZArV/CsSmTKj5jNuuUAuEsML3VF9vqPO+25VuxCvsRf/z3py+xOWRaN2HY/rjMeZog==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@swc/core-linux-x64-gnu": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.5.tgz",
-      "integrity": "sha512-kj4ZwWJGeBEUzHrRQP2VudN+kkkYH7OI1dPVDc6kWQx5X4329JeKOas4qY0l7gDVjBbRwN9IbbPI6TIn2KfAug==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.7.tgz",
+      "integrity": "sha512-k/OxLLMl/edYqbZyUNg6/bqEHTXJT15l9WGqsl/2QaIGwWGvles8YjruQYQ9d4h/thSXLT9gd8bExU2D0N+bUA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@swc/core-linux-x64-musl": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.5.tgz",
-      "integrity": "sha512-6pTorCs4mYhPhYtC4jNOnhGgjNd3DZcRoZ9P0tzXXP69aCbYjvlgNH/NRvAROp9AaVFeZ7a7PmCWb6+Rbe7NKg==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.7.tgz",
+      "integrity": "sha512-XeDoURdWt/ybYmXLCEE8aSiTOzEn0o3Dx5l9hgt0IZEmTts7HgHHVeRgzGXbR4yDo0MfRuX5nE1dYpTmCz0uyA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@swc/core-win32-arm64-msvc": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.5.tgz",
-      "integrity": "sha512-o0/9pstmEjwZyrY/bA+mymF0zH7E+GT/XCVqdKeWW9Wn3gTTyWa5MZnrFgI2THQ+AXwdglMB/Zo76ARQPaz/+A==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.7.tgz",
+      "integrity": "sha512-nYAbi/uLS+CU0wFtBx8TquJw2uIMKBnl04LBmiVoFrsIhqSl+0MklaA9FVMGA35NcxSJfcm92Prl2W2LfSnTqQ==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@swc/core-win32-ia32-msvc": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.5.tgz",
-      "integrity": "sha512-B+nypUwsmCuaH6RtKWgiPCb+ENjxstJPPJeMJvBqlJqyCaIkZzN4M07Ozi3xVv1VG21SRkd6G3xIqRoalrNc0Q==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.7.tgz",
+      "integrity": "sha512-+aGAbsDsIxeLxw0IzyQLtvtAcI1ctlXVvVcXZMNXIXtTURM876yNrufRo4ngoXB3jnb1MLjIIjgXfFs/eZTUSw==",
       "cpu": [
         "ia32"
       ],
       }
     },
     "node_modules/@swc/core-win32-x64-msvc": {
-      "version": "1.5.5",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.5.tgz",
-      "integrity": "sha512-ry83ki9ZX0Q+GWGnqc2J618Z+FvKE8Ajn42F8EYi8Wj0q6Jz3mj+pJzgzakk2INm2ldEZ+FaRPipn4ozsZDcBg==",
+      "version": "1.10.7",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.7.tgz",
+      "integrity": "sha512-TBf4clpDBjF/UUnkKrT0/th76/zwvudk5wwobiTFqDywMApHip5O0VpBgZ+4raY2TM8k5+ujoy7bfHb22zu17Q==",
       "cpu": [
         "x64"
       ],
       "dev": true
     },
     "node_modules/@swc/helpers": {
-      "version": "0.5.3",
-      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz",
-      "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==",
+      "version": "0.5.15",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+      "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
       "dependencies": {
-        "tslib": "^2.4.0"
+        "tslib": "^2.8.0"
       }
     },
-    "node_modules/@swc/helpers/node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
-    },
     "node_modules/@swc/types": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz",
-      "integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==",
+      "version": "0.1.17",
+      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
+      "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
       "dev": true,
       "dependencies": {
         "@swc/counter": "^0.1.3"
       }
     },
     "node_modules/@types/babel__generator": {
-      "version": "7.6.7",
-      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz",
-      "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==",
+      "version": "7.6.8",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
+      "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
       "dev": true,
       "dependencies": {
         "@babel/types": "^7.0.0"
       }
     },
     "node_modules/@types/babel__traverse": {
-      "version": "7.20.4",
-      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz",
-      "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==",
+      "version": "7.20.6",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
+      "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
       "dev": true,
       "dependencies": {
         "@babel/types": "^7.20.7"
       }
     },
     "node_modules/@types/estree": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
-      "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
-      "dev": true
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
     },
     "node_modules/@types/events": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz",
-      "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
-      "license": "MIT"
+      "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g=="
     },
     "node_modules/@types/file-saver": {
       "version": "2.0.5",
       "dev": true
     },
     "node_modules/@types/hoist-non-react-statics": {
-      "version": "3.3.5",
-      "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
-      "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
+      "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==",
       "dependencies": {
         "@types/react": "*",
         "hoist-non-react-statics": "^3.3.0"
       }
     },
     "node_modules/@types/is-hotkey": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz",
-      "integrity": "sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ=="
+      "version": "0.1.10",
+      "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.10.tgz",
+      "integrity": "sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ=="
     },
     "node_modules/@types/json-schema": {
-      "version": "7.0.11",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
-      "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
       "dev": true
     },
     "node_modules/@types/json5": {
       "dev": true
     },
     "node_modules/@types/lodash": {
-      "version": "4.14.191",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
-      "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
+      "version": "4.17.14",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz",
+      "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A=="
     },
     "node_modules/@types/node": {
       "version": "18.11.18",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
-      "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
-      "dev": true
+      "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
     },
     "node_modules/@types/prismjs": {
       "version": "1.26.0",
       "dev": true
     },
     "node_modules/@types/prop-types": {
-      "version": "15.7.5",
-      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
-      "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+      "version": "15.7.14",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+      "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
     },
     "node_modules/@types/react": {
       "version": "18.2.39",
       "version": "1.20.2",
       "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
       "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/@types/retry": {
       "version": "0.12.0",
       }
     },
     "node_modules/@types/scheduler": {
-      "version": "0.16.2",
-      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
-      "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+      "version": "0.23.0",
+      "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz",
+      "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw=="
     },
     "node_modules/@types/semver": {
-      "version": "7.3.13",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
-      "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
+      "version": "7.5.8",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
       "dev": true
     },
     "node_modules/@types/trusted-types": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
       "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/@types/ua-parser-js": {
       "version": "0.7.36",
         }
       }
     },
-    "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/@typescript-eslint/parser": {
       "version": "5.46.1",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.46.1.tgz",
         }
       }
     },
-    "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/@typescript-eslint/utils": {
       "version": "5.46.1",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.1.tgz",
         "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
-    "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
-      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
-      "dev": true,
-      "dependencies": {
-        "esrecurse": "^4.3.0",
-        "estraverse": "^4.1.1"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
-    "node_modules/@typescript-eslint/utils/node_modules/estraverse": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
-      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
-      "dev": true,
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
-    "node_modules/@typescript-eslint/utils/node_modules/semver": {
-      "version": "7.3.8",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
-      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/@typescript-eslint/visitor-keys": {
       "version": "5.46.1",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz",
       }
     },
     "node_modules/@vanilla-extract/babel-plugin-debug-ids": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.1.tgz",
-      "integrity": "sha512-ynyKqsJiMzM1/yiIJ6QdqpWKlK4IMJJWREpPtaemZrE1xG1B4E/Nfa6YazuDWjDkCJC1tRIpEGnVs+pMIjUxyw==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.2.0.tgz",
+      "integrity": "sha512-z5nx2QBnOhvmlmBKeRX5sPVLz437wV30u+GJL+Hzj1rGiJYVNvgIIlzUpRNjVQ0MgAgiQIqIUbqPnmMc6HmDlQ==",
       "dependencies": {
-        "@babel/core": "^7.20.7"
+        "@babel/core": "^7.23.9"
       }
     },
     "node_modules/@vanilla-extract/css": {
         "outdent": "^0.8.0"
       }
     },
-    "node_modules/@vanilla-extract/css/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/@vanilla-extract/css/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+    "node_modules/@vanilla-extract/integration": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-6.5.0.tgz",
+      "integrity": "sha512-E2YcfO8vA+vs+ua+gpvy1HRqvgWbI+MTlUpxA8FvatOvybuNcWAY0CKwQ/Gpj7rswYKtC6C7+xw33emM6/ImdQ==",
       "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
+        "@babel/core": "^7.20.7",
+        "@babel/plugin-syntax-typescript": "^7.20.0",
+        "@vanilla-extract/babel-plugin-debug-ids": "^1.0.4",
+        "@vanilla-extract/css": "^1.14.0",
+        "esbuild": "npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0",
+        "eval": "0.1.8",
+        "find-up": "^5.0.0",
+        "javascript-stringify": "^2.0.1",
+        "lodash": "^4.17.21",
+        "mlly": "^1.4.2",
+        "outdent": "^0.8.0",
+        "vite": "^5.0.11",
+        "vite-node": "^1.2.0"
       }
     },
-    "node_modules/@vanilla-extract/css/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+    "node_modules/@vanilla-extract/integration/node_modules/@vanilla-extract/css": {
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.17.0.tgz",
+      "integrity": "sha512-W6FqVFDD+C71ZlKsuj0MxOXSvHb1tvQ9h/+79aYfi097wLsALrnnBzd0by8C///iurrpQ3S+SH74lXd7Lr9MvA==",
       "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/@vanilla-extract/css/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
-    },
-    "node_modules/@vanilla-extract/css/node_modules/deepmerge": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
-      "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
-      "engines": {
-        "node": ">=0.10.0"
+        "@emotion/hash": "^0.9.0",
+        "@vanilla-extract/private": "^1.0.6",
+        "css-what": "^6.1.0",
+        "cssesc": "^3.0.0",
+        "csstype": "^3.0.7",
+        "dedent": "^1.5.3",
+        "deep-object-diff": "^1.1.9",
+        "deepmerge": "^4.2.2",
+        "lru-cache": "^10.4.3",
+        "media-query-parser": "^2.0.2",
+        "modern-ahocorasick": "^1.0.0",
+        "picocolors": "^1.0.0"
       }
     },
-    "node_modules/@vanilla-extract/css/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+    "node_modules/@vanilla-extract/integration/node_modules/css-what": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
+      "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
       "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/@vanilla-extract/css/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dependencies": {
-        "has-flag": "^4.0.0"
+        "node": ">= 6"
       },
-      "engines": {
-        "node": ">=8"
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
       }
     },
-    "node_modules/@vanilla-extract/integration": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-6.0.2.tgz",
-      "integrity": "sha512-LwfXlh0THeNvVXdA3iWFYvJs1mvEP1PkfQD/7S6Purry7L8iDizDV/87FgWBJ79FnTmYIvMrc7BOQsUajNj9VQ==",
-      "dependencies": {
-        "@babel/core": "^7.20.7",
-        "@babel/plugin-syntax-typescript": "^7.20.0",
-        "@vanilla-extract/babel-plugin-debug-ids": "^1.0.1",
-        "@vanilla-extract/css": "^1.9.3",
-        "esbuild": "^0.16.3",
-        "eval": "0.1.6",
-        "find-up": "^5.0.0",
-        "javascript-stringify": "^2.0.1",
-        "lodash": "^4.17.21",
-        "outdent": "^0.8.0"
-      }
+    "node_modules/@vanilla-extract/integration/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
     },
     "node_modules/@vanilla-extract/private": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.3.tgz",
-      "integrity": "sha512-17kVyLq3ePTKOkveHxXuIJZtGYs+cSoev7BlP+Lf4916qfDhk/HBjvlYDe8egrea7LNPHKwSZJK/bzZC+Q6AwQ=="
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.6.tgz",
+      "integrity": "sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw=="
     },
     "node_modules/@vanilla-extract/recipes": {
       "version": "0.3.0",
       "optional": true
     },
     "node_modules/acorn": {
-      "version": "8.12.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
-      "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
-      "dev": true,
-      "license": "MIT",
+      "version": "8.14.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+      "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
       "bin": {
         "acorn": "bin/acorn"
       },
       }
     },
     "node_modules/ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "license": "MIT",
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
       "dependencies": {
-        "color-convert": "^1.9.0"
+        "color-convert": "^2.0.1"
       },
       "engines": {
-        "node": ">=4"
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
     "node_modules/anymatch": {
         "node": ">= 8"
       }
     },
+    "node_modules/anymatch/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
     "node_modules/aproba": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
       "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+      "deprecated": "This package is no longer supported.",
       "optional": true,
       "dependencies": {
         "delegates": "^1.0.0",
       }
     },
     "node_modules/array-buffer-byte-length": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
-      "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+      "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
-        "is-array-buffer": "^3.0.4"
+        "call-bound": "^1.0.3",
+        "is-array-buffer": "^3.0.5"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/array.prototype.flat": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
-      "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+      "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "es-shim-unscopables": "^1.0.0"
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-shim-unscopables": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/array.prototype.flatmap": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
-      "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+      "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "es-shim-unscopables": "^1.0.0"
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-shim-unscopables": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/array.prototype.tosorted": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz",
-      "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+      "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.20.4",
-        "es-shim-unscopables": "^1.0.0",
-        "get-intrinsic": "^1.1.3"
+        "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"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/arraybuffer.prototype.slice": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
-      "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+      "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
       "dev": true,
       "dependencies": {
         "array-buffer-byte-length": "^1.0.1",
-        "call-bind": "^1.0.5",
+        "call-bind": "^1.0.8",
         "define-properties": "^1.2.1",
-        "es-abstract": "^1.22.3",
-        "es-errors": "^1.2.1",
-        "get-intrinsic": "^1.2.3",
-        "is-array-buffer": "^3.0.4",
-        "is-shared-array-buffer": "^1.0.2"
+        "es-abstract": "^1.23.5",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "is-array-buffer": "^3.0.4"
       },
       "engines": {
         "node": ">= 0.4"
       "version": "3.2.6",
       "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
       "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/at-least-node": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
       "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
       "dev": true,
-      "license": "ISC",
       "engines": {
         "node": ">= 4.0.0"
       }
       }
     },
     "node_modules/axe-core": {
-      "version": "4.6.0",
-      "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.6.0.tgz",
-      "integrity": "sha512-L3ZNbXPTxMrl0+qTXAzn9FBRvk5XdO56K8CvcCKtlxv44Aw2w2NCclGuvCWxHPw1Riiq3ncP/sxFYj2nUqdoTw==",
+      "version": "4.10.2",
+      "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz",
+      "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==",
       "dev": true,
       "engines": {
         "node": ">=4"
       "dev": true
     },
     "node_modules/babel-plugin-polyfill-corejs2": {
-      "version": "0.4.11",
-      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz",
-      "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==",
+      "version": "0.4.12",
+      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz",
+      "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@babel/compat-data": "^7.22.6",
-        "@babel/helper-define-polyfill-provider": "^0.6.2",
+        "@babel/helper-define-polyfill-provider": "^0.6.3",
         "semver": "^6.3.1"
       },
       "peerDependencies": {
         "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
       }
     },
-    "node_modules/babel-plugin-polyfill-corejs3": {
-      "version": "0.10.6",
-      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
-      "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
+    "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/babel-plugin-polyfill-corejs3": {
+      "version": "0.10.6",
+      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
+      "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
+      "dev": true,
+      "dependencies": {
         "@babel/helper-define-polyfill-provider": "^0.6.2",
         "core-js-compat": "^3.38.0"
       },
       }
     },
     "node_modules/babel-plugin-polyfill-regenerator": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz",
-      "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==",
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz",
+      "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/helper-define-polyfill-provider": "^0.6.2"
+        "@babel/helper-define-polyfill-provider": "^0.6.3"
       },
       "peerDependencies": {
         "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
     "node_modules/base-x": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz",
-      "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==",
-      "license": "MIT"
+      "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ=="
     },
     "node_modules/base64-js": {
       "version": "1.5.1",
       ]
     },
     "node_modules/binary-extensions": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
       "dev": true,
       "engines": {
         "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/bind-event-listener": {
       }
     },
     "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dev": true,
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
       "integrity": "sha512-L7siI766UCH6+arP9yT5wpA5AFxnmGbKiGSsxEVACl1tE0pvDJeQvMmbY2UmJiuffrr0ZJ2+U6Om46wQBqh1Lw=="
     },
     "node_modules/browserslist": {
-      "version": "4.23.3",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
-      "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
+      "version": "4.24.4",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
+      "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
       "funding": [
         {
           "type": "opencollective",
           "url": "https://github.com/sponsors/ai"
         }
       ],
-      "license": "MIT",
       "dependencies": {
-        "caniuse-lite": "^1.0.30001646",
-        "electron-to-chromium": "^1.5.4",
-        "node-releases": "^2.0.18",
-        "update-browserslist-db": "^1.1.0"
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
       },
       "bin": {
         "browserslist": "cli.js"
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
       "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
-      "license": "MIT",
       "dependencies": {
         "base-x": "^5.0.0"
       }
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
       "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
-    "node_modules/builtin-modules": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
-      "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+    "node_modules/cac": {
+      "version": "6.7.14",
+      "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+      "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/call-bind": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
       "dev": true,
-      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
       "engines": {
-        "node": ">=6"
+        "node": ">= 0.4"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/call-bind": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
-      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
+      "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
       "dev": true,
       "dependencies": {
-        "es-define-property": "^1.0.0",
         "es-errors": "^1.3.0",
-        "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.4",
-        "set-function-length": "^1.2.1"
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
+      "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "get-intrinsic": "^1.2.6"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001658",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001658.tgz",
-      "integrity": "sha512-N2YVqWbJELVdrnsW5p+apoQyYt51aBMSsBZki1XZEfeBCexcM/sf4xiAHcXQBkuOwJBXtWF7aW1sYX6tKebPHw==",
+      "version": "1.0.30001692",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz",
+      "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==",
       "funding": [
         {
           "type": "opencollective",
           "type": "github",
           "url": "https://github.com/sponsors/ai"
         }
-      ],
-      "license": "CC-BY-4.0"
+      ]
     },
     "node_modules/canvas": {
       "version": "2.11.2",
       }
     },
     "node_modules/chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "license": "MIT",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
       "dependencies": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
       },
       "engines": {
-        "node": ">=4"
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
     "node_modules/chokidar": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
-      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
       "dev": true,
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://paulmillr.com/funding/"
-        }
-      ],
       "dependencies": {
         "anymatch": "~3.1.2",
         "braces": "~3.0.2",
       "engines": {
         "node": ">= 8.10.0"
       },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
       "optionalDependencies": {
         "fsevents": "~2.3.2"
       }
       }
     },
     "node_modules/clsx": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
-      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
       "engines": {
         "node": ">=6"
       }
     },
     "node_modules/color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "license": "MIT",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
       "dependencies": {
-        "color-name": "1.1.3"
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
       }
     },
     "node_modules/color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "license": "MIT"
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
     },
     "node_modules/color-support": {
       "version": "1.1.3",
       "version": "2.20.3",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/common-tags": {
       "version": "1.8.2",
       "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
       "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=4.0.0"
       }
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
       "devOptional": true
     },
+    "node_modules/confbox": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+      "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="
+    },
     "node_modules/confusing-browser-globals": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
       "optional": true
     },
     "node_modules/content-type": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
-      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
       "engines": {
         "node": ">= 0.6"
       }
       "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
     },
     "node_modules/core-js-compat": {
-      "version": "3.38.1",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz",
-      "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==",
+      "version": "3.40.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz",
+      "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "browserslist": "^4.23.3"
+        "browserslist": "^4.24.3"
       },
       "funding": {
         "type": "opencollective",
       }
     },
     "node_modules/core-js-pure": {
-      "version": "3.26.1",
-      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz",
-      "integrity": "sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ==",
+      "version": "3.40.0",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.40.0.tgz",
+      "integrity": "sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A==",
       "dev": true,
       "hasInstallScript": true,
       "funding": {
       }
     },
     "node_modules/cross-fetch": {
-      "version": "3.1.5",
-      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
-      "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
+      "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
       "dependencies": {
-        "node-fetch": "2.6.7"
+        "node-fetch": "^2.7.0"
       }
     },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
       "dev": true,
       "dependencies": {
         "path-key": "^3.1.0",
       "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
       "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
       }
     },
     "node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
     },
     "node_modules/damerau-levenshtein": {
       "version": "1.0.8",
       "dev": true
     },
     "node_modules/data-view-buffer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
-      "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+      "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bound": "^1.0.3",
         "es-errors": "^1.3.0",
-        "is-data-view": "^1.0.1"
+        "is-data-view": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/data-view-byte-length": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
-      "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+      "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bound": "^1.0.3",
         "es-errors": "^1.3.0",
-        "is-data-view": "^1.0.1"
+        "is-data-view": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/inspect-js"
       }
     },
     "node_modules/data-view-byte-offset": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
-      "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+      "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bound": "^1.0.2",
         "es-errors": "^1.3.0",
         "is-data-view": "^1.0.1"
       },
       "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
     },
     "node_modules/debug": {
-      "version": "4.3.7",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
-      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
-      "license": "MIT",
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
       "dependencies": {
         "ms": "^2.1.3"
       },
         }
       }
     },
+    "node_modules/decimal.js": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+      "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
+    },
     "node_modules/decompress-response": {
       "version": "4.2.1",
       "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
         "node": ">=8"
       }
     },
+    "node_modules/dedent": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
+      "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==",
+      "peerDependencies": {
+        "babel-plugin-macros": "^3.1.0"
+      },
+      "peerDependenciesMeta": {
+        "babel-plugin-macros": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
       "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA=="
     },
     "node_modules/deepmerge": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
-      "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
       "engines": {
         "node": ">=0.10.0"
       }
       "optional": true
     },
     "node_modules/detect-libc": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
-      "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+      "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
       "optional": true,
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/domutils": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
-      "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+      "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
       "dependencies": {
         "dom-serializer": "^2.0.0",
         "domelementtype": "^2.3.0",
         "url": "https://github.com/fb55/domutils?sponsor=1"
       }
     },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dev": true,
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/ejs": {
       "version": "3.1.10",
       "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
       "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
       "dev": true,
-      "license": "Apache-2.0",
       "dependencies": {
         "jake": "^10.8.5"
       },
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.16",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.16.tgz",
-      "integrity": "sha512-2gQpi2WYobXmz2q23FrOBYTLcI1O/P4heW3eqX+ldmPVDQELRqhiebV380EhlGG12NtnX1qbK/FHpN0ba+7bLA==",
-      "license": "ISC"
+      "version": "1.5.83",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.83.tgz",
+      "integrity": "sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ=="
     },
     "node_modules/emoji-regex": {
       "version": "9.2.2",
       }
     },
     "node_modules/es-abstract": {
-      "version": "1.23.3",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
-      "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==",
+      "version": "1.23.9",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
+      "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
       "dev": true,
       "dependencies": {
-        "array-buffer-byte-length": "^1.0.1",
-        "arraybuffer.prototype.slice": "^1.0.3",
+        "array-buffer-byte-length": "^1.0.2",
+        "arraybuffer.prototype.slice": "^1.0.4",
         "available-typed-arrays": "^1.0.7",
-        "call-bind": "^1.0.7",
-        "data-view-buffer": "^1.0.1",
-        "data-view-byte-length": "^1.0.1",
-        "data-view-byte-offset": "^1.0.0",
-        "es-define-property": "^1.0.0",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "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.0.0",
-        "es-set-tostringtag": "^2.0.3",
-        "es-to-primitive": "^1.2.1",
-        "function.prototype.name": "^1.1.6",
-        "get-intrinsic": "^1.2.4",
-        "get-symbol-description": "^1.0.2",
-        "globalthis": "^1.0.3",
-        "gopd": "^1.0.1",
+        "es-set-tostringtag": "^2.1.0",
+        "es-to-primitive": "^1.3.0",
+        "function.prototype.name": "^1.1.8",
+        "get-intrinsic": "^1.2.7",
+        "get-proto": "^1.0.0",
+        "get-symbol-description": "^1.1.0",
+        "globalthis": "^1.0.4",
+        "gopd": "^1.2.0",
         "has-property-descriptors": "^1.0.2",
-        "has-proto": "^1.0.3",
-        "has-symbols": "^1.0.3",
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
         "hasown": "^2.0.2",
-        "internal-slot": "^1.0.7",
-        "is-array-buffer": "^3.0.4",
+        "internal-slot": "^1.1.0",
+        "is-array-buffer": "^3.0.5",
         "is-callable": "^1.2.7",
-        "is-data-view": "^1.0.1",
-        "is-negative-zero": "^2.0.3",
-        "is-regex": "^1.1.4",
-        "is-shared-array-buffer": "^1.0.3",
-        "is-string": "^1.0.7",
-        "is-typed-array": "^1.1.13",
-        "is-weakref": "^1.0.2",
-        "object-inspect": "^1.13.1",
+        "is-data-view": "^1.0.2",
+        "is-regex": "^1.2.1",
+        "is-shared-array-buffer": "^1.0.4",
+        "is-string": "^1.1.1",
+        "is-typed-array": "^1.1.15",
+        "is-weakref": "^1.1.0",
+        "math-intrinsics": "^1.1.0",
+        "object-inspect": "^1.13.3",
         "object-keys": "^1.1.1",
-        "object.assign": "^4.1.5",
-        "regexp.prototype.flags": "^1.5.2",
-        "safe-array-concat": "^1.1.2",
-        "safe-regex-test": "^1.0.3",
-        "string.prototype.trim": "^1.2.9",
-        "string.prototype.trimend": "^1.0.8",
+        "object.assign": "^4.1.7",
+        "own-keys": "^1.0.1",
+        "regexp.prototype.flags": "^1.5.3",
+        "safe-array-concat": "^1.1.3",
+        "safe-push-apply": "^1.0.0",
+        "safe-regex-test": "^1.1.0",
+        "set-proto": "^1.0.0",
+        "string.prototype.trim": "^1.2.10",
+        "string.prototype.trimend": "^1.0.9",
         "string.prototype.trimstart": "^1.0.8",
-        "typed-array-buffer": "^1.0.2",
-        "typed-array-byte-length": "^1.0.1",
-        "typed-array-byte-offset": "^1.0.2",
-        "typed-array-length": "^1.0.6",
-        "unbox-primitive": "^1.0.2",
-        "which-typed-array": "^1.1.15"
+        "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.18"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/es-define-property": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
-      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
       "dev": true,
-      "dependencies": {
-        "get-intrinsic": "^1.2.4"
-      },
       "engines": {
         "node": ">= 0.4"
       }
       }
     },
     "node_modules/es-object-atoms": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
-      "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
       "dev": true,
       "dependencies": {
         "es-errors": "^1.3.0"
       }
     },
     "node_modules/es-set-tostringtag": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
-      "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.2.4",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
         "has-tostringtag": "^1.0.2",
-        "hasown": "^2.0.1"
+        "hasown": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/es-to-primitive": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
-      "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+      "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
       "dev": true,
       "dependencies": {
-        "is-callable": "^1.1.4",
-        "is-date-object": "^1.0.1",
-        "is-symbol": "^1.0.2"
+        "is-callable": "^1.2.7",
+        "is-date-object": "^1.0.5",
+        "is-symbol": "^1.0.4"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/esbuild": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.9.tgz",
-      "integrity": "sha512-gkH83yHyijMSZcZFs1IWew342eMdFuWXmQo3zkDPTre25LIPBJsXryg02M3u8OpTwCJdBkdaQwqKkDLnAsAeLQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
+      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
       "hasInstallScript": true,
       "bin": {
         "esbuild": "bin/esbuild"
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/android-arm": "0.16.9",
-        "@esbuild/android-arm64": "0.16.9",
-        "@esbuild/android-x64": "0.16.9",
-        "@esbuild/darwin-arm64": "0.16.9",
-        "@esbuild/darwin-x64": "0.16.9",
-        "@esbuild/freebsd-arm64": "0.16.9",
-        "@esbuild/freebsd-x64": "0.16.9",
-        "@esbuild/linux-arm": "0.16.9",
-        "@esbuild/linux-arm64": "0.16.9",
-        "@esbuild/linux-ia32": "0.16.9",
-        "@esbuild/linux-loong64": "0.16.9",
-        "@esbuild/linux-mips64el": "0.16.9",
-        "@esbuild/linux-ppc64": "0.16.9",
-        "@esbuild/linux-riscv64": "0.16.9",
-        "@esbuild/linux-s390x": "0.16.9",
-        "@esbuild/linux-x64": "0.16.9",
-        "@esbuild/netbsd-x64": "0.16.9",
-        "@esbuild/openbsd-x64": "0.16.9",
-        "@esbuild/sunos-x64": "0.16.9",
-        "@esbuild/win32-arm64": "0.16.9",
-        "@esbuild/win32-ia32": "0.16.9",
-        "@esbuild/win32-x64": "0.16.9"
+        "@esbuild/aix-ppc64": "0.19.12",
+        "@esbuild/android-arm": "0.19.12",
+        "@esbuild/android-arm64": "0.19.12",
+        "@esbuild/android-x64": "0.19.12",
+        "@esbuild/darwin-arm64": "0.19.12",
+        "@esbuild/darwin-x64": "0.19.12",
+        "@esbuild/freebsd-arm64": "0.19.12",
+        "@esbuild/freebsd-x64": "0.19.12",
+        "@esbuild/linux-arm": "0.19.12",
+        "@esbuild/linux-arm64": "0.19.12",
+        "@esbuild/linux-ia32": "0.19.12",
+        "@esbuild/linux-loong64": "0.19.12",
+        "@esbuild/linux-mips64el": "0.19.12",
+        "@esbuild/linux-ppc64": "0.19.12",
+        "@esbuild/linux-riscv64": "0.19.12",
+        "@esbuild/linux-s390x": "0.19.12",
+        "@esbuild/linux-x64": "0.19.12",
+        "@esbuild/netbsd-x64": "0.19.12",
+        "@esbuild/openbsd-x64": "0.19.12",
+        "@esbuild/sunos-x64": "0.19.12",
+        "@esbuild/win32-arm64": "0.19.12",
+        "@esbuild/win32-ia32": "0.19.12",
+        "@esbuild/win32-x64": "0.19.12"
       }
     },
     "node_modules/escalade": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
       "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
-      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
     },
     "node_modules/escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "license": "MIT",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
       "engines": {
-        "node": ">=0.8.0"
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/eslint": {
       "version": "8.29.0",
       "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz",
       "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==",
+      "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
       "dev": true,
       "dependencies": {
         "@eslint/eslintrc": "^1.3.3",
         "eslint-plugin-import": "^2.25.2"
       }
     },
+    "node_modules/eslint-config-airbnb-base/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/eslint-config-prettier": {
       "version": "8.5.0",
       "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
       }
     },
     "node_modules/eslint-module-utils": {
-      "version": "2.8.1",
-      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
-      "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
+      "version": "2.12.0",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
+      "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
       "dev": true,
       "dependencies": {
         "debug": "^3.2.7"
         "node": ">=0.10.0"
       }
     },
+    "node_modules/eslint-plugin-import/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/eslint-plugin-jsx-a11y": {
       "version": "6.6.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz",
         "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
       }
     },
+    "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/eslint-plugin-react": {
       "version": "7.31.11",
       "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.11.tgz",
       }
     },
     "node_modules/eslint-plugin-react/node_modules/resolve": {
-      "version": "2.0.0-next.4",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
-      "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==",
+      "version": "2.0.0-next.5",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+      "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
       "dev": true,
       "dependencies": {
-        "is-core-module": "^2.9.0",
+        "is-core-module": "^2.13.0",
         "path-parse": "^1.0.7",
         "supports-preserve-symlinks-flag": "^1.0.0"
       },
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/eslint-plugin-react/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/eslint-scope": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
-      "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
       "dev": true,
       "dependencies": {
         "esrecurse": "^4.3.0",
-        "estraverse": "^5.2.0"
+        "estraverse": "^4.1.1"
       },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/eslint-scope/node_modules/estraverse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "dev": true,
+      "engines": {
+        "node": ">=4.0"
       }
     },
     "node_modules/eslint-utils": {
       }
     },
     "node_modules/eslint-visitor-keys": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
-      "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/eslint/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+    "node_modules/eslint/node_modules/eslint-scope": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
       "dev": true,
       "dependencies": {
-        "color-convert": "^2.0.1"
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
       "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/eslint/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/eslint/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/eslint/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
-    },
-    "node_modules/eslint/node_modules/escape-string-regexp": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
-      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://opencollective.com/eslint"
       }
     },
     "node_modules/eslint/node_modules/globals": {
-      "version": "13.19.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
-      "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
+      "version": "13.24.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
       "dev": true,
       "dependencies": {
         "type-fest": "^0.20.2"
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/eslint/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+    "node_modules/eslint/node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
       "dev": true,
       "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/eslint/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^4.0.0"
+        "node": ">=10"
       },
-      "engines": {
-        "node": ">=8"
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/espree": {
-      "version": "9.4.1",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
-      "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
       "dev": true,
       "dependencies": {
-        "acorn": "^8.8.0",
+        "acorn": "^8.9.0",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^3.3.0"
+        "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
     "node_modules/esquery": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
-      "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
       "dev": true,
       "dependencies": {
         "estraverse": "^5.1.0"
       }
     },
     "node_modules/estree-walker": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
-      "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
-      "dev": true,
-      "license": "MIT"
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "dev": true
     },
     "node_modules/esutils": {
       "version": "2.0.3",
       }
     },
     "node_modules/eval": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.6.tgz",
-      "integrity": "sha512-o0XUw+5OGkXw4pJZzQoXUk+H87DHuC+7ZE//oSrRGtatTmr12oTnLfg6QOq9DyTt0c/p4TwzgmkKrBzWTSizyQ==",
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz",
+      "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==",
       "dependencies": {
+        "@types/node": "*",
         "require-like": ">= 0.1.1"
       },
       "engines": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
       "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
-      "license": "MIT",
       "engines": {
         "node": ">=0.8.x"
       }
       "dev": true
     },
     "node_modules/fast-glob": {
-      "version": "3.2.12",
-      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
-      "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
       "dev": true,
       "dependencies": {
         "@nodelib/fs.stat": "^2.0.2",
         "@nodelib/fs.walk": "^1.2.3",
         "glob-parent": "^5.1.2",
         "merge2": "^1.3.0",
-        "micromatch": "^4.0.4"
+        "micromatch": "^4.0.8"
       },
       "engines": {
         "node": ">=8.6.0"
       "dev": true
     },
     "node_modules/fast-uri": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
-      "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==",
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz",
+      "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==",
       "dev": true,
-      "license": "MIT"
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ]
     },
     "node_modules/fastq": {
-      "version": "1.14.0",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz",
-      "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==",
+      "version": "1.18.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
+      "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
       "dev": true,
       "dependencies": {
         "reusify": "^1.0.4"
       }
     },
     "node_modules/fbjs": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz",
-      "integrity": "sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==",
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
+      "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
       "dependencies": {
         "cross-fetch": "^3.1.5",
         "fbjs-css-vars": "^1.0.0",
         "object-assign": "^4.1.0",
         "promise": "^7.1.1",
         "setimmediate": "^1.0.5",
-        "ua-parser-js": "^0.7.30"
+        "ua-parser-js": "^1.0.35"
       }
     },
     "node_modules/fbjs-css-vars": {
       "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
       "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="
     },
-    "node_modules/fbjs/node_modules/ua-parser-js": {
-      "version": "0.7.35",
-      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
-      "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==",
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/ua-parser-js"
-        },
-        {
-          "type": "paypal",
-          "url": "https://paypal.me/faisalman"
-        }
-      ],
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/fdir": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz",
-      "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==",
+      "version": "6.4.3",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
+      "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
       "dev": true,
-      "license": "MIT",
       "peerDependencies": {
         "picomatch": "^3 || ^4"
       },
       "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
       "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
       "dev": true,
-      "license": "Apache-2.0",
       "dependencies": {
         "minimatch": "^5.0.1"
       }
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
       }
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
       "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
       "dev": true,
-      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
       }
     },
     "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dev": true,
       "dependencies": {
         "to-regex-range": "^5.0.1"
       }
     },
     "node_modules/flat-cache": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
-      "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+      "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
       "dev": true,
       "dependencies": {
-        "flatted": "^3.1.0",
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.3",
         "rimraf": "^3.0.2"
       },
       "engines": {
       }
     },
     "node_modules/flatted": {
-      "version": "3.2.7",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
-      "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
+      "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
       "dev": true
     },
     "node_modules/flux": {
       }
     },
     "node_modules/focus-trap": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.2.0.tgz",
-      "integrity": "sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==",
+      "version": "7.6.4",
+      "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz",
+      "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==",
       "dependencies": {
-        "tabbable": "^6.0.1"
+        "tabbable": "^6.2.0"
       }
     },
     "node_modules/focus-trap-react": {
       }
     },
     "node_modules/folds": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/folds/-/folds-2.0.0.tgz",
-      "integrity": "sha512-lKv31vij4GEpEzGKWk5c3ar78fMZ9Di5n1XFR14Z2wnnpqhiiM5JTIzr127Gk5dOfy4mJkjnv/ZfMZvM2k+OQg==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/folds/-/folds-2.1.0.tgz",
+      "integrity": "sha512-KwAG8bH3jsyZ9FKPMg+6ABV2YOcpp4nL0cCelsalnaPeRThkc5fgG1Xj5mhmdffYKjEXpEbERi5qmGbepgJryg==",
       "peerDependencies": {
         "@vanilla-extract/css": "^1.9.2",
         "@vanilla-extract/recipes": "^0.3.0",
         "react": ">=16.8.0"
       }
     },
-    "node_modules/formik/node_modules/tslib": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
-      "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
+    "node_modules/formik/node_modules/deepmerge": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
+      "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
     },
     "node_modules/fs-extra": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
-      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+      "version": "11.3.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
+      "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "at-least-node": "^1.0.0",
         "graceful-fs": "^4.2.0",
         "jsonfile": "^6.0.1",
         "universalify": "^2.0.0"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=14.14"
       }
     },
     "node_modules/fs-minipass": {
         "node": ">=8"
       }
     },
+    "node_modules/fs-minipass/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "optional": true
+    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
       "hasInstallScript": true,
       "optional": true,
       "os": [
       }
     },
     "node_modules/function.prototype.name": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
-      "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+      "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "functions-have-names": "^1.2.3"
+        "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"
       },
       "engines": {
         "node": ">= 0.4"
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
       "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+      "deprecated": "This package is no longer supported.",
       "optional": true,
       "dependencies": {
         "aproba": "^1.0.3 || ^2.0.0",
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
-      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
+      "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
       "dev": true,
       "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-define-property": "^1.0.1",
         "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
         "function-bind": "^1.1.2",
-        "has-proto": "^1.0.1",
-        "has-symbols": "^1.0.3",
-        "hasown": "^2.0.0"
+        "get-proto": "^1.0.0",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
       "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
+      "dev": true
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
       "dev": true,
-      "license": "ISC"
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
     },
     "node_modules/get-symbol-description": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
-      "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+      "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
+        "call-bound": "^1.0.3",
         "es-errors": "^1.3.0",
-        "get-intrinsic": "^1.2.4"
+        "get-intrinsic": "^1.2.6"
       },
       "engines": {
         "node": ">= 0.4"
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
       "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
       "devOptional": true,
       "dependencies": {
         "fs.realpath": "^1.0.0",
       "version": "11.12.0",
       "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
       "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
-      "license": "MIT",
       "engines": {
         "node": ">=4"
       }
       }
     },
     "node_modules/gopd": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
-      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
       "dev": true,
-      "dependencies": {
-        "get-intrinsic": "^1.1.3"
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/graceful-fs": {
-      "version": "4.2.10",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
-      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
       "dev": true
     },
     "node_modules/grapheme-splitter": {
       "dev": true
     },
     "node_modules/has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
+      "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
       "dev": true,
-      "dependencies": {
-        "function-bind": "^1.1.1"
-      },
       "engines": {
         "node": ">= 0.4.0"
       }
     },
     "node_modules/has-bigints": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
-      "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+      "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
       "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "license": "MIT",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
       "engines": {
-        "node": ">=4"
+        "node": ">=8"
       }
     },
     "node_modules/has-property-descriptors": {
       }
     },
     "node_modules/has-proto": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
-      "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+      "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
       "dev": true,
+      "dependencies": {
+        "dunder-proto": "^1.0.0"
+      },
       "engines": {
         "node": ">= 0.4"
       },
       }
     },
     "node_modules/has-symbols": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
-      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
       "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
-      "license": "MIT",
       "dependencies": {
         "void-elements": "3.1.0"
       }
       }
     },
     "node_modules/htmlparser2": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz",
-      "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==",
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+      "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
       "funding": [
         "https://github.com/fb55/htmlparser2?sponsor=1",
         {
       ],
       "dependencies": {
         "domelementtype": "^2.3.0",
-        "domhandler": "^5.0.2",
+        "domhandler": "^5.0.3",
         "domutils": "^3.0.1",
-        "entities": "^4.3.0"
+        "entities": "^4.4.0"
       }
     },
     "node_modules/https-proxy-agent": {
           "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
         }
       ],
-      "license": "MIT",
       "dependencies": {
         "@babel/runtime": "^7.23.2"
       }
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
       "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
-      "license": "MIT",
       "dependencies": {
         "@babel/runtime": "^7.23.2"
       }
       "version": "2.5.2",
       "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.5.2.tgz",
       "integrity": "sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw==",
-      "license": "MIT",
       "dependencies": {
         "cross-fetch": "4.0.0"
       }
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
       "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
-      "license": "MIT",
       "dependencies": {
         "node-fetch": "^2.6.12"
       }
     },
-    "node_modules/i18next-http-backend/node_modules/node-fetch": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
-      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
-      "license": "MIT",
-      "dependencies": {
-        "whatwg-url": "^5.0.0"
-      },
-      "engines": {
-        "node": "4.x || >=6.0.0"
-      },
-      "peerDependencies": {
-        "encoding": "^0.1.0"
-      },
-      "peerDependenciesMeta": {
-        "encoding": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/idb": {
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
       "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
-      "dev": true,
-      "license": "ISC"
+      "dev": true
     },
     "node_modules/ieee754": {
       "version": "1.2.1",
       ]
     },
     "node_modules/ignore": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz",
-      "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==",
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
       "dev": true,
       "engines": {
         "node": ">= 4"
       }
     },
     "node_modules/immutable": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
-      "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
+      "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
       "dev": true
     },
     "node_modules/import-fresh": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
       "devOptional": true,
       "dependencies": {
         "once": "^1.3.0",
       "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
     },
     "node_modules/internal-slot": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
-      "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+      "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
       "dev": true,
       "dependencies": {
         "es-errors": "^1.3.0",
-        "hasown": "^2.0.0",
-        "side-channel": "^1.0.4"
+        "hasown": "^2.0.2",
+        "side-channel": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/intl-messageformat": {
-      "version": "10.5.4",
-      "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.4.tgz",
-      "integrity": "sha512-z+hrFdiJ/heRYlzegrdFYqU1m/KOMOVMqNilIArj+PbsuU8TNE7v4TWdQgSoxlxbT4AcZH3Op3/Fu15QTp+W1w==",
+      "version": "10.7.12",
+      "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.12.tgz",
+      "integrity": "sha512-4HBsPDJ61jZwNikauvm0mcLvs1AfCBbihiqOX2AGs1MX7SA1H0SNKJRSWxpZpToGoNzvoYLsJJ2pURkbEDg+Dw==",
       "dependencies": {
-        "@formatjs/ecma402-abstract": "1.17.2",
-        "@formatjs/fast-memoize": "2.2.0",
-        "@formatjs/icu-messageformat-parser": "2.7.0",
-        "tslib": "^2.4.0"
+        "@formatjs/ecma402-abstract": "2.3.2",
+        "@formatjs/fast-memoize": "2.2.6",
+        "@formatjs/icu-messageformat-parser": "2.10.0",
+        "tslib": "2"
       }
     },
-    "node_modules/intl-messageformat/node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
-    },
     "node_modules/is-array-buffer": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
-      "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+      "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-async-function": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.0.tgz",
+      "integrity": "sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.1"
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.1",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/is-bigint": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
-      "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+      "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
       "dev": true,
       "dependencies": {
-        "has-bigints": "^1.0.1"
+        "has-bigints": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/is-boolean-object": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
-      "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz",
+      "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/is-builtin-module": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
-      "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "builtin-modules": "^3.3.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/is-callable": {
       "version": "1.2.7",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
       }
     },
     "node_modules/is-core-module": {
-      "version": "2.13.1",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
-      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
       "dev": true,
       "dependencies": {
-        "hasown": "^2.0.0"
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/is-data-view": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
-      "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+      "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
       "dev": true,
       "dependencies": {
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
         "is-typed-array": "^1.1.13"
       },
       "engines": {
       }
     },
     "node_modules/is-date-object": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
-      "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+      "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
       "dev": true,
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-finalizationregistry": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+      "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-fullwidth-code-point": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
         "node": ">=8"
       }
     },
-    "node_modules/is-glob": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+    "node_modules/is-generator-function": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
+      "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.0",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
       "dev": true,
       "dependencies": {
         "is-extglob": "^2.1.1"
       "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz",
       "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="
     },
-    "node_modules/is-module": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
-      "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/is-negative-zero": {
+    "node_modules/is-map": {
       "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
-      "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+      "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+      "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+      "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
+      "dev": true
+    },
     "node_modules/is-number": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
       }
     },
     "node_modules/is-number-object": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
-      "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+      "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
       "dev": true,
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
       "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
       }
     },
     "node_modules/is-regex": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
-      "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+      "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.2",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
       "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-set": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+      "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-shared-array-buffer": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
-      "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+      "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7"
+        "call-bound": "^1.0.3"
       },
       "engines": {
         "node": ">= 0.4"
       "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
       "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=8"
       },
       }
     },
     "node_modules/is-string": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
-      "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+      "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
       "dev": true,
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/is-symbol": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
-      "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+      "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
       "dev": true,
       "dependencies": {
-        "has-symbols": "^1.0.2"
+        "call-bound": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "safe-regex-test": "^1.1.0"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/is-typed-array": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
-      "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+      "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
       "dev": true,
       "dependencies": {
-        "which-typed-array": "^1.1.14"
+        "which-typed-array": "^1.1.16"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakmap": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+      "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+      "dev": true,
       "engines": {
         "node": ">= 0.4"
       },
       }
     },
     "node_modules/is-weakref": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
-      "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz",
+      "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakset": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+      "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2"
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
       "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
       "dev": true,
-      "license": "Apache-2.0",
       "dependencies": {
         "async": "^3.2.3",
         "chalk": "^4.0.2",
         "node": ">=10"
       }
     },
-    "node_modules/jake/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/jake/node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/jake/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/jake/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/jake/node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/jake/node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/javascript-stringify": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
       }
     },
     "node_modules/js-sdsl": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz",
-      "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==",
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz",
+      "integrity": "sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==",
       "dev": true,
       "funding": {
         "type": "opencollective",
       }
     },
     "node_modules/jsesc": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
-      "license": "MIT",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
       "bin": {
         "jsesc": "bin/jsesc"
       },
       "engines": {
-        "node": ">=4"
+        "node": ">=6"
       }
     },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true
+    },
     "node_modules/json-schema": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
       "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
-      "dev": true,
-      "license": "(AFL-2.1 OR BSD-3-Clause)"
+      "dev": true
     },
     "node_modules/json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
       "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "universalify": "^2.0.0"
       },
       "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
       "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/jsx-ast-utils": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz",
-      "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==",
+      "version": "3.3.5",
+      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+      "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
       "dev": true,
       "dependencies": {
-        "array-includes": "^3.1.5",
-        "object.assign": "^4.1.3"
+        "array-includes": "^3.1.6",
+        "array.prototype.flat": "^1.3.1",
+        "object.assign": "^4.1.4",
+        "object.values": "^1.1.6"
       },
       "engines": {
         "node": ">=4.0"
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
       "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
-      "license": "MIT",
       "engines": {
         "node": ">=18"
       }
     },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
     "node_modules/language-subtag-registry": {
-      "version": "0.3.22",
-      "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
-      "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+      "version": "0.3.23",
+      "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+      "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
       "dev": true
     },
     "node_modules/language-tags": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.6.tgz",
-      "integrity": "sha512-HNkaCgM8wZgE/BZACeotAAgpL9FUjEnhgF0FVQMIgH//zqTPreLYMb3rWYkYAqPoF75Jwuycp1da7uz66cfFQg==",
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+      "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
       "dev": true,
       "dependencies": {
         "language-subtag-registry": "^0.3.20"
+      },
+      "engines": {
+        "node": ">=0.10"
       }
     },
-    "node_modules/legacy-swc-helpers": {
-      "name": "@swc/helpers",
-      "version": "0.4.14",
-      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz",
-      "integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==",
-      "dependencies": {
-        "tslib": "^2.4.0"
-      }
-    },
-    "node_modules/legacy-swc-helpers/node_modules/tslib": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
-      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
-    },
     "node_modules/leven": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
       "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
       }
     },
     "node_modules/lilconfig": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz",
-      "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
       "engines": {
         "node": ">=10"
       }
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
       "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
       "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/loglevel": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
-      "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==",
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
+      "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
       "engines": {
         "node": ">= 0.6.0"
       },
       }
     },
     "node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "devOptional": true,
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
       "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
+        "yallist": "^3.0.2"
       }
     },
     "node_modules/magic-string": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/make-dir/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "optional": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/matrix-events-sdk": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz",
       "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA=="
     },
     "node_modules/matrix-js-sdk": {
-      "version": "34.11.1",
-      "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.11.1.tgz",
-      "integrity": "sha512-rDbIUIqEsN/pbHb6haBQmjxxgeb9G3Df2IhPPOotUbX6R1KseA8yJ6TAY0YySM2zVaBV3yZ6dnKWexF/uWvZfA==",
-      "license": "Apache-2.0",
+      "version": "35.0.0",
+      "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-35.0.0.tgz",
+      "integrity": "sha512-X8hIsd/8x1SC9vRr8DiNKQxmdrfRujtvEWPz8mY4FxVDJG8HEGDHvqUmaSy2jrtnOUn4oHzGQVLFO3DnhsSf8w==",
       "dependencies": {
         "@babel/runtime": "^7.12.5",
-        "@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
+        "@matrix-org/matrix-sdk-crypto-wasm": "^11.0.0",
         "@matrix-org/olm": "3.2.15",
         "another-json": "^0.2.0",
         "bs58": "^6.0.0",
         "jwt-decode": "^4.0.0",
         "loglevel": "^1.7.1",
         "matrix-events-sdk": "0.0.1",
-        "matrix-widget-api": "^1.8.2",
+        "matrix-widget-api": "^1.10.0",
         "oidc-client-ts": "^3.0.1",
         "p-retry": "4",
         "sdp-transform": "^2.14.1",
         "unhomoglyph": "^1.0.6",
-        "uuid": "10"
+        "uuid": "11"
       },
       "engines": {
         "node": ">=20.0.0"
       }
     },
     "node_modules/matrix-js-sdk/node_modules/uuid": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
-      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+      "version": "11.0.5",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
+      "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
       "funding": [
         "https://github.com/sponsors/broofa",
         "https://github.com/sponsors/ctavan"
       ],
-      "license": "MIT",
       "bin": {
-        "uuid": "dist/bin/uuid"
+        "uuid": "dist/esm/bin/uuid"
       }
     },
     "node_modules/matrix-widget-api": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.9.0.tgz",
-      "integrity": "sha512-au8mqralNDqrEvaVAkU37bXOb8I9SCe+ACdPk11QWw58FKstVq31q2wRz+qWA6J+42KJ6s1DggWbG/S3fEs3jw==",
-      "license": "Apache-2.0",
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.12.0.tgz",
+      "integrity": "sha512-6JRd9fJGGvuBRhcTg9wX+Skn/Q1wox3jdp5yYQKJ6pPw4urW9bkTR90APBKVDB1vorJKT44jml+lCzkDMRBjww==",
       "dependencies": {
         "@types/events": "^3.0.0",
         "events": "^3.2.0"
       }
     },
     "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
       "dependencies": {
-        "braces": "^3.0.2",
+        "braces": "^3.0.3",
         "picomatch": "^2.3.1"
       },
       "engines": {
         "node": ">=8.6"
       }
     },
+    "node_modules/micromatch/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
     "node_modules/millify": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/millify/-/millify-6.1.0.tgz",
         "node": ">=8"
       }
     },
+    "node_modules/minizlib/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "optional": true
+    },
     "node_modules/mkdirp": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
         "node": ">=10"
       }
     },
+    "node_modules/mlly": {
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
+      "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
+      "dependencies": {
+        "acorn": "^8.14.0",
+        "pathe": "^2.0.1",
+        "pkg-types": "^1.3.0",
+        "ufo": "^1.5.4"
+      }
+    },
+    "node_modules/modern-ahocorasick": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz",
+      "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ=="
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-      "license": "MIT"
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
     },
     "node_modules/nan": {
-      "version": "2.17.0",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
-      "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==",
+      "version": "2.22.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
+      "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
       "optional": true
     },
     "node_modules/nanoid": {
-      "version": "3.3.7",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
-      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "version": "3.3.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+      "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
       "funding": [
         {
           "type": "github",
       "dev": true
     },
     "node_modules/node-fetch": {
-      "version": "2.6.7",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
-      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
       "dependencies": {
         "whatwg-url": "^5.0.0"
       },
       }
     },
     "node_modules/node-releases": {
-      "version": "2.0.18",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
-      "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
-      "license": "MIT"
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="
     },
     "node_modules/nopt": {
       "version": "5.0.0",
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
       "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+      "deprecated": "This package is no longer supported.",
       "optional": true,
       "dependencies": {
         "are-we-there-yet": "^2.0.0",
       }
     },
     "node_modules/object-inspect": {
-      "version": "1.13.1",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
-      "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+      "version": "1.13.3",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+      "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
       "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
       }
     },
     "node_modules/object.assign": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
-      "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+      "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.5",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
         "define-properties": "^1.2.1",
-        "has-symbols": "^1.0.3",
+        "es-object-atoms": "^1.0.0",
+        "has-symbols": "^1.1.0",
         "object-keys": "^1.1.1"
       },
       "engines": {
       }
     },
     "node_modules/object.entries": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz",
-      "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==",
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
+      "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.20.4"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/object.hasown": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz",
-      "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz",
+      "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==",
       "dev": true,
       "dependencies": {
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.20.4"
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/object.values": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
-      "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+      "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
         "define-properties": "^1.2.1",
         "es-object-atoms": "^1.0.0"
       },
       }
     },
     "node_modules/oidc-client-ts": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz",
-      "integrity": "sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg==",
-      "license": "Apache-2.0",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.1.0.tgz",
+      "integrity": "sha512-IDopEXjiwjkmJLYZo6BTlvwOtnlSniWZkKZoXforC/oLZHC9wkIxd25Kwtmo5yKFMMVcsp3JY6bhcNJqdYk8+g==",
       "dependencies": {
         "jwt-decode": "^4.0.0"
       },
       }
     },
     "node_modules/optionator": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
-      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
       "dev": true,
       "dependencies": {
         "deep-is": "^0.1.3",
         "levn": "^0.4.1",
         "prelude-ls": "^1.2.1",
         "type-check": "^0.4.0",
-        "word-wrap": "^1.2.3"
+        "word-wrap": "^1.2.5"
       },
       "engines": {
         "node": ">= 0.8.0"
       "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz",
       "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A=="
     },
-    "node_modules/p-limit": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+    "node_modules/own-keys": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+      "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+      "dev": true,
       "dependencies": {
-        "yocto-queue": "^0.1.0"
+        "get-intrinsic": "^1.2.6",
+        "object-keys": "^1.1.1",
+        "safe-push-apply": "^1.0.0"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">= 0.4"
       },
       "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/p-locate": {
       }
     },
     "node_modules/path2d": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.0.tgz",
-      "integrity": "sha512-KdPAykQX6kmLSOO6Jpu2KNcCED7CKjmaBNGGNuctOsG0hgYO1OdYQaan6cYXJiG0WmXOwZZPILPBimu5QAIw3A==",
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.2.tgz",
+      "integrity": "sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==",
       "optional": true,
       "engines": {
         "node": ">=6"
       }
     },
+    "node_modules/pathe": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz",
+      "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w=="
+    },
     "node_modules/pdfjs-dist": {
       "version": "4.2.67",
       "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.2.67.tgz",
       }
     },
     "node_modules/picocolors": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
-      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
-      "license": "ISC"
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
     },
     "node_modules/picomatch": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
       "dev": true,
       "engines": {
-        "node": ">=8.6"
+        "node": ">=12"
       },
       "funding": {
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/pkg-types": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+      "dependencies": {
+        "confbox": "^0.1.8",
+        "mlly": "^1.7.4",
+        "pathe": "^2.0.1"
+      }
+    },
     "node_modules/possible-typed-array-names": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.32",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
-      "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
+      "version": "8.5.1",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
+      "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
       "funding": [
         {
           "type": "opencollective",
         }
       ],
       "dependencies": {
-        "nanoid": "^3.3.7",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.0.2"
+        "nanoid": "^3.3.8",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
       "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
       "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": "^14.13.1 || >=16.0.0"
       },
       }
     },
     "node_modules/punycode": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
       "dev": true,
       "engines": {
         "node": ">=6"
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "safe-buffer": "^5.1.0"
       }
       "version": "15.0.0",
       "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.0.tgz",
       "integrity": "sha512-2O3IgF4zivg57Q6p6i+ChDgJ371IDcEWbuWC6gvoh5NbkDMs0Q+O7RPr4v61+Se32E0V+LmtwePAeqWZW0bi6g==",
-      "license": "MIT",
       "dependencies": {
         "@babel/runtime": "^7.24.8",
         "html-parse-stringify": "^3.0.1"
       }
     },
     "node_modules/react-refresh": {
-      "version": "0.14.0",
-      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
-      "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
+      "version": "0.14.2",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+      "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
       "dev": true,
       "engines": {
         "node": ">=0.10.0"
         "node": ">=8.10.0"
       }
     },
+    "node_modules/readdirp/node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/reflect.getprototypeof": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+      "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+      "dev": true,
+      "dependencies": {
+        "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"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/regenerate": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
       "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/regenerate-unicode-properties": {
-      "version": "10.1.1",
-      "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz",
-      "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==",
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz",
+      "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "regenerate": "^1.4.2"
       },
     "node_modules/regenerator-runtime": {
       "version": "0.14.1",
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
-      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
-      "license": "MIT"
+      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
     },
     "node_modules/regenerator-transform": {
       "version": "0.15.2",
       "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
       "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@babel/runtime": "^7.8.4"
       }
     },
     "node_modules/regexp.prototype.flags": {
-      "version": "1.5.2",
-      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
-      "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+      "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bind": "^1.0.8",
         "define-properties": "^1.2.1",
         "es-errors": "^1.3.0",
-        "set-function-name": "^2.0.1"
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "set-function-name": "^2.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/regexpu-core": {
-      "version": "5.3.2",
-      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz",
-      "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==",
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz",
+      "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "@babel/regjsgen": "^0.8.0",
         "regenerate": "^1.4.2",
-        "regenerate-unicode-properties": "^10.1.0",
-        "regjsparser": "^0.9.1",
+        "regenerate-unicode-properties": "^10.2.0",
+        "regjsgen": "^0.8.0",
+        "regjsparser": "^0.12.0",
         "unicode-match-property-ecmascript": "^2.0.0",
         "unicode-match-property-value-ecmascript": "^2.1.0"
       },
         "node": ">=4"
       }
     },
+    "node_modules/regjsgen": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
+      "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
+      "dev": true
+    },
     "node_modules/regjsparser": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz",
-      "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==",
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
+      "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
       "dev": true,
-      "license": "BSD-2-Clause",
       "dependencies": {
-        "jsesc": "~0.5.0"
+        "jsesc": "~3.0.2"
       },
       "bin": {
         "regjsparser": "bin/parser"
       }
     },
     "node_modules/regjsparser/node_modules/jsesc": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
-      "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+      "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
       "dev": true,
       "bin": {
         "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
       }
     },
     "node_modules/require-directory": {
       "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
       "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
       }
     },
     "node_modules/resolve": {
-      "version": "1.22.8",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
-      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+      "version": "1.22.10",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
       "dev": true,
       "dependencies": {
-        "is-core-module": "^2.13.0",
+        "is-core-module": "^2.16.0",
         "path-parse": "^1.0.7",
         "supports-preserve-symlinks-flag": "^1.0.0"
       },
       "bin": {
         "resolve": "bin/resolve"
       },
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
       "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
       "devOptional": true,
       "dependencies": {
         "glob": "^7.1.3"
       }
     },
     "node_modules/rollup": {
-      "version": "4.8.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.8.0.tgz",
-      "integrity": "sha512-NpsklK2fach5CdI+PScmlE5R4Ao/FSWtF7LkoIrHDxPACY/xshNasPsbpG0VVHxUTbf74tJbVT4PrP8JsJ6ZDA==",
-      "dev": true,
+      "version": "4.30.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz",
+      "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==",
+      "dependencies": {
+        "@types/estree": "1.0.6"
+      },
       "bin": {
         "rollup": "dist/bin/rollup"
       },
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.8.0",
-        "@rollup/rollup-android-arm64": "4.8.0",
-        "@rollup/rollup-darwin-arm64": "4.8.0",
-        "@rollup/rollup-darwin-x64": "4.8.0",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.8.0",
-        "@rollup/rollup-linux-arm64-gnu": "4.8.0",
-        "@rollup/rollup-linux-arm64-musl": "4.8.0",
-        "@rollup/rollup-linux-riscv64-gnu": "4.8.0",
-        "@rollup/rollup-linux-x64-gnu": "4.8.0",
-        "@rollup/rollup-linux-x64-musl": "4.8.0",
-        "@rollup/rollup-win32-arm64-msvc": "4.8.0",
-        "@rollup/rollup-win32-ia32-msvc": "4.8.0",
-        "@rollup/rollup-win32-x64-msvc": "4.8.0",
+        "@rollup/rollup-android-arm-eabi": "4.30.1",
+        "@rollup/rollup-android-arm64": "4.30.1",
+        "@rollup/rollup-darwin-arm64": "4.30.1",
+        "@rollup/rollup-darwin-x64": "4.30.1",
+        "@rollup/rollup-freebsd-arm64": "4.30.1",
+        "@rollup/rollup-freebsd-x64": "4.30.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.30.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.30.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.30.1",
+        "@rollup/rollup-linux-arm64-musl": "4.30.1",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.30.1",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.30.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.30.1",
+        "@rollup/rollup-linux-x64-gnu": "4.30.1",
+        "@rollup/rollup-linux-x64-musl": "4.30.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.30.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.30.1",
+        "@rollup/rollup-win32-x64-msvc": "4.30.1",
         "fsevents": "~2.3.2"
       }
     },
       }
     },
     "node_modules/safe-array-concat": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
-      "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+      "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
-        "get-intrinsic": "^1.2.4",
-        "has-symbols": "^1.0.3",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "has-symbols": "^1.1.0",
         "isarray": "^2.0.5"
       },
       "engines": {
         }
       ]
     },
+    "node_modules/safe-push-apply": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+      "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/safe-regex-test": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
-      "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+      "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.6",
+        "call-bound": "^1.0.2",
         "es-errors": "^1.3.0",
-        "is-regex": "^1.1.4"
+        "is-regex": "^1.2.1"
       },
       "engines": {
         "node": ">= 0.4"
         "postcss": "^8.3.11"
       }
     },
-    "node_modules/sanitize-html/node_modules/deepmerge": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
-      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/sanitize-html/node_modules/escape-string-regexp": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
-      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/sass": {
       "version": "1.56.2",
       "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz",
       }
     },
     "node_modules/scheduler": {
-      "version": "0.23.0",
-      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
-      "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+      "version": "0.23.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
       "dependencies": {
         "loose-envify": "^1.1.0"
       }
       }
     },
     "node_modules/sdp-transform": {
-      "version": "2.14.1",
-      "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz",
-      "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==",
+      "version": "2.15.0",
+      "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz",
+      "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==",
       "bin": {
         "sdp-verify": "checker.js"
       }
     },
     "node_modules/semver": {
-      "version": "6.3.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "version": "7.6.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+      "devOptional": true,
       "bin": {
         "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
       }
     },
     "node_modules/serialize-javascript": {
       "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
       "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
       "dev": true,
-      "license": "BSD-3-Clause",
       "dependencies": {
         "randombytes": "^2.1.0"
       }
         "node": ">= 0.4"
       }
     },
+    "node_modules/set-proto": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+      "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+      "dev": true,
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/setimmediate": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
       }
     },
     "node_modules/side-channel": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
-      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
       "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/source-map": {
       "version": "0.8.0-beta.0",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
       "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==",
       "dev": true,
-      "license": "BSD-3-Clause",
       "dependencies": {
         "whatwg-url": "^7.0.0"
       },
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
-      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
       "engines": {
         "node": ">=0.10.0"
       }
       "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
       "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "buffer-from": "^1.0.0",
         "source-map": "^0.6.0"
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
       "dev": true,
-      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
       }
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
       "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "punycode": "^2.1.0"
       }
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
       "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
-      "dev": true,
-      "license": "BSD-2-Clause"
+      "dev": true
     },
     "node_modules/source-map/node_modules/whatwg-url": {
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
       "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "lodash.sortby": "^4.7.0",
         "tr46": "^1.0.1",
       "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
       "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
       "deprecated": "Please use @jridgewell/sourcemap-codec instead",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/string_decoder": {
       "version": "1.3.0",
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
     },
     "node_modules/string.prototype.matchall": {
-      "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
-      "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==",
+      "version": "4.0.12",
+      "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+      "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.20.4",
-        "get-intrinsic": "^1.1.3",
-        "has-symbols": "^1.0.3",
-        "internal-slot": "^1.0.3",
-        "regexp.prototype.flags": "^1.4.3",
-        "side-channel": "^1.0.4"
+        "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"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/string.prototype.trim": {
-      "version": "1.2.9",
-      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
-      "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
+      "version": "1.2.10",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+      "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "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.0",
-        "es-object-atoms": "^1.0.0"
+        "es-abstract": "^1.23.5",
+        "es-object-atoms": "^1.0.0",
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/string.prototype.trimend": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
-      "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+      "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
         "define-properties": "^1.2.1",
         "es-object-atoms": "^1.0.0"
       },
+      "engines": {
+        "node": ">= 0.4"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
       "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
       "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
       "dev": true,
-      "license": "BSD-2-Clause",
       "dependencies": {
         "get-own-enumerable-property-symbols": "^3.0.0",
         "is-obj": "^1.0.1",
       "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz",
       "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=10"
       }
       }
     },
     "node_modules/supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "license": "MIT",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
       "dependencies": {
-        "has-flag": "^3.0.0"
+        "has-flag": "^4.0.0"
       },
       "engines": {
-        "node": ">=4"
+        "node": ">=8"
       }
     },
     "node_modules/supports-preserve-symlinks-flag": {
       }
     },
     "node_modules/tabbable": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.0.1.tgz",
-      "integrity": "sha512-SYJSIgeyXW7EuX1ytdneO5e8jip42oHWg9xl/o3oTYhmXusZVgiA+VlPvjIN+kHii9v90AmzTZEBcsEvuAY+TA=="
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+      "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
     },
     "node_modules/tar": {
-      "version": "6.1.15",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz",
-      "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
       "optional": true,
       "dependencies": {
         "chownr": "^2.0.0",
         "node": ">=10"
       }
     },
+    "node_modules/tar/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "optional": true
+    },
     "node_modules/temp-dir": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
       "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
       "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz",
       "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "is-stream": "^2.0.0",
         "temp-dir": "^2.0.0",
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/tempy/node_modules/type-fest": {
-      "version": "0.16.0",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
-      "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==",
-      "dev": true,
-      "license": "(MIT OR CC0-1.0)",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/terser": {
-      "version": "5.31.6",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz",
-      "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==",
+      "version": "5.37.0",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
+      "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
       "dev": true,
-      "license": "BSD-2-Clause",
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
         "acorn": "^8.8.2",
       "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
     },
     "node_modules/tinyglobby": {
-      "version": "0.2.5",
-      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.5.tgz",
-      "integrity": "sha512-Dlqgt6h0QkoHttG53/WGADNh9QhcjCAIZMTERAVhdpmIBEejSuLI9ZmGKWzB7tweBjlk30+s/ofi4SLmBeTYhw==",
+      "version": "0.2.10",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz",
+      "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==",
       "dev": true,
-      "license": "ISC",
       "dependencies": {
-        "fdir": "^6.2.0",
+        "fdir": "^6.4.2",
         "picomatch": "^4.0.2"
       },
       "engines": {
         "node": ">=12.0.0"
       }
     },
-    "node_modules/tinyglobby/node_modules/picomatch": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
-      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
     "node_modules/tippy.js": {
       "version": "6.3.7",
       "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
         "@popperjs/core": "^2.9.0"
       }
     },
-    "node_modules/to-fast-properties": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       }
     },
     "node_modules/tslib": {
-      "version": "1.14.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
-      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
-      "dev": true
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
     },
     "node_modules/tsutils": {
       "version": "3.21.0",
         "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
       }
     },
+    "node_modules/tsutils/node_modules/tslib": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+      "dev": true
+    },
     "node_modules/type-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
       }
     },
     "node_modules/type-fest": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
-      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "version": "0.16.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
+      "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==",
       "dev": true,
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/typed-array-buffer": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
-      "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+      "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bound": "^1.0.3",
         "es-errors": "^1.3.0",
-        "is-typed-array": "^1.1.13"
+        "is-typed-array": "^1.1.14"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/typed-array-byte-length": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
-      "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+      "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
         "for-each": "^0.3.3",
-        "gopd": "^1.0.1",
-        "has-proto": "^1.0.3",
-        "is-typed-array": "^1.1.13"
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.14"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/typed-array-byte-offset": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
-      "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+      "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
       "dev": true,
       "dependencies": {
         "available-typed-arrays": "^1.0.7",
-        "call-bind": "^1.0.7",
+        "call-bind": "^1.0.8",
         "for-each": "^0.3.3",
-        "gopd": "^1.0.1",
-        "has-proto": "^1.0.3",
-        "is-typed-array": "^1.1.13"
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.15",
+        "reflect.getprototypeof": "^1.0.9"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/typed-array-length": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
-      "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+      "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
         "gopd": "^1.0.1",
-        "has-proto": "^1.0.3",
         "is-typed-array": "^1.1.13",
-        "possible-typed-array-names": "^1.0.0"
+        "possible-typed-array-names": "^1.0.0",
+        "reflect.getprototypeof": "^1.0.6"
       },
       "engines": {
         "node": ">= 0.4"
         "node": "*"
       }
     },
+    "node_modules/ufo": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
+      "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="
+    },
     "node_modules/unbox-primitive": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
-      "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+      "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
+        "call-bound": "^1.0.3",
         "has-bigints": "^1.0.2",
-        "has-symbols": "^1.0.3",
-        "which-boxed-primitive": "^1.0.2"
+        "has-symbols": "^1.1.0",
+        "which-boxed-primitive": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg=="
     },
     "node_modules/unicode-canonical-property-names-ecmascript": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
-      "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+      "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=4"
       }
       "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
       "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "unicode-canonical-property-names-ecmascript": "^2.0.0",
         "unicode-property-aliases-ecmascript": "^2.0.0"
       }
     },
     "node_modules/unicode-match-property-value-ecmascript": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz",
-      "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz",
+      "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=4"
       }
       "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
       "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=4"
       }
       "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
       "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "crypto-random-string": "^2.0.0"
       },
       "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
       "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">= 10.0.0"
       }
       "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
       "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=4",
         "yarn": "*"
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
-      "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
+      "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
       "funding": [
         {
           "type": "opencollective",
           "url": "https://github.com/sponsors/ai"
         }
       ],
-      "license": "MIT",
       "dependencies": {
-        "escalade": "^3.1.2",
-        "picocolors": "^1.0.1"
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
       "optional": true
     },
     "node_modules/uuid": {
-      "version": "9.0.1",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
-      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
       "dev": true,
       "funding": [
         "https://github.com/sponsors/broofa",
       "version": "5.0.13",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.13.tgz",
       "integrity": "sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==",
-      "dev": true,
       "dependencies": {
         "esbuild": "^0.19.3",
         "postcss": "^8.4.32",
         }
       }
     },
+    "node_modules/vite-node": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz",
+      "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
+      "dependencies": {
+        "cac": "^6.7.14",
+        "debug": "^4.3.4",
+        "pathe": "^1.1.1",
+        "picocolors": "^1.0.0",
+        "vite": "^5.0.0"
+      },
+      "bin": {
+        "vite-node": "vite-node.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/vite-node/node_modules/pathe": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+      "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="
+    },
     "node_modules/vite-plugin-pwa": {
       "version": "0.20.5",
       "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.20.5.tgz",
         "vite": "^5.0.0"
       }
     },
-    "node_modules/vite-plugin-static-copy/node_modules/fs-extra": {
-      "version": "11.1.0",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz",
-      "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==",
-      "dev": true,
-      "dependencies": {
-        "graceful-fs": "^4.2.0",
-        "jsonfile": "^6.0.1",
-        "universalify": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=14.14"
-      }
-    },
     "node_modules/vite-plugin-top-level-await": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.1.tgz",
-      "integrity": "sha512-hogbZ6yT7+AqBaV6lK9JRNvJDn4/IJvHLu6ET06arNfo0t2IsyCaon7el9Xa8OumH+ESuq//SDf8xscZFE0rWw==",
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.4.tgz",
+      "integrity": "sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==",
       "dev": true,
       "dependencies": {
         "@rollup/plugin-virtual": "^3.0.2",
-        "@swc/core": "^1.3.100",
-        "uuid": "^9.0.1"
+        "@swc/core": "^1.7.0",
+        "uuid": "^10.0.0"
       },
       "peerDependencies": {
         "vite": ">=2.8"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/android-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
-      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
+    "node_modules/void-elements": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+      "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
       "engines": {
-        "node": ">=12"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/android-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
-      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
+    "node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/android-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
-      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
-      "cpu": [
-        "x64"
-      ],
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">= 8"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
-      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
-      "cpu": [
-        "arm64"
-      ],
+    "node_modules/which-boxed-primitive": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+      "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
+      "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"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/darwin-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
-      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
-      "cpu": [
-        "x64"
-      ],
+    "node_modules/which-builtin-type": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+      "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
+      "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"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
-      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
-      "cpu": [
-        "arm64"
-      ],
+    "node_modules/which-collection": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+      "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
+      "dependencies": {
+        "is-map": "^2.0.3",
+        "is-set": "^2.0.3",
+        "is-weakmap": "^2.0.2",
+        "is-weakset": "^2.0.3"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
-      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
-      "cpu": [
-        "x64"
-      ],
+    "node_modules/which-typed-array": {
+      "version": "1.1.18",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz",
+      "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "for-each": "^0.3.3",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2"
+      },
       "engines": {
-        "node": ">=12"
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/linux-arm": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
-      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
+    "node_modules/wide-align": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+      "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
       "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
+      "dependencies": {
+        "string-width": "^1.0.2 || 2 || 3 || 4"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/linux-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
-      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
-      "cpu": [
-        "arm64"
-      ],
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
       "engines": {
-        "node": ">=12"
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/linux-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
-      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
-      "cpu": [
-        "ia32"
-      ],
+    "node_modules/workbox-background-sync": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz",
+      "integrity": "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==",
       "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-loong64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
-      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
-      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
-      "cpu": [
-        "mips64el"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
-      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
-      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-s390x": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
-      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
-      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
-      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/sunos-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
-      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/win32-arm64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
-      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/win32-ia32": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
-      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/win32-x64": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
-      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/esbuild": {
-      "version": "0.19.12",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
-      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
-      "dev": true,
-      "hasInstallScript": true,
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.19.12",
-        "@esbuild/android-arm": "0.19.12",
-        "@esbuild/android-arm64": "0.19.12",
-        "@esbuild/android-x64": "0.19.12",
-        "@esbuild/darwin-arm64": "0.19.12",
-        "@esbuild/darwin-x64": "0.19.12",
-        "@esbuild/freebsd-arm64": "0.19.12",
-        "@esbuild/freebsd-x64": "0.19.12",
-        "@esbuild/linux-arm": "0.19.12",
-        "@esbuild/linux-arm64": "0.19.12",
-        "@esbuild/linux-ia32": "0.19.12",
-        "@esbuild/linux-loong64": "0.19.12",
-        "@esbuild/linux-mips64el": "0.19.12",
-        "@esbuild/linux-ppc64": "0.19.12",
-        "@esbuild/linux-riscv64": "0.19.12",
-        "@esbuild/linux-s390x": "0.19.12",
-        "@esbuild/linux-x64": "0.19.12",
-        "@esbuild/netbsd-x64": "0.19.12",
-        "@esbuild/openbsd-x64": "0.19.12",
-        "@esbuild/sunos-x64": "0.19.12",
-        "@esbuild/win32-arm64": "0.19.12",
-        "@esbuild/win32-ia32": "0.19.12",
-        "@esbuild/win32-x64": "0.19.12"
-      }
-    },
-    "node_modules/void-elements": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
-      "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/warning": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
-      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
-      "dependencies": {
-        "loose-envify": "^1.0.0"
-      }
-    },
-    "node_modules/webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
-    },
-    "node_modules/whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
-      "dependencies": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
-    "node_modules/which": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-      "dev": true,
-      "dependencies": {
-        "isexe": "^2.0.0"
-      },
-      "bin": {
-        "node-which": "bin/node-which"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/which-boxed-primitive": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
-      "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
-      "dev": true,
-      "dependencies": {
-        "is-bigint": "^1.0.1",
-        "is-boolean-object": "^1.1.0",
-        "is-number-object": "^1.0.4",
-        "is-string": "^1.0.5",
-        "is-symbol": "^1.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/which-typed-array": {
-      "version": "1.1.15",
-      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
-      "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
-      "dev": true,
-      "dependencies": {
-        "available-typed-arrays": "^1.0.7",
-        "call-bind": "^1.0.7",
-        "for-each": "^0.3.3",
-        "gopd": "^1.0.1",
-        "has-tostringtag": "^1.0.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/wide-align": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
-      "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
-      "optional": true,
-      "dependencies": {
-        "string-width": "^1.0.2 || 2 || 3 || 4"
-      }
-    },
-    "node_modules/word-wrap": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/workbox-background-sync": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.1.0.tgz",
-      "integrity": "sha512-rMbgrzueVWDFcEq1610YyDW71z0oAXLfdRHRQcKw4SGihkfOK0JUEvqWHFwA6rJ+6TClnMIn7KQI5PNN1XQXwQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "idb": "^7.0.1",
-        "workbox-core": "7.1.0"
+      "dependencies": {
+        "idb": "^7.0.1",
+        "workbox-core": "7.3.0"
       }
     },
     "node_modules/workbox-broadcast-update": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.1.0.tgz",
-      "integrity": "sha512-O36hIfhjej/c5ar95pO67k1GQw0/bw5tKP7CERNgK+JdxBANQhDmIuOXZTNvwb2IHBx9hj2kxvcDyRIh5nzOgQ==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz",
+      "integrity": "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-core": "7.1.0"
+        "workbox-core": "7.3.0"
       }
     },
     "node_modules/workbox-build": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.1.1.tgz",
-      "integrity": "sha512-WdkVdC70VMpf5NBCtNbiwdSZeKVuhTEd5PV3mAwpTQCGAB5XbOny1P9egEgNdetv4srAMmMKjvBk4RD58LpooA==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.3.0.tgz",
+      "integrity": "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@apideck/better-ajv-errors": "^0.3.1",
         "@babel/core": "^7.24.4",
         "strip-comments": "^2.0.1",
         "tempy": "^0.6.0",
         "upath": "^1.2.0",
-        "workbox-background-sync": "7.1.0",
-        "workbox-broadcast-update": "7.1.0",
-        "workbox-cacheable-response": "7.1.0",
-        "workbox-core": "7.1.0",
-        "workbox-expiration": "7.1.0",
-        "workbox-google-analytics": "7.1.0",
-        "workbox-navigation-preload": "7.1.0",
-        "workbox-precaching": "7.1.0",
-        "workbox-range-requests": "7.1.0",
-        "workbox-recipes": "7.1.0",
-        "workbox-routing": "7.1.0",
-        "workbox-strategies": "7.1.0",
-        "workbox-streams": "7.1.0",
-        "workbox-sw": "7.1.0",
-        "workbox-window": "7.1.0"
+        "workbox-background-sync": "7.3.0",
+        "workbox-broadcast-update": "7.3.0",
+        "workbox-cacheable-response": "7.3.0",
+        "workbox-core": "7.3.0",
+        "workbox-expiration": "7.3.0",
+        "workbox-google-analytics": "7.3.0",
+        "workbox-navigation-preload": "7.3.0",
+        "workbox-precaching": "7.3.0",
+        "workbox-range-requests": "7.3.0",
+        "workbox-recipes": "7.3.0",
+        "workbox-routing": "7.3.0",
+        "workbox-strategies": "7.3.0",
+        "workbox-streams": "7.3.0",
+        "workbox-sw": "7.3.0",
+        "workbox-window": "7.3.0"
       },
       "engines": {
         "node": ">=16.0.0"
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
       "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "fast-deep-equal": "^3.1.3",
         "fast-uri": "^3.0.1",
         "url": "https://github.com/sponsors/epoberezkin"
       }
     },
+    "node_modules/workbox-build/node_modules/fs-extra": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+      "dev": true,
+      "dependencies": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/workbox-build/node_modules/json-schema-traverse": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/workbox-build/node_modules/pretty-bytes": {
       "version": "5.6.0",
       "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
       "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": ">=6"
       },
       }
     },
     "node_modules/workbox-build/node_modules/rollup": {
-      "version": "2.79.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
-      "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+      "version": "2.79.2",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
+      "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
       "dev": true,
-      "license": "MIT",
       "bin": {
         "rollup": "dist/bin/rollup"
       },
       }
     },
     "node_modules/workbox-cacheable-response": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.1.0.tgz",
-      "integrity": "sha512-iwsLBll8Hvua3xCuBB9h92+/e0wdsmSVgR2ZlvcfjepZWwhd3osumQB3x9o7flj+FehtWM2VHbZn8UJeBXXo6Q==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz",
+      "integrity": "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-core": "7.1.0"
+        "workbox-core": "7.3.0"
       }
     },
     "node_modules/workbox-core": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.1.0.tgz",
-      "integrity": "sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==",
-      "dev": true,
-      "license": "MIT"
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.3.0.tgz",
+      "integrity": "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==",
+      "dev": true
     },
     "node_modules/workbox-expiration": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.1.0.tgz",
-      "integrity": "sha512-m5DcMY+A63rJlPTbbBNtpJ20i3enkyOtSgYfv/l8h+D6YbbNiA0zKEkCUaMsdDlxggla1oOfRkyqTvl5Ni5KQQ==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.3.0.tgz",
+      "integrity": "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "idb": "^7.0.1",
-        "workbox-core": "7.1.0"
+        "workbox-core": "7.3.0"
       }
     },
     "node_modules/workbox-google-analytics": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.1.0.tgz",
-      "integrity": "sha512-FvE53kBQHfVTcZyczeBVRexhh7JTkyQ8HAvbVY6mXd2n2A7Oyz/9fIwnY406ZcDhvE4NFfKGjW56N4gBiqkrew==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz",
+      "integrity": "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-background-sync": "7.1.0",
-        "workbox-core": "7.1.0",
-        "workbox-routing": "7.1.0",
-        "workbox-strategies": "7.1.0"
+        "workbox-background-sync": "7.3.0",
+        "workbox-core": "7.3.0",
+        "workbox-routing": "7.3.0",
+        "workbox-strategies": "7.3.0"
       }
     },
     "node_modules/workbox-navigation-preload": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.1.0.tgz",
-      "integrity": "sha512-4wyAbo0vNI/X0uWNJhCMKxnPanNyhybsReMGN9QUpaePLTiDpKxPqFxl4oUmBNddPwIXug01eTSLVIFXimRG/A==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz",
+      "integrity": "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-core": "7.1.0"
+        "workbox-core": "7.3.0"
       }
     },
     "node_modules/workbox-precaching": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.1.0.tgz",
-      "integrity": "sha512-LyxzQts+UEpgtmfnolo0hHdNjoB7EoRWcF7EDslt+lQGd0lW4iTvvSe3v5JiIckQSB5KTW5xiCqjFviRKPj1zA==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.3.0.tgz",
+      "integrity": "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-core": "7.1.0",
-        "workbox-routing": "7.1.0",
-        "workbox-strategies": "7.1.0"
+        "workbox-core": "7.3.0",
+        "workbox-routing": "7.3.0",
+        "workbox-strategies": "7.3.0"
       }
     },
     "node_modules/workbox-range-requests": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.1.0.tgz",
-      "integrity": "sha512-m7+O4EHolNs5yb/79CrnwPR/g/PRzMFYEdo01LqwixVnc/sbzNSvKz0d04OE3aMRel1CwAAZQheRsqGDwATgPQ==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz",
+      "integrity": "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-core": "7.1.0"
+        "workbox-core": "7.3.0"
       }
     },
     "node_modules/workbox-recipes": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.1.0.tgz",
-      "integrity": "sha512-NRrk4ycFN9BHXJB6WrKiRX3W3w75YNrNrzSX9cEZgFB5ubeGoO8s/SDmOYVrFYp9HMw6sh1Pm3eAY/1gVS8YLg==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.3.0.tgz",
+      "integrity": "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-cacheable-response": "7.1.0",
-        "workbox-core": "7.1.0",
-        "workbox-expiration": "7.1.0",
-        "workbox-precaching": "7.1.0",
-        "workbox-routing": "7.1.0",
-        "workbox-strategies": "7.1.0"
+        "workbox-cacheable-response": "7.3.0",
+        "workbox-core": "7.3.0",
+        "workbox-expiration": "7.3.0",
+        "workbox-precaching": "7.3.0",
+        "workbox-routing": "7.3.0",
+        "workbox-strategies": "7.3.0"
       }
     },
     "node_modules/workbox-routing": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.1.0.tgz",
-      "integrity": "sha512-oOYk+kLriUY2QyHkIilxUlVcFqwduLJB7oRZIENbqPGeBP/3TWHYNNdmGNhz1dvKuw7aqvJ7CQxn27/jprlTdg==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.3.0.tgz",
+      "integrity": "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-core": "7.1.0"
+        "workbox-core": "7.3.0"
       }
     },
     "node_modules/workbox-strategies": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.1.0.tgz",
-      "integrity": "sha512-/UracPiGhUNehGjRm/tLUQ+9PtWmCbRufWtV0tNrALuf+HZ4F7cmObSEK+E4/Bx1p8Syx2tM+pkIrvtyetdlew==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.3.0.tgz",
+      "integrity": "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-core": "7.1.0"
+        "workbox-core": "7.3.0"
       }
     },
     "node_modules/workbox-streams": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.1.0.tgz",
-      "integrity": "sha512-WyHAVxRXBMfysM8ORwiZnI98wvGWTVAq/lOyBjf00pXFvG0mNaVz4Ji+u+fKa/mf1i2SnTfikoYKto4ihHeS6w==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.3.0.tgz",
+      "integrity": "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
-        "workbox-core": "7.1.0",
-        "workbox-routing": "7.1.0"
+        "workbox-core": "7.3.0",
+        "workbox-routing": "7.3.0"
       }
     },
     "node_modules/workbox-sw": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.1.0.tgz",
-      "integrity": "sha512-Hml/9+/njUXBglv3dtZ9WBKHI235AQJyLBV1G7EFmh4/mUdSQuXui80RtjDeVRrXnm/6QWgRUEHG3/YBVbxtsA==",
-      "dev": true,
-      "license": "MIT"
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.3.0.tgz",
+      "integrity": "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==",
+      "dev": true
     },
     "node_modules/workbox-window": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.1.0.tgz",
-      "integrity": "sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.3.0.tgz",
+      "integrity": "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
         "@types/trusted-types": "^2.0.2",
-        "workbox-core": "7.1.0"
+        "workbox-core": "7.3.0"
       }
     },
     "node_modules/wrap-ansi": {
         "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
       }
     },
-    "node_modules/wrap-ansi/node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/wrap-ansi/node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/wrap-ansi/node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
-    },
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       }
     },
     "node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "devOptional": true
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
     },
     "node_modules/yaml": {
       "version": "1.10.2",
index 1ecbc1900fab138411314d0c2018363d5a181310..1b3cb7a6bd150076c3755c4a9460389b3a7bd046 100644 (file)
@@ -44,7 +44,7 @@
     "file-saver": "2.0.5",
     "flux": "4.0.3",
     "focus-trap-react": "10.0.2",
-    "folds": "2.0.0",
+    "folds": "2.1.0",
     "formik": "2.4.6",
     "html-dom-parser": "4.0.0",
     "html-react-parser": "4.2.0",
@@ -56,7 +56,7 @@
     "jotai": "2.6.0",
     "linkify-react": "4.1.3",
     "linkifyjs": "4.1.3",
-    "matrix-js-sdk": "34.11.1",
+    "matrix-js-sdk": "35.0.0",
     "millify": "6.1.0",
     "pdfjs-dist": "4.2.67",
     "prismjs": "1.29.0",
     "vite": "5.0.13",
     "vite-plugin-pwa": "0.20.5",
     "vite-plugin-static-copy": "1.0.4",
-    "vite-plugin-top-level-await": "1.4.1"
+    "vite-plugin-top-level-await": "1.4.4"
   }
 }
diff --git a/src/app/components/ActionUIA.tsx b/src/app/components/ActionUIA.tsx
new file mode 100644 (file)
index 0000000..b9cd122
--- /dev/null
@@ -0,0 +1,73 @@
+import React, { ReactNode } from 'react';
+import { AuthDict, AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk';
+import { getUIAFlowForStages } from '../utils/matrix-uia';
+import { useSupportedUIAFlows, useUIACompleted, useUIAFlow } from '../hooks/useUIAFlows';
+import { UIAFlowOverlay } from './UIAFlowOverlay';
+import { PasswordStage, SSOStage } from './uia-stages';
+import { useMatrixClient } from '../hooks/useMatrixClient';
+
+export const SUPPORTED_IN_APP_UIA_STAGES = [AuthType.Password, AuthType.Sso];
+
+export const pickUIAFlow = (uiaFlows: UIAFlow[]): UIAFlow | undefined => {
+  const passwordFlow = getUIAFlowForStages(uiaFlows, [AuthType.Password]);
+  if (passwordFlow) return passwordFlow;
+  return getUIAFlowForStages(uiaFlows, [AuthType.Sso]);
+};
+
+type ActionUIAProps = {
+  authData: IAuthData;
+  ongoingFlow: UIAFlow;
+  action: (authDict: AuthDict) => void;
+  onCancel: () => void;
+};
+export function ActionUIA({ authData, ongoingFlow, action, onCancel }: ActionUIAProps) {
+  const mx = useMatrixClient();
+  const completed = useUIACompleted(authData);
+  const { getStageToComplete } = useUIAFlow(authData, ongoingFlow);
+
+  const stageToComplete = getStageToComplete();
+
+  if (!stageToComplete) return null;
+  return (
+    <UIAFlowOverlay
+      currentStep={completed.length + 1}
+      stepCount={ongoingFlow.stages.length}
+      onCancel={onCancel}
+    >
+      {stageToComplete.type === AuthType.Password && (
+        <PasswordStage
+          userId={mx.getUserId()!}
+          stageData={stageToComplete}
+          onCancel={onCancel}
+          submitAuthDict={action}
+        />
+      )}
+      {stageToComplete.type === AuthType.Sso && stageToComplete.session && (
+        <SSOStage
+          ssoRedirectURL={mx.getFallbackAuthUrl(AuthType.Sso, stageToComplete.session)}
+          stageData={stageToComplete}
+          onCancel={onCancel}
+          submitAuthDict={action}
+        />
+      )}
+    </UIAFlowOverlay>
+  );
+}
+
+type ActionUIAFlowsLoaderProps = {
+  authData: IAuthData;
+  unsupported: () => ReactNode;
+  children: (ongoingFlow: UIAFlow) => ReactNode;
+};
+export function ActionUIAFlowsLoader({
+  authData,
+  unsupported,
+  children,
+}: ActionUIAFlowsLoaderProps) {
+  const supportedFlows = useSupportedUIAFlows(authData.flows ?? [], SUPPORTED_IN_APP_UIA_STAGES);
+  const ongoingFlow = supportedFlows.length > 0 ? supportedFlows[0] : undefined;
+
+  if (!ongoingFlow) return unsupported();
+
+  return children(ongoingFlow);
+}
diff --git a/src/app/components/BackupRestore.tsx b/src/app/components/BackupRestore.tsx
new file mode 100644 (file)
index 0000000..068f437
--- /dev/null
@@ -0,0 +1,281 @@
+import React, { MouseEventHandler, useCallback, useState } from 'react';
+import { useAtom } from 'jotai';
+import { CryptoApi, KeyBackupInfo } from 'matrix-js-sdk/lib/crypto-api';
+import {
+  Badge,
+  Box,
+  Button,
+  color,
+  config,
+  Icon,
+  IconButton,
+  Icons,
+  Menu,
+  percent,
+  PopOut,
+  ProgressBar,
+  RectCords,
+  Spinner,
+  Text,
+} from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { BackupProgressStatus, backupRestoreProgressAtom } from '../state/backupRestore';
+import { InfoCard } from './info-card';
+import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
+import {
+  useKeyBackupInfo,
+  useKeyBackupStatus,
+  useKeyBackupSync,
+  useKeyBackupTrust,
+} from '../hooks/useKeyBackup';
+import { stopPropagation } from '../utils/keyboard';
+import { useRestoreBackupOnVerification } from '../hooks/useRestoreBackupOnVerification';
+
+type BackupStatusProps = {
+  enabled: boolean;
+};
+function BackupStatus({ enabled }: BackupStatusProps) {
+  return (
+    <Box as="span" gap="100" alignItems="Center">
+      <Badge variant={enabled ? 'Success' : 'Critical'} fill="Solid" size="200" radii="Pill" />
+      <Text
+        as="span"
+        size="L400"
+        style={{ color: enabled ? color.Success.Main : color.Critical.Main }}
+      >
+        {enabled ? 'Connected' : 'Disconnected'}
+      </Text>
+    </Box>
+  );
+}
+type BackupSyncingProps = {
+  count: number;
+};
+function BackupSyncing({ count }: BackupSyncingProps) {
+  return (
+    <Box as="span" gap="100" alignItems="Center">
+      <Spinner size="50" variant="Primary" fill="Soft" />
+      <Text as="span" size="L400" style={{ color: color.Primary.Main }}>
+        Syncing ({count})
+      </Text>
+    </Box>
+  );
+}
+
+function BackupProgressFetching() {
+  return (
+    <Box grow="Yes" gap="200" alignItems="Center">
+      <Badge variant="Secondary" fill="Solid" radii="300">
+        <Text size="L400">Restoring: 0%</Text>
+      </Badge>
+      <Box grow="Yes" direction="Column">
+        <ProgressBar variant="Secondary" size="300" min={0} max={1} value={0} />
+      </Box>
+      <Spinner size="50" variant="Secondary" fill="Soft" />
+    </Box>
+  );
+}
+
+type BackupProgressProps = {
+  total: number;
+  downloaded: number;
+};
+function BackupProgress({ total, downloaded }: BackupProgressProps) {
+  return (
+    <Box grow="Yes" gap="200" alignItems="Center">
+      <Badge variant="Secondary" fill="Solid" radii="300">
+        <Text size="L400">Restoring: {`${Math.round(percent(0, total, downloaded))}%`}</Text>
+      </Badge>
+      <Box grow="Yes" direction="Column">
+        <ProgressBar variant="Secondary" size="300" min={0} max={total} value={downloaded} />
+      </Box>
+      <Badge variant="Secondary" fill="Soft" radii="Pill">
+        <Text size="L400">
+          {downloaded} / {total}
+        </Text>
+      </Badge>
+    </Box>
+  );
+}
+
+type BackupTrustInfoProps = {
+  crypto: CryptoApi;
+  backupInfo: KeyBackupInfo;
+};
+function BackupTrustInfo({ crypto, backupInfo }: BackupTrustInfoProps) {
+  const trust = useKeyBackupTrust(crypto, backupInfo);
+
+  if (!trust) return null;
+
+  return (
+    <Box direction="Column">
+      {trust.matchesDecryptionKey ? (
+        <Text size="T200" style={{ color: color.Success.Main }}>
+          <b>Backup has trusted decryption key.</b>
+        </Text>
+      ) : (
+        <Text size="T200" style={{ color: color.Critical.Main }}>
+          <b>Backup does not have trusted decryption key!</b>
+        </Text>
+      )}
+      {trust.trusted ? (
+        <Text size="T200" style={{ color: color.Success.Main }}>
+          <b>Backup has trusted by signature.</b>
+        </Text>
+      ) : (
+        <Text size="T200" style={{ color: color.Critical.Main }}>
+          <b>Backup does not have trusted signature!</b>
+        </Text>
+      )}
+    </Box>
+  );
+}
+
+type BackupRestoreTileProps = {
+  crypto: CryptoApi;
+};
+export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
+  const [restoreProgress, setRestoreProgress] = useAtom(backupRestoreProgressAtom);
+  const restoring =
+    restoreProgress.status === BackupProgressStatus.Fetching ||
+    restoreProgress.status === BackupProgressStatus.Loading;
+
+  const backupEnabled = useKeyBackupStatus(crypto);
+  const backupInfo = useKeyBackupInfo(crypto);
+  const [remainingSession, syncFailure] = useKeyBackupSync();
+
+  const [menuCords, setMenuCords] = useState<RectCords>();
+
+  const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setMenuCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const [restoreState, restoreBackup] = useAsyncCallback<void, Error, []>(
+    useCallback(async () => {
+      await crypto.restoreKeyBackup({
+        progressCallback(progress) {
+          setRestoreProgress(progress);
+        },
+      });
+    }, [crypto, setRestoreProgress])
+  );
+
+  const handleRestore = () => {
+    setMenuCords(undefined);
+    restoreBackup();
+  };
+
+  return (
+    <InfoCard
+      variant="Surface"
+      title="Encryption Backup"
+      after={
+        <Box alignItems="Center" gap="200">
+          {remainingSession === 0 ? (
+            <BackupStatus enabled={backupEnabled} />
+          ) : (
+            <BackupSyncing count={remainingSession} />
+          )}
+          <IconButton
+            aria-pressed={!!menuCords}
+            size="300"
+            variant="Surface"
+            radii="300"
+            onClick={handleMenu}
+          >
+            <Icon size="100" src={Icons.VerticalDots} />
+          </IconButton>
+          <PopOut
+            anchor={menuCords}
+            offset={5}
+            position="Bottom"
+            align="End"
+            content={
+              <FocusTrap
+                focusTrapOptions={{
+                  initialFocus: false,
+                  onDeactivate: () => setMenuCords(undefined),
+                  clickOutsideDeactivates: true,
+                  isKeyForward: (evt: KeyboardEvent) =>
+                    evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+                  isKeyBackward: (evt: KeyboardEvent) =>
+                    evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+                  escapeDeactivates: stopPropagation,
+                }}
+              >
+                <Menu
+                  style={{
+                    padding: config.space.S100,
+                  }}
+                >
+                  <Box direction="Column" gap="100">
+                    <Box direction="Column" gap="200">
+                      <InfoCard
+                        variant="SurfaceVariant"
+                        title="Backup Details"
+                        description={
+                          <>
+                            <span>Version: {backupInfo?.version ?? 'NIL'}</span>
+                            <br />
+                            <span>Keys: {backupInfo?.count ?? 'NIL'}</span>
+                          </>
+                        }
+                      />
+                    </Box>
+                    <Button
+                      size="300"
+                      variant="Success"
+                      radii="300"
+                      aria-disabled={restoreState.status === AsyncStatus.Loading || restoring}
+                      onClick={
+                        restoreState.status === AsyncStatus.Loading || restoring
+                          ? undefined
+                          : handleRestore
+                      }
+                      before={<Icon size="100" src={Icons.Download} />}
+                    >
+                      <Text size="B300">Restore Backup</Text>
+                    </Button>
+                  </Box>
+                </Menu>
+              </FocusTrap>
+            }
+          />
+        </Box>
+      }
+    >
+      {syncFailure && (
+        <Text size="T200" style={{ color: color.Critical.Main }}>
+          <b>{syncFailure}</b>
+        </Text>
+      )}
+      {!backupEnabled && backupInfo === null && (
+        <Text size="T200" style={{ color: color.Critical.Main }}>
+          <b>No backup present on server!</b>
+        </Text>
+      )}
+      {!syncFailure && !backupEnabled && backupInfo && (
+        <BackupTrustInfo crypto={crypto} backupInfo={backupInfo} />
+      )}
+      {restoreState.status === AsyncStatus.Loading && !restoring && <BackupProgressFetching />}
+      {restoreProgress.status === BackupProgressStatus.Fetching && <BackupProgressFetching />}
+      {restoreProgress.status === BackupProgressStatus.Loading && (
+        <BackupProgress
+          total={restoreProgress.data.total}
+          downloaded={restoreProgress.data.downloaded}
+        />
+      )}
+      {restoreState.status === AsyncStatus.Error && (
+        <Text size="T200" style={{ color: color.Critical.Main }}>
+          <b>{restoreState.error.message}</b>
+        </Text>
+      )}
+    </InfoCard>
+  );
+}
+
+export function AutoRestoreBackupOnVerification() {
+  useRestoreBackupOnVerification();
+
+  return null;
+}
index 338e5280501815262a145843c46c13f8873a17b7..574d0ca79ce41602486148ddb93c289d7f73ef2c 100644 (file)
@@ -19,7 +19,7 @@ export function CapabilitiesAndMediaConfigLoader({
     []
   >(
     useCallback(async () => {
-      const result = await Promise.allSettled([mx.getCapabilities(true), mx.getMediaConfig()]);
+      const result = await Promise.allSettled([mx.getCapabilities(), mx.getMediaConfig()]);
       const capabilities = promiseFulfilledResult(result[0]);
       const mediaConfig = promiseFulfilledResult(result[1]);
       return [capabilities, mediaConfig];
index dad59ec84ff1931c8a2384a501b953e027ead840..667191e905a5e5c74468a4072fae6e319ef61b6b 100644 (file)
@@ -9,7 +9,7 @@ type CapabilitiesLoaderProps = {
 export function CapabilitiesLoader({ children }: CapabilitiesLoaderProps) {
   const mx = useMatrixClient();
 
-  const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(true), [mx]));
+  const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(), [mx]));
 
   useEffect(() => {
     load();
diff --git a/src/app/components/DeviceVerification.tsx b/src/app/components/DeviceVerification.tsx
new file mode 100644 (file)
index 0000000..d2502cc
--- /dev/null
@@ -0,0 +1,318 @@
+import {
+  ShowSasCallbacks,
+  VerificationPhase,
+  VerificationRequest,
+  Verifier,
+} from 'matrix-js-sdk/lib/crypto-api';
+import React, { CSSProperties, useCallback, useEffect, useState } from 'react';
+import { VerificationMethod } from 'matrix-js-sdk/lib/types';
+import {
+  Box,
+  Button,
+  config,
+  Dialog,
+  Header,
+  Icon,
+  IconButton,
+  Icons,
+  Overlay,
+  OverlayBackdrop,
+  OverlayCenter,
+  Spinner,
+  Text,
+} from 'folds';
+import FocusTrap from 'focus-trap-react';
+import {
+  useVerificationRequestPhase,
+  useVerificationRequestReceived,
+  useVerifierCancel,
+  useVerifierShowSas,
+} from '../hooks/useVerificationRequest';
+import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
+import { ContainerColor } from '../styles/ContainerColor.css';
+
+const DialogHeaderStyles: CSSProperties = {
+  padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
+  borderBottomWidth: config.borderWidth.B300,
+};
+
+type WaitingMessageProps = {
+  message: string;
+};
+function WaitingMessage({ message }: WaitingMessageProps) {
+  return (
+    <Box alignItems="Center" gap="200">
+      <Spinner variant="Secondary" size="200" />
+      <Text size="T300">{message}</Text>
+    </Box>
+  );
+}
+
+type VerificationUnexpectedProps = { message: string; onClose: () => void };
+function VerificationUnexpected({ message, onClose }: VerificationUnexpectedProps) {
+  return (
+    <Box direction="Column" gap="400">
+      <Text>{message}</Text>
+      <Button variant="Secondary" fill="Soft" onClick={onClose}>
+        <Text size="B400">Close</Text>
+      </Button>
+    </Box>
+  );
+}
+
+function VerificationWaitAccept() {
+  return (
+    <Box direction="Column" gap="400">
+      <Text>Please accept the request from other device.</Text>
+      <WaitingMessage message="Waiting for request to be accepted..." />
+    </Box>
+  );
+}
+
+type VerificationAcceptProps = {
+  onAccept: () => Promise<void>;
+};
+function VerificationAccept({ onAccept }: VerificationAcceptProps) {
+  const [acceptState, accept] = useAsyncCallback(onAccept);
+
+  const accepting = acceptState.status === AsyncStatus.Loading;
+  return (
+    <Box direction="Column" gap="400">
+      <Text>Click accept to start the verification process.</Text>
+      <Button
+        variant="Primary"
+        fill="Solid"
+        onClick={accept}
+        before={accepting && <Spinner size="100" variant="Primary" fill="Solid" />}
+        disabled={accepting}
+      >
+        <Text size="B400">Accept</Text>
+      </Button>
+    </Box>
+  );
+}
+
+function VerificationWaitStart() {
+  return (
+    <Box direction="Column" gap="400">
+      <Text>Verification request has been accepted.</Text>
+      <WaitingMessage message="Waiting for the response from other device..." />
+    </Box>
+  );
+}
+
+type VerificationStartProps = {
+  onStart: () => Promise<void>;
+};
+function AutoVerificationStart({ onStart }: VerificationStartProps) {
+  useEffect(() => {
+    onStart();
+  }, [onStart]);
+
+  return (
+    <Box direction="Column" gap="400">
+      <WaitingMessage message="Starting verification using emoji comparison..." />
+    </Box>
+  );
+}
+
+function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
+  const [confirmState, confirm] = useAsyncCallback(useCallback(() => sasData.confirm(), [sasData]));
+
+  const confirming =
+    confirmState.status === AsyncStatus.Loading || confirmState.status === AsyncStatus.Success;
+
+  return (
+    <Box direction="Column" gap="400">
+      <Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
+      <Box
+        className={ContainerColor({ variant: 'SurfaceVariant' })}
+        style={{
+          borderRadius: config.radii.R400,
+          padding: config.space.S500,
+        }}
+        gap="700"
+        wrap="Wrap"
+        justifyContent="Center"
+      >
+        {sasData.sas.emoji?.map(([emoji, name], index) => (
+          <Box
+            // eslint-disable-next-line react/no-array-index-key
+            key={`${emoji}${name}${index}`}
+            direction="Column"
+            gap="100"
+            justifyContent="Center"
+            alignItems="Center"
+          >
+            <Text size="H1">{emoji}</Text>
+            <Text size="T200">{name}</Text>
+          </Box>
+        ))}
+      </Box>
+      <Box direction="Column" gap="200">
+        <Button
+          variant="Primary"
+          fill="Soft"
+          onClick={confirm}
+          disabled={confirming}
+          before={confirming && <Spinner size="100" variant="Primary" />}
+        >
+          <Text size="B400">They Match</Text>
+        </Button>
+        <Button
+          variant="Primary"
+          fill="Soft"
+          onClick={() => sasData.mismatch()}
+          disabled={confirming}
+        >
+          <Text size="B400">Do not Match</Text>
+        </Button>
+      </Box>
+    </Box>
+  );
+}
+
+type SasVerificationProps = {
+  verifier: Verifier;
+  onCancel: () => void;
+};
+function SasVerification({ verifier, onCancel }: SasVerificationProps) {
+  const [sasData, setSasData] = useState<ShowSasCallbacks>();
+
+  useVerifierShowSas(verifier, setSasData);
+  useVerifierCancel(verifier, onCancel);
+
+  useEffect(() => {
+    verifier.verify();
+  }, [verifier]);
+
+  if (sasData) {
+    return <CompareEmoji sasData={sasData} />;
+  }
+
+  return (
+    <Box direction="Column" gap="400">
+      <WaitingMessage message="Starting verification using emoji comparison..." />
+    </Box>
+  );
+}
+
+type VerificationDoneProps = {
+  onExit: () => void;
+};
+function VerificationDone({ onExit }: VerificationDoneProps) {
+  return (
+    <Box direction="Column" gap="400">
+      <div>
+        <Text>Your device is verified.</Text>
+      </div>
+      <Button variant="Primary" fill="Solid" onClick={onExit}>
+        <Text size="B400">Okay</Text>
+      </Button>
+    </Box>
+  );
+}
+
+type VerificationCanceledProps = {
+  onClose: () => void;
+};
+function VerificationCanceled({ onClose }: VerificationCanceledProps) {
+  return (
+    <Box direction="Column" gap="400">
+      <Text>Verification has been canceled.</Text>
+      <Button variant="Secondary" fill="Soft" onClick={onClose}>
+        <Text size="B400">Close</Text>
+      </Button>
+    </Box>
+  );
+}
+
+type DeviceVerificationProps = {
+  request: VerificationRequest;
+  onExit: () => void;
+};
+export function DeviceVerification({ request, onExit }: DeviceVerificationProps) {
+  const phase = useVerificationRequestPhase(request);
+
+  const handleCancel = useCallback(() => {
+    if (request.phase !== VerificationPhase.Done && request.phase !== VerificationPhase.Cancelled) {
+      request.cancel();
+    }
+    onExit();
+  }, [request, onExit]);
+
+  const handleAccept = useCallback(() => request.accept(), [request]);
+  const handleStart = useCallback(async () => {
+    await request.startVerification(VerificationMethod.Sas);
+  }, [request]);
+
+  return (
+    <Overlay open backdrop={<OverlayBackdrop />}>
+      <OverlayCenter>
+        <FocusTrap
+          focusTrapOptions={{
+            initialFocus: false,
+            clickOutsideDeactivates: false,
+            escapeDeactivates: false,
+          }}
+        >
+          <Dialog variant="Surface">
+            <Header style={DialogHeaderStyles} variant="Surface" size="500">
+              <Box grow="Yes">
+                <Text size="H4">Device Verification</Text>
+              </Box>
+              <IconButton size="300" radii="300" onClick={handleCancel}>
+                <Icon src={Icons.Cross} />
+              </IconButton>
+            </Header>
+            <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+              {phase === VerificationPhase.Requested &&
+                (request.initiatedByMe ? (
+                  <VerificationWaitAccept />
+                ) : (
+                  <VerificationAccept onAccept={handleAccept} />
+                ))}
+              {phase === VerificationPhase.Ready &&
+                (request.initiatedByMe ? (
+                  <AutoVerificationStart onStart={handleStart} />
+                ) : (
+                  <VerificationWaitStart />
+                ))}
+              {phase === VerificationPhase.Started &&
+                (request.verifier ? (
+                  <SasVerification verifier={request.verifier} onCancel={handleCancel} />
+                ) : (
+                  <VerificationUnexpected
+                    message="Unexpected Error! Verification is started but verifier is missing."
+                    onClose={handleCancel}
+                  />
+                ))}
+              {phase === VerificationPhase.Done && <VerificationDone onExit={onExit} />}
+              {phase === VerificationPhase.Cancelled && (
+                <VerificationCanceled onClose={handleCancel} />
+              )}
+            </Box>
+          </Dialog>
+        </FocusTrap>
+      </OverlayCenter>
+    </Overlay>
+  );
+}
+
+export function ReceiveSelfDeviceVerification() {
+  const [request, setRequest] = useState<VerificationRequest>();
+
+  useVerificationRequestReceived(setRequest);
+
+  const handleExit = useCallback(() => {
+    setRequest(undefined);
+  }, []);
+
+  if (!request) return null;
+
+  if (!request.isSelfVerification) {
+    return null;
+  }
+
+  return <DeviceVerification request={request} onExit={handleExit} />;
+}
diff --git a/src/app/components/DeviceVerificationSetup.tsx b/src/app/components/DeviceVerificationSetup.tsx
new file mode 100644 (file)
index 0000000..fb09c15
--- /dev/null
@@ -0,0 +1,375 @@
+import React, { FormEventHandler, forwardRef, useCallback, useState } from 'react';
+import {
+  Dialog,
+  Header,
+  Box,
+  Text,
+  IconButton,
+  Icon,
+  Icons,
+  config,
+  Button,
+  Chip,
+  color,
+  Spinner,
+} from 'folds';
+import FileSaver from 'file-saver';
+import to from 'await-to-js';
+import { AuthDict, IAuthData, MatrixError, UIAuthCallback } from 'matrix-js-sdk';
+import { PasswordInput } from './password-input';
+import { ContainerColor } from '../styles/ContainerColor.css';
+import { copyToClipboard } from '../utils/dom';
+import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
+import { clearSecretStorageKeys } from '../../client/state/secretStorageKeys';
+import { ActionUIA, ActionUIAFlowsLoader } from './ActionUIA';
+import { useMatrixClient } from '../hooks/useMatrixClient';
+import { useAlive } from '../hooks/useAlive';
+import { UseStateProvider } from './UseStateProvider';
+
+type UIACallback<T> = (
+  authDict: AuthDict | null
+) => Promise<[IAuthData, undefined] | [undefined, T]>;
+
+type PerformAction<T> = (authDict: AuthDict | null) => Promise<T>;
+
+type UIAAction<T> = {
+  authData: IAuthData;
+  callback: UIACallback<T>;
+  cancelCallback: () => void;
+};
+
+function makeUIAAction<T>(
+  authData: IAuthData,
+  performAction: PerformAction<T>,
+  resolve: (data: T) => void,
+  reject: (error?: any) => void
+): UIAAction<T> {
+  const action: UIAAction<T> = {
+    authData,
+    callback: async (authDict) => {
+      const [error, data] = await to<T, MatrixError | Error>(performAction(authDict));
+
+      if (error instanceof MatrixError && error.httpStatus === 401) {
+        return [error.data as IAuthData, undefined];
+      }
+
+      if (error) {
+        reject(error);
+        throw error;
+      }
+
+      resolve(data);
+      return [undefined, data];
+    },
+    cancelCallback: reject,
+  };
+
+  return action;
+}
+
+type SetupVerificationProps = {
+  onComplete: (recoveryKey: string) => void;
+};
+function SetupVerification({ onComplete }: SetupVerificationProps) {
+  const mx = useMatrixClient();
+  const alive = useAlive();
+
+  const [uiaAction, setUIAAction] = useState<UIAAction<void>>();
+  const [nextAuthData, setNextAuthData] = useState<IAuthData | null>(); // null means no next action.
+
+  const handleAction = useCallback(
+    async (authDict: AuthDict) => {
+      if (!uiaAction) {
+        throw new Error('Unexpected Error! UIA action is perform without data.');
+      }
+      if (alive()) {
+        setNextAuthData(null);
+      }
+      const [authData] = await uiaAction.callback(authDict);
+
+      if (alive() && authData) {
+        setNextAuthData(authData);
+      }
+    },
+    [uiaAction, alive]
+  );
+
+  const resetUIA = useCallback(() => {
+    if (!alive()) return;
+    setUIAAction(undefined);
+    setNextAuthData(undefined);
+  }, [alive]);
+
+  const authUploadDeviceSigningKeys: UIAuthCallback<void> = useCallback(
+    (makeRequest) =>
+      new Promise<void>((resolve, reject) => {
+        makeRequest(null)
+          .then(() => {
+            resolve();
+            resetUIA();
+          })
+          .catch((error) => {
+            if (error instanceof MatrixError && error.httpStatus === 401) {
+              const authData = error.data as IAuthData;
+              const action = makeUIAAction(
+                authData,
+                makeRequest as PerformAction<void>,
+                resolve,
+                (err) => {
+                  resetUIA();
+                  reject(err);
+                }
+              );
+              if (alive()) {
+                setUIAAction(action);
+              } else {
+                reject(new Error('Authentication failed! Failed to setup device verification.'));
+              }
+              return;
+            }
+            reject(error);
+          });
+      }),
+    [alive, resetUIA]
+  );
+
+  const [setupState, setup] = useAsyncCallback<void, Error, [string | undefined]>(
+    useCallback(
+      async (passphrase) => {
+        const crypto = mx.getCrypto();
+        if (!crypto) throw new Error('Unexpected Error! Crypto module not found!');
+
+        const recoveryKeyData = await crypto.createRecoveryKeyFromPassphrase(passphrase);
+        if (!recoveryKeyData.encodedPrivateKey) {
+          throw new Error('Unexpected Error! Failed to create recovery key.');
+        }
+        clearSecretStorageKeys();
+
+        await crypto.bootstrapSecretStorage({
+          createSecretStorageKey: async () => recoveryKeyData,
+          setupNewSecretStorage: true,
+        });
+
+        await crypto.bootstrapCrossSigning({
+          authUploadDeviceSigningKeys,
+          setupNewCrossSigning: true,
+        });
+
+        await crypto.resetKeyBackup();
+
+        onComplete(recoveryKeyData.encodedPrivateKey);
+      },
+      [mx, onComplete, authUploadDeviceSigningKeys]
+    )
+  );
+
+  const loading = setupState.status === AsyncStatus.Loading;
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+    if (loading) return;
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const passphraseInput = target?.passphraseInput as HTMLInputElement | undefined;
+    let passphrase: string | undefined;
+    if (passphraseInput && passphraseInput.value.length > 0) {
+      passphrase = passphraseInput.value;
+    }
+
+    setup(passphrase);
+  };
+
+  return (
+    <Box as="form" onSubmit={handleSubmit} direction="Column" gap="400">
+      <Text size="T300">
+        Generate a <b>Recovery Key</b> for verifying identity if you do not have access to other
+        devices. Additionally, setup a passphrase as a memorable alternative.
+      </Text>
+      <Box direction="Column" gap="100">
+        <Text size="L400">Passphrase (Optional)</Text>
+        <PasswordInput name="passphraseInput" size="400" readOnly={loading} />
+      </Box>
+      <Button
+        type="submit"
+        disabled={loading}
+        before={loading && <Spinner size="200" variant="Primary" fill="Solid" />}
+      >
+        <Text size="B400">Continue</Text>
+      </Button>
+      {setupState.status === AsyncStatus.Error && (
+        <Text size="T200" style={{ color: color.Critical.Main }}>
+          <b>{setupState.error ? setupState.error.message : 'Unexpected Error!'}</b>
+        </Text>
+      )}
+      {nextAuthData !== null && uiaAction && (
+        <ActionUIAFlowsLoader
+          authData={nextAuthData ?? uiaAction.authData}
+          unsupported={() => (
+            <Text size="T200">
+              Authentication steps to perform this action are not supported by client.
+            </Text>
+          )}
+        >
+          {(ongoingFlow) => (
+            <ActionUIA
+              authData={nextAuthData ?? uiaAction.authData}
+              ongoingFlow={ongoingFlow}
+              action={handleAction}
+              onCancel={uiaAction.cancelCallback}
+            />
+          )}
+        </ActionUIAFlowsLoader>
+      )}
+    </Box>
+  );
+}
+
+type RecoveryKeyDisplayProps = {
+  recoveryKey: string;
+};
+function RecoveryKeyDisplay({ recoveryKey }: RecoveryKeyDisplayProps) {
+  const [show, setShow] = useState(false);
+
+  const handleCopy = () => {
+    copyToClipboard(recoveryKey);
+  };
+
+  const handleDownload = () => {
+    const blob = new Blob([recoveryKey], {
+      type: 'text/plain;charset=us-ascii',
+    });
+    FileSaver.saveAs(blob, 'recovery-key.txt');
+  };
+
+  const safeToDisplayKey = show ? recoveryKey : recoveryKey.replace(/[^\s]/g, '*');
+
+  return (
+    <Box direction="Column" gap="400">
+      <Text size="T300">
+        Store the Recovery Key in a safe place for future use, as you will need it to verify your
+        identity if you do not have access to other devices.
+      </Text>
+      <Box direction="Column" gap="100">
+        <Text size="L400">Recovery Key</Text>
+        <Box
+          className={ContainerColor({ variant: 'SurfaceVariant' })}
+          style={{
+            padding: config.space.S300,
+            borderRadius: config.radii.R400,
+          }}
+          alignItems="Center"
+          justifyContent="Center"
+          gap="400"
+        >
+          <Text style={{ fontFamily: 'monospace' }} size="T200" priority="300">
+            {safeToDisplayKey}
+          </Text>
+          <Chip onClick={() => setShow(!show)} variant="Secondary" radii="Pill">
+            <Text size="B300">{show ? 'Hide' : 'Show'}</Text>
+          </Chip>
+        </Box>
+      </Box>
+      <Box direction="Column" gap="200">
+        <Button onClick={handleCopy}>
+          <Text size="B400">Copy</Text>
+        </Button>
+        <Button onClick={handleDownload} fill="Soft">
+          <Text size="B400">Download</Text>
+        </Button>
+      </Box>
+    </Box>
+  );
+}
+
+type DeviceVerificationSetupProps = {
+  onCancel: () => void;
+};
+export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerificationSetupProps>(
+  ({ onCancel }, ref) => {
+    const [recoveryKey, setRecoveryKey] = useState<string>();
+
+    return (
+      <Dialog ref={ref}>
+        <Header
+          style={{
+            padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
+            borderBottomWidth: config.borderWidth.B300,
+          }}
+          variant="Surface"
+          size="500"
+        >
+          <Box grow="Yes">
+            <Text size="H4">Setup Device Verification</Text>
+          </Box>
+          <IconButton size="300" radii="300" onClick={onCancel}>
+            <Icon src={Icons.Cross} />
+          </IconButton>
+        </Header>
+        <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+          {recoveryKey ? (
+            <RecoveryKeyDisplay recoveryKey={recoveryKey} />
+          ) : (
+            <SetupVerification onComplete={setRecoveryKey} />
+          )}
+        </Box>
+      </Dialog>
+    );
+  }
+);
+type DeviceVerificationResetProps = {
+  onCancel: () => void;
+};
+export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerificationResetProps>(
+  ({ onCancel }, ref) => {
+    const [reset, setReset] = useState(false);
+
+    return (
+      <Dialog ref={ref}>
+        <Header
+          style={{
+            padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
+            borderBottomWidth: config.borderWidth.B300,
+          }}
+          variant="Surface"
+          size="500"
+        >
+          <Box grow="Yes">
+            <Text size="H4">Reset Device Verification</Text>
+          </Box>
+          <IconButton size="300" radii="300" onClick={onCancel}>
+            <Icon src={Icons.Cross} />
+          </IconButton>
+        </Header>
+        {reset ? (
+          <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+            <UseStateProvider initial={undefined}>
+              {(recoveryKey: string | undefined, setRecoveryKey) =>
+                recoveryKey ? (
+                  <RecoveryKeyDisplay recoveryKey={recoveryKey} />
+                ) : (
+                  <SetupVerification onComplete={setRecoveryKey} />
+                )
+              }
+            </UseStateProvider>
+          </Box>
+        ) : (
+          <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+            <Box direction="Column" gap="200">
+              <Text size="H1">✋🧑‍🚒🤚</Text>
+              <Text size="T300">Resetting device verification is permanent.</Text>
+              <Text size="T300">
+                Anyone you have verified with will see security alerts and your encryption backup
+                will be lost. You almost certainly do not want to do this, unless you have lost{' '}
+                <b>Recovery Key</b> or <b>Recovery Passphrase</b> and every device you can verify
+                from.
+              </Text>
+            </Box>
+            <Button variant="Critical" onClick={() => setReset(true)}>
+              <Text size="B400">Reset</Text>
+            </Button>
+          </Box>
+        )}
+      </Dialog>
+    );
+  }
+);
diff --git a/src/app/components/DeviceVerificationStatus.ts b/src/app/components/DeviceVerificationStatus.ts
new file mode 100644 (file)
index 0000000..e8a6b0c
--- /dev/null
@@ -0,0 +1,24 @@
+import { ReactNode } from 'react';
+import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api';
+import {
+  useDeviceVerificationStatus,
+  VerificationStatus,
+} from '../hooks/useDeviceVerificationStatus';
+
+type DeviceVerificationStatusProps = {
+  crypto?: CryptoApi;
+  userId: string;
+  deviceId: string;
+  children: (verificationStatus: VerificationStatus) => ReactNode;
+};
+
+export function DeviceVerificationStatus({
+  crypto,
+  userId,
+  deviceId,
+  children,
+}: DeviceVerificationStatusProps) {
+  const status = useDeviceVerificationStatus(crypto, userId, deviceId);
+
+  return children(status);
+}
diff --git a/src/app/components/LogoutDialog.tsx b/src/app/components/LogoutDialog.tsx
new file mode 100644 (file)
index 0000000..cc9bf06
--- /dev/null
@@ -0,0 +1,89 @@
+import React, { forwardRef, useCallback } from 'react';
+import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds';
+import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
+import { logoutClient } from '../../client/initMatrix';
+import { useMatrixClient } from '../hooks/useMatrixClient';
+import { useCrossSigningActive } from '../hooks/useCrossSigning';
+import { InfoCard } from './info-card';
+import {
+  useDeviceVerificationStatus,
+  VerificationStatus,
+} from '../hooks/useDeviceVerificationStatus';
+
+type LogoutDialogProps = {
+  handleClose: () => void;
+};
+export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
+  ({ handleClose }, ref) => {
+    const mx = useMatrixClient();
+    const hasEncryptedRoom = !!mx.getRooms().find((room) => room.hasEncryptionStateEvent());
+    const crossSigningActive = useCrossSigningActive();
+    const verificationStatus = useDeviceVerificationStatus(
+      mx.getCrypto(),
+      mx.getSafeUserId(),
+      mx.getDeviceId() ?? undefined
+    );
+
+    const [logoutState, logout] = useAsyncCallback<void, Error, []>(
+      useCallback(async () => {
+        await logoutClient(mx);
+      }, [mx])
+    );
+
+    const ongoingLogout = logoutState.status === AsyncStatus.Loading;
+
+    return (
+      <Dialog variant="Surface" ref={ref}>
+        <Header
+          style={{
+            padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
+            borderBottomWidth: config.borderWidth.B300,
+          }}
+          variant="Surface"
+          size="500"
+        >
+          <Box grow="Yes">
+            <Text size="H4">Logout</Text>
+          </Box>
+        </Header>
+        <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+          {hasEncryptedRoom &&
+            (crossSigningActive ? (
+              verificationStatus === VerificationStatus.Unverified && (
+                <InfoCard
+                  variant="Critical"
+                  title="Unverified Device"
+                  description="Verify your device before logging out to save your encrypted messages."
+                />
+              )
+            ) : (
+              <InfoCard
+                variant="Critical"
+                title="Alert"
+                description="Enable device verification or export your encrypted data from settings to avoid losing access to your messages."
+              />
+            ))}
+          <Text priority="400">You’re about to log out. Are you sure?</Text>
+          {logoutState.status === AsyncStatus.Error && (
+            <Text style={{ color: color.Critical.Main }} size="T300">
+              Failed to logout! {logoutState.error.message}
+            </Text>
+          )}
+          <Box direction="Column" gap="200">
+            <Button
+              variant="Critical"
+              onClick={logout}
+              disabled={ongoingLogout}
+              before={ongoingLogout && <Spinner variant="Critical" fill="Solid" size="200" />}
+            >
+              <Text size="B400">Logout</Text>
+            </Button>
+            <Button variant="Secondary" fill="Soft" onClick={handleClose} disabled={ongoingLogout}>
+              <Text size="B400">Cancel</Text>
+            </Button>
+          </Box>
+        </Box>
+      </Dialog>
+    );
+  }
+);
diff --git a/src/app/components/ManualVerification.tsx b/src/app/components/ManualVerification.tsx
new file mode 100644 (file)
index 0000000..03f3e71
--- /dev/null
@@ -0,0 +1,199 @@
+import React, { MouseEventHandler, ReactNode, useCallback, useState } from 'react';
+import {
+  Box,
+  Text,
+  Chip,
+  Icon,
+  Icons,
+  RectCords,
+  PopOut,
+  Menu,
+  config,
+  MenuItem,
+  color,
+} from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { stopPropagation } from '../utils/keyboard';
+import { SettingTile } from './setting-tile';
+import { SecretStorageKeyContent } from '../../types/matrix/accountData';
+import { SecretStorageRecoveryKey, SecretStorageRecoveryPassphrase } from './SecretStorage';
+import { useMatrixClient } from '../hooks/useMatrixClient';
+import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
+import { storePrivateKey } from '../../client/state/secretStorageKeys';
+
+export enum ManualVerificationMethod {
+  RecoveryPassphrase = 'passphrase',
+  RecoveryKey = 'key',
+}
+type ManualVerificationMethodSwitcherProps = {
+  value: ManualVerificationMethod;
+  onChange: (value: ManualVerificationMethod) => void;
+};
+export function ManualVerificationMethodSwitcher({
+  value,
+  onChange,
+}: ManualVerificationMethodSwitcherProps) {
+  const [menuCords, setMenuCords] = useState<RectCords>();
+
+  const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setMenuCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const handleSelect = (method: ManualVerificationMethod) => {
+    setMenuCords(undefined);
+    onChange(method);
+  };
+
+  return (
+    <>
+      <Chip
+        type="button"
+        variant="Secondary"
+        fill="Soft"
+        radii="Pill"
+        before={<Icon size="100" src={Icons.ChevronBottom} />}
+        onClick={handleMenu}
+      >
+        <Text as="span" size="B300">
+          {value === ManualVerificationMethod.RecoveryPassphrase && 'Recovery Passphrase'}
+          {value === ManualVerificationMethod.RecoveryKey && 'Recovery Key'}
+        </Text>
+      </Chip>
+      <PopOut
+        anchor={menuCords}
+        offset={5}
+        position="Bottom"
+        align="End"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setMenuCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <Menu>
+              <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+                <MenuItem
+                  size="300"
+                  variant="Surface"
+                  aria-selected={value === ManualVerificationMethod.RecoveryPassphrase}
+                  radii="300"
+                  onClick={() => handleSelect(ManualVerificationMethod.RecoveryPassphrase)}
+                >
+                  <Box grow="Yes">
+                    <Text size="T300">Recovery Passphrase</Text>
+                  </Box>
+                </MenuItem>
+                <MenuItem
+                  size="300"
+                  variant="Surface"
+                  aria-selected={value === ManualVerificationMethod.RecoveryKey}
+                  radii="300"
+                  onClick={() => handleSelect(ManualVerificationMethod.RecoveryKey)}
+                >
+                  <Box grow="Yes">
+                    <Text size="T300">Recovery Key</Text>
+                  </Box>
+                </MenuItem>
+              </Box>
+            </Menu>
+          </FocusTrap>
+        }
+      />
+    </>
+  );
+}
+
+type ManualVerificationTileProps = {
+  secretStorageKeyId: string;
+  secretStorageKeyContent: SecretStorageKeyContent;
+  options?: ReactNode;
+};
+export function ManualVerificationTile({
+  secretStorageKeyId,
+  secretStorageKeyContent,
+  options,
+}: ManualVerificationTileProps) {
+  const mx = useMatrixClient();
+
+  const hasPassphrase = !!secretStorageKeyContent.passphrase;
+  const [method, setMethod] = useState(
+    hasPassphrase
+      ? ManualVerificationMethod.RecoveryPassphrase
+      : ManualVerificationMethod.RecoveryKey
+  );
+
+  const verifyAndRestoreBackup = useCallback(
+    async (recoveryKey: Uint8Array) => {
+      const crypto = mx.getCrypto();
+      if (!crypto) {
+        throw new Error('Unexpected Error! Crypto object not found.');
+      }
+
+      storePrivateKey(secretStorageKeyId, recoveryKey);
+
+      await crypto.bootstrapCrossSigning({});
+      await crypto.bootstrapSecretStorage({});
+
+      await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
+    },
+    [mx, secretStorageKeyId]
+  );
+
+  const [verifyState, handleDecodedRecoveryKey] = useAsyncCallback<void, Error, [Uint8Array]>(
+    verifyAndRestoreBackup
+  );
+  const verifying = verifyState.status === AsyncStatus.Loading;
+
+  return (
+    <Box direction="Column" gap="200">
+      <SettingTile
+        title="Verify Manually"
+        description={hasPassphrase ? 'Select a verification method.' : 'Provide recovery key.'}
+        after={
+          <Box alignItems="Center" gap="200">
+            {hasPassphrase && (
+              <ManualVerificationMethodSwitcher value={method} onChange={setMethod} />
+            )}
+            {options}
+          </Box>
+        }
+      />
+      {verifyState.status === AsyncStatus.Success ? (
+        <Text size="T200" style={{ color: color.Success.Main }}>
+          <b>Device verified!</b>
+        </Text>
+      ) : (
+        <Box direction="Column" gap="100">
+          {method === ManualVerificationMethod.RecoveryKey && (
+            <SecretStorageRecoveryKey
+              processing={verifying}
+              keyContent={secretStorageKeyContent}
+              onDecodedRecoveryKey={handleDecodedRecoveryKey}
+            />
+          )}
+          {method === ManualVerificationMethod.RecoveryPassphrase &&
+            secretStorageKeyContent.passphrase && (
+              <SecretStorageRecoveryPassphrase
+                processing={verifying}
+                keyContent={secretStorageKeyContent}
+                passphraseContent={secretStorageKeyContent.passphrase}
+                onDecodedRecoveryKey={handleDecodedRecoveryKey}
+              />
+            )}
+          {verifyState.status === AsyncStatus.Error && (
+            <Text size="T200" style={{ color: color.Critical.Main }}>
+              <b>{verifyState.error.message}</b>
+            </Text>
+          )}
+        </Box>
+      )}
+    </Box>
+  );
+}
diff --git a/src/app/components/Modal500.tsx b/src/app/components/Modal500.tsx
new file mode 100644 (file)
index 0000000..d421b62
--- /dev/null
@@ -0,0 +1,29 @@
+import React, { ReactNode } from 'react';
+import FocusTrap from 'focus-trap-react';
+import { Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
+import { stopPropagation } from '../utils/keyboard';
+
+type Modal500Props = {
+  requestClose: () => void;
+  children: ReactNode;
+};
+export function Modal500({ requestClose, children }: Modal500Props) {
+  return (
+    <Overlay open backdrop={<OverlayBackdrop />}>
+      <OverlayCenter>
+        <FocusTrap
+          focusTrapOptions={{
+            initialFocus: false,
+            clickOutsideDeactivates: true,
+            onDeactivate: requestClose,
+            escapeDeactivates: stopPropagation,
+          }}
+        >
+          <Modal size="500" variant="Background">
+            {children}
+          </Modal>
+        </FocusTrap>
+      </OverlayCenter>
+    </Overlay>
+  );
+}
diff --git a/src/app/components/SecretStorage.tsx b/src/app/components/SecretStorage.tsx
new file mode 100644 (file)
index 0000000..55d466d
--- /dev/null
@@ -0,0 +1,200 @@
+import React, { FormEventHandler, useCallback } from 'react';
+import { Box, Text, Button, Spinner, color } from 'folds';
+import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto-api';
+import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
+import { PasswordInput } from './password-input';
+import {
+  SecretStorageKeyContent,
+  SecretStoragePassphraseContent,
+} from '../../types/matrix/accountData';
+import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
+import { useMatrixClient } from '../hooks/useMatrixClient';
+import { useAlive } from '../hooks/useAlive';
+
+type SecretStorageRecoveryPassphraseProps = {
+  processing?: boolean;
+  keyContent: SecretStorageKeyContent;
+  passphraseContent: SecretStoragePassphraseContent;
+  onDecodedRecoveryKey: (recoveryKey: Uint8Array) => void;
+};
+export function SecretStorageRecoveryPassphrase({
+  processing,
+  keyContent,
+  passphraseContent,
+  onDecodedRecoveryKey,
+}: SecretStorageRecoveryPassphraseProps) {
+  const mx = useMatrixClient();
+  const alive = useAlive();
+
+  const [driveKeyState, submitPassphrase] = useAsyncCallback<
+    Uint8Array,
+    Error,
+    Parameters<typeof deriveKey>
+  >(
+    useCallback(
+      async (passphrase, salt, iterations, bits) => {
+        const decodedRecoveryKey = await deriveKey(passphrase, salt, iterations, bits);
+
+        const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any);
+
+        if (!match) {
+          throw new Error('Invalid recovery passphrase.');
+        }
+
+        return decodedRecoveryKey;
+      },
+      [mx, keyContent]
+    )
+  );
+
+  const drivingKey = driveKeyState.status === AsyncStatus.Loading;
+  const loading = drivingKey || processing;
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    if (loading) return;
+    evt.preventDefault();
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const recoveryPassphraseInput = target?.recoveryPassphraseInput as HTMLInputElement | undefined;
+    if (!recoveryPassphraseInput) return;
+    const recoveryPassphrase = recoveryPassphraseInput.value.trim();
+    if (!recoveryPassphrase) return;
+
+    const { salt, iterations, bits } = passphraseContent;
+    submitPassphrase(recoveryPassphrase, salt, iterations, bits).then((decodedRecoveryKey) => {
+      if (alive()) {
+        recoveryPassphraseInput.value = '';
+        onDecodedRecoveryKey(decodedRecoveryKey);
+      }
+    });
+  };
+
+  return (
+    <Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
+      <Box gap="200" alignItems="End">
+        <Box grow="Yes" direction="Column" gap="100">
+          <Text size="L400">Recovery Passphrase</Text>
+          <PasswordInput
+            name="recoveryPassphraseInput"
+            size="400"
+            variant="Secondary"
+            radii="300"
+            autoFocus
+            required
+            outlined
+            readOnly={loading}
+          />
+        </Box>
+        <Box shrink="No" gap="200">
+          <Button
+            type="submit"
+            variant="Success"
+            size="400"
+            radii="300"
+            disabled={loading}
+            before={loading && <Spinner size="200" variant="Success" fill="Solid" />}
+          >
+            <Text as="span" size="B400">
+              Verify
+            </Text>
+          </Button>
+        </Box>
+      </Box>
+      {driveKeyState.status === AsyncStatus.Error && (
+        <Text size="T200" style={{ color: color.Critical.Main }}>
+          <b>{driveKeyState.error.message}</b>
+        </Text>
+      )}
+    </Box>
+  );
+}
+
+type SecretStorageRecoveryKeyProps = {
+  processing?: boolean;
+  keyContent: SecretStorageKeyContent;
+  onDecodedRecoveryKey: (recoveryKey: Uint8Array) => void;
+};
+export function SecretStorageRecoveryKey({
+  processing,
+  keyContent,
+  onDecodedRecoveryKey,
+}: SecretStorageRecoveryKeyProps) {
+  const mx = useMatrixClient();
+  const alive = useAlive();
+
+  const [driveKeyState, submitRecoveryKey] = useAsyncCallback<Uint8Array, Error, [string]>(
+    useCallback(
+      async (recoveryKey) => {
+        const decodedRecoveryKey = decodeRecoveryKey(recoveryKey);
+
+        const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any);
+
+        if (!match) {
+          throw new Error('Invalid recovery key.');
+        }
+
+        return decodedRecoveryKey;
+      },
+      [mx, keyContent]
+    )
+  );
+
+  const drivingKey = driveKeyState.status === AsyncStatus.Loading;
+  const loading = drivingKey || processing;
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const recoveryKeyInput = target?.recoveryKeyInput as HTMLInputElement | undefined;
+    if (!recoveryKeyInput) return;
+    const recoveryKey = recoveryKeyInput.value.trim();
+    if (!recoveryKey) return;
+
+    submitRecoveryKey(recoveryKey).then((decodedRecoveryKey) => {
+      if (alive()) {
+        recoveryKeyInput.value = '';
+        onDecodedRecoveryKey(decodedRecoveryKey);
+      }
+    });
+  };
+
+  return (
+    <Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
+      <Box gap="200" alignItems="End">
+        <Box grow="Yes" direction="Column" gap="100">
+          <Text size="L400">Recovery Key</Text>
+          <PasswordInput
+            name="recoveryKeyInput"
+            size="400"
+            variant="Secondary"
+            radii="300"
+            autoFocus
+            required
+            outlined
+            readOnly={loading}
+          />
+        </Box>
+        <Box shrink="No" gap="200">
+          <Button
+            type="submit"
+            variant="Success"
+            size="400"
+            radii="300"
+            disabled={loading}
+            before={loading && <Spinner size="200" variant="Success" fill="Solid" />}
+          >
+            <Text as="span" size="B400">
+              Verify
+            </Text>
+          </Button>
+        </Box>
+      </Box>
+      {driveKeyState.status === AsyncStatus.Error && (
+        <Text size="T200" style={{ color: color.Critical.Main }}>
+          <b>{driveKeyState.error.message}</b>
+        </Text>
+      )}
+    </Box>
+  );
+}
index dc517b48207589c35498def6637fec281e6bf4aa..a2bdd1b875a01f113a1715f61ada66f6e35bbcc3 100644 (file)
@@ -13,7 +13,6 @@ import {
   IconButton,
 } from 'folds';
 import FocusTrap from 'focus-trap-react';
-import { stopPropagation } from '../utils/keyboard';
 
 export type UIAFlowOverlayProps = {
   currentStep: number;
@@ -29,7 +28,7 @@ export function UIAFlowOverlay({
 }: UIAFlowOverlayProps) {
   return (
     <Overlay open backdrop={<OverlayBackdrop />}>
-      <FocusTrap focusTrapOptions={{ initialFocus: false, escapeDeactivates: stopPropagation }}>
+      <FocusTrap focusTrapOptions={{ initialFocus: false, escapeDeactivates: false }}>
         <Box style={{ height: '100%' }} direction="Column" grow="Yes" gap="400">
           <Box grow="Yes" direction="Column" alignItems="Center" justifyContent="Center">
             {children}
index c3787691783b8fa5b96f1ee0102a1a4581792aea..591f1bffe08bb6152b98c523be8d21cd29c8d2ea 100644 (file)
@@ -16,14 +16,14 @@ import { createEmoticonElement, moveCursor, replaceWithElement } from '../utils'
 import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
 import { useRelevantImagePacks } from '../../../hooks/useImagePacks';
 import { IEmoji, emojis } from '../../../plugins/emoji';
-import { ExtendedPackImage, PackUsage } from '../../../plugins/custom-emoji';
 import { useKeyDown } from '../../../hooks/useKeyDown';
 import { mxcUrlToHttp } from '../../../utils/matrix';
 import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { ImageUsage, PackImageReader } from '../../../plugins/custom-emoji';
 
 type EmoticonCompleteHandler = (key: string, shortcode: string) => void;
 
-type EmoticonSearchItem = ExtendedPackImage | IEmoji;
+type EmoticonSearchItem = PackImageReader | IEmoji;
 
 type EmoticonAutocompleteProps = {
   imagePackRooms: Room[];
@@ -52,21 +52,21 @@ export function EmoticonAutocomplete({
   const mx = useMatrixClient();
   const useAuthentication = useMediaAuthentication();
 
-  const imagePacks = useRelevantImagePacks(mx, PackUsage.Emoticon, imagePackRooms);
+  const imagePacks = useRelevantImagePacks(ImageUsage.Emoticon, imagePackRooms);
   const recentEmoji = useRecentEmoji(mx, 20);
 
   const searchList = useMemo(() => {
     const list: Array<EmoticonSearchItem> = [];
-    return list
-      .concat(
-        imagePacks.flatMap((pack) => pack.getImagesFor(PackUsage.Emoticon)),
-        emojis
-      )
+    return list.concat(
+      imagePacks.flatMap((pack) => pack.getImages(ImageUsage.Emoticon)),
+      emojis
+    );
   }, [imagePacks]);
 
   const [result, search, resetSearch] = useAsyncSearch(searchList, getEmoticonStr, SEARCH_OPTIONS);
-  const autoCompleteEmoticon = (result ? result.items : recentEmoji)
-      .sort((a, b) => a.shortcode.localeCompare(b.shortcode));
+  const autoCompleteEmoticon = (result ? result.items : recentEmoji).sort((a, b) =>
+    a.shortcode.localeCompare(b.shortcode)
+  );
 
   useEffect(() => {
     if (query.text) search(query.text);
index f81b75ec9b5c0e8673eb964955ebfe744f18a556..f3bd551f5f98256946c32e827001aa42fc95e7ed 100644 (file)
@@ -41,7 +41,6 @@ import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard
 import { useRelevantImagePacks } from '../../hooks/useImagePacks';
 import { useMatrixClient } from '../../hooks/useMatrixClient';
 import { useRecentEmoji } from '../../hooks/useRecentEmoji';
-import { ExtendedPackImage, ImagePack, PackUsage } from '../../plugins/custom-emoji';
 import { isUserId, mxcUrlToHttp } from '../../utils/matrix';
 import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from '../../utils/dom';
 import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
@@ -50,6 +49,7 @@ import { useThrottle } from '../../hooks/useThrottle';
 import { addRecentEmoji } from '../../plugins/recent-emoji';
 import { mobileOrTablet } from '../../utils/user-agent';
 import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { ImagePack, ImageUsage, PackImageReader } from '../../plugins/custom-emoji';
 
 const RECENT_GROUP_ID = 'recent_group';
 const SEARCH_GROUP_ID = 'search_group';
@@ -359,16 +359,16 @@ function ImagePackSidebarStack({
 }: {
   mx: MatrixClient;
   packs: ImagePack[];
-  usage: PackUsage;
+  usage: ImageUsage;
   onItemClick: (id: string) => void;
   useAuthentication?: boolean;
 }) {
   const activeGroupId = useAtomValue(activeGroupIdAtom);
   return (
     <SidebarStack>
-      {usage === PackUsage.Emoticon && <SidebarDivider />}
+      {usage === ImageUsage.Emoticon && <SidebarDivider />}
       {packs.map((pack) => {
-        let label = pack.displayName;
+        let label = pack.meta.name;
         if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
         return (
           <SidebarBtn
@@ -384,7 +384,10 @@ function ImagePackSidebarStack({
                 height: toRem(24),
                 objectFit: 'contain',
               }}
-              src={mxcUrlToHttp(mx, pack.getPackAvatarUrl(usage) ?? '', useAuthentication) || pack.avatarUrl}
+              src={
+                mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ||
+                pack.meta.avatar
+              }
               alt={label || 'Unknown Pack'}
             />
           </SidebarBtn>
@@ -462,130 +465,156 @@ export function SearchEmojiGroup({
   tab: EmojiBoardTab;
   label: string;
   id: string;
-  emojis: Array<ExtendedPackImage | IEmoji>;
+  emojis: Array<PackImageReader | IEmoji>;
   useAuthentication?: boolean;
 }) {
   return (
     <EmojiGroup key={id} id={id} label={label}>
       {tab === EmojiBoardTab.Emoji
-        ? searchResult.sort((a, b) => a.shortcode.localeCompare(b.shortcode)).map((emoji) =>
-          'unicode' in emoji ? (
-            <EmojiItem
-              key={emoji.unicode}
-              label={emoji.label}
-              type={EmojiType.Emoji}
-              data={emoji.unicode}
-              shortcode={emoji.shortcode}
-            >
-              {emoji.unicode}
-            </EmojiItem>
-          ) : (
-            <EmojiItem
-              key={emoji.shortcode}
-              label={emoji.body || emoji.shortcode}
-              type={EmojiType.CustomEmoji}
-              data={emoji.url}
-              shortcode={emoji.shortcode}
-            >
-              <img
-                loading="lazy"
-                className={css.CustomEmojiImg}
-                alt={emoji.body || emoji.shortcode}
-                src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
-              />
-            </EmojiItem>
-          )
-        )
+        ? searchResult
+            .sort((a, b) => a.shortcode.localeCompare(b.shortcode))
+            .map((emoji) =>
+              'unicode' in emoji ? (
+                <EmojiItem
+                  key={emoji.unicode}
+                  label={emoji.label}
+                  type={EmojiType.Emoji}
+                  data={emoji.unicode}
+                  shortcode={emoji.shortcode}
+                >
+                  {emoji.unicode}
+                </EmojiItem>
+              ) : (
+                <EmojiItem
+                  key={emoji.shortcode}
+                  label={emoji.body || emoji.shortcode}
+                  type={EmojiType.CustomEmoji}
+                  data={emoji.url}
+                  shortcode={emoji.shortcode}
+                >
+                  <img
+                    loading="lazy"
+                    className={css.CustomEmojiImg}
+                    alt={emoji.body || emoji.shortcode}
+                    src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
+                  />
+                </EmojiItem>
+              )
+            )
         : searchResult.map((emoji) =>
-          'unicode' in emoji ? null : (
-            <StickerItem
-              key={emoji.shortcode}
-              label={emoji.body || emoji.shortcode}
-              type={EmojiType.Sticker}
-              data={emoji.url}
-              shortcode={emoji.shortcode}
-            >
-              <img
-                loading="lazy"
-                className={css.StickerImg}
-                alt={emoji.body || emoji.shortcode}
-                src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
-              />
-            </StickerItem>
-          )
-        )}
+            'unicode' in emoji ? null : (
+              <StickerItem
+                key={emoji.shortcode}
+                label={emoji.body || emoji.shortcode}
+                type={EmojiType.Sticker}
+                data={emoji.url}
+                shortcode={emoji.shortcode}
+              >
+                <img
+                  loading="lazy"
+                  className={css.StickerImg}
+                  alt={emoji.body || emoji.shortcode}
+                  src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
+                />
+              </StickerItem>
+            )
+          )}
     </EmojiGroup>
   );
 }
 
 export const CustomEmojiGroups = memo(
-  ({ mx, groups, useAuthentication }: { mx: MatrixClient; groups: ImagePack[]; useAuthentication?: boolean }) => (
+  ({
+    mx,
+    groups,
+    useAuthentication,
+  }: {
+    mx: MatrixClient;
+    groups: ImagePack[];
+    useAuthentication?: boolean;
+  }) => (
     <>
       {groups.map((pack) => (
-        <EmojiGroup key={pack.id} id={pack.id} label={pack.displayName || 'Unknown'}>
-          {pack.getEmojis().sort((a, b) => a.shortcode.localeCompare(b.shortcode)).map((image) => (
-            <EmojiItem
-              key={image.shortcode}
-              label={image.body || image.shortcode}
-              type={EmojiType.CustomEmoji}
-              data={image.url}
-              shortcode={image.shortcode}
-            >
-              <img
-                loading="lazy"
-                className={css.CustomEmojiImg}
-                alt={image.body || image.shortcode}
-                src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
-              />
-            </EmojiItem>
-          ))}
+        <EmojiGroup key={pack.id} id={pack.id} label={pack.meta.name || 'Unknown'}>
+          {pack
+            .getImages(ImageUsage.Emoticon)
+            .sort((a, b) => a.shortcode.localeCompare(b.shortcode))
+            .map((image) => (
+              <EmojiItem
+                key={image.shortcode}
+                label={image.body || image.shortcode}
+                type={EmojiType.CustomEmoji}
+                data={image.url}
+                shortcode={image.shortcode}
+              >
+                <img
+                  loading="lazy"
+                  className={css.CustomEmojiImg}
+                  alt={image.body || image.shortcode}
+                  src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
+                />
+              </EmojiItem>
+            ))}
         </EmojiGroup>
       ))}
     </>
   )
 );
 
-export const StickerGroups = memo(({ mx, groups, useAuthentication }: { mx: MatrixClient; groups: ImagePack[]; useAuthentication?: boolean }) => (
-  <>
-    {groups.length === 0 && (
-      <Box
-        style={{ padding: `${toRem(60)} ${config.space.S500}` }}
-        alignItems="Center"
-        justifyContent="Center"
-        direction="Column"
-        gap="300"
-      >
-        <Icon size="600" src={Icons.Sticker} />
-        <Box direction="Inherit">
-          <Text align="Center">No Sticker Packs!</Text>
-          <Text priority="300" align="Center" size="T200">
-            Add stickers from user, room or space settings.
-          </Text>
+export const StickerGroups = memo(
+  ({
+    mx,
+    groups,
+    useAuthentication,
+  }: {
+    mx: MatrixClient;
+    groups: ImagePack[];
+    useAuthentication?: boolean;
+  }) => (
+    <>
+      {groups.length === 0 && (
+        <Box
+          style={{ padding: `${toRem(60)} ${config.space.S500}` }}
+          alignItems="Center"
+          justifyContent="Center"
+          direction="Column"
+          gap="300"
+        >
+          <Icon size="600" src={Icons.Sticker} />
+          <Box direction="Inherit">
+            <Text align="Center">No Sticker Packs!</Text>
+            <Text priority="300" align="Center" size="T200">
+              Add stickers from user, room or space settings.
+            </Text>
+          </Box>
         </Box>
-      </Box>
-    )}
-    {groups.map((pack) => (
-      <EmojiGroup key={pack.id} id={pack.id} label={pack.displayName || 'Unknown'}>
-        {pack.getStickers().sort((a, b) => a.shortcode.localeCompare(b.shortcode)).map((image) => (
-          <StickerItem
-            key={image.shortcode}
-            label={image.body || image.shortcode}
-            type={EmojiType.Sticker}
-            data={image.url}
-            shortcode={image.shortcode}
-          >
-            <img
-              loading="lazy"
-              className={css.StickerImg}
-              alt={image.body || image.shortcode}
-              src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
-            />
-          </StickerItem>
-        ))}
-      </EmojiGroup>
-    ))}
-  </>
-));
+      )}
+      {groups.map((pack) => (
+        <EmojiGroup key={pack.id} id={pack.id} label={pack.meta.name || 'Unknown'}>
+          {pack
+            .getImages(ImageUsage.Sticker)
+            .sort((a, b) => a.shortcode.localeCompare(b.shortcode))
+            .map((image) => (
+              <StickerItem
+                key={image.shortcode}
+                label={image.body || image.shortcode}
+                type={EmojiType.Sticker}
+                data={image.url}
+                shortcode={image.shortcode}
+              >
+                <img
+                  loading="lazy"
+                  className={css.StickerImg}
+                  alt={image.body || image.shortcode}
+                  src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
+                />
+              </StickerItem>
+            ))}
+        </EmojiGroup>
+      ))}
+    </>
+  )
+);
 
 export const NativeEmojiGroups = memo(
   ({ groups, labels }: { groups: IEmojiGroup[]; labels: IEmojiGroupLabels }) => (
@@ -609,7 +638,7 @@ export const NativeEmojiGroups = memo(
   )
 );
 
-const getSearchListItemStr = (item: ExtendedPackImage | IEmoji) => {
+const getSearchListItemStr = (item: PackImageReader | IEmoji) => {
   const shortcode = `:${item.shortcode}:`;
   if ('body' in item) {
     return [shortcode, item.body ?? ''];
@@ -646,14 +675,14 @@ export function EmojiBoard({
 }) {
   const emojiTab = tab === EmojiBoardTab.Emoji;
   const stickerTab = tab === EmojiBoardTab.Sticker;
-  const usage = emojiTab ? PackUsage.Emoticon : PackUsage.Sticker;
+  const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker;
 
   const setActiveGroupId = useSetAtom(activeGroupIdAtom);
   const mx = useMatrixClient();
   const useAuthentication = useMediaAuthentication();
   const emojiGroupLabels = useEmojiGroupLabels();
   const emojiGroupIcons = useEmojiGroupIcons();
-  const imagePacks = useRelevantImagePacks(mx, usage, imagePackRooms);
+  const imagePacks = useRelevantImagePacks(usage, imagePackRooms);
   const recentEmojis = useRecentEmoji(mx, 21);
 
   const contentScrollRef = useRef<HTMLDivElement>(null);
@@ -661,8 +690,8 @@ export function EmojiBoard({
   const emojiPreviewTextRef = useRef<HTMLParagraphElement>(null);
 
   const searchList = useMemo(() => {
-    let list: Array<ExtendedPackImage | IEmoji> = [];
-    list = list.concat(imagePacks.flatMap((pack) => pack.getImagesFor(usage)));
+    let list: Array<PackImageReader | IEmoji> = [];
+    list = list.concat(imagePacks.flatMap((pack) => pack.getImages(usage)));
     if (emojiTab) list = list.concat(emojis);
     return list;
   }, [emojiTab, usage, imagePacks]);
@@ -688,7 +717,7 @@ export function EmojiBoard({
   const syncActiveGroupId = useCallback(() => {
     const targetEl = contentScrollRef.current;
     if (!targetEl) return;
-    const groupEls = [...targetEl.querySelectorAll('div[data-group-id]')] as HTMLElement[];
+    const groupEls = Array.from(targetEl.querySelectorAll('div[data-group-id]')) as HTMLElement[];
     const groupEl = groupEls.find((el) => isIntersectingScrollView(targetEl, el));
     const groupId = groupEl?.getAttribute('data-group-id') ?? undefined;
     setActiveGroupId(groupId);
@@ -735,7 +764,10 @@ export function EmojiBoard({
       } else if (emojiInfo.type === EmojiType.CustomEmoji && emojiPreviewRef.current) {
         const img = document.createElement('img');
         img.className = css.CustomEmojiImg;
-        img.setAttribute('src', mxcUrlToHttp(mx, emojiInfo.data, useAuthentication) || emojiInfo.data);
+        img.setAttribute(
+          'src',
+          mxcUrlToHttp(mx, emojiInfo.data, useAuthentication) || emojiInfo.data
+        );
         img.setAttribute('alt', emojiInfo.shortcode);
         emojiPreviewRef.current.textContent = '';
         emojiPreviewRef.current.appendChild(img);
@@ -903,8 +935,16 @@ export function EmojiBoard({
               {emojiTab && recentEmojis.length > 0 && (
                 <RecentEmojiGroup id={RECENT_GROUP_ID} label="Recent" emojis={recentEmojis} />
               )}
-              {emojiTab && <CustomEmojiGroups mx={mx} groups={imagePacks} useAuthentication={useAuthentication} />}
-              {stickerTab && <StickerGroups mx={mx} groups={imagePacks} useAuthentication={useAuthentication} />}
+              {emojiTab && (
+                <CustomEmojiGroups
+                  mx={mx}
+                  groups={imagePacks}
+                  useAuthentication={useAuthentication}
+                />
+              )}
+              {stickerTab && (
+                <StickerGroups mx={mx} groups={imagePacks} useAuthentication={useAuthentication} />
+              )}
               {emojiTab && <NativeEmojiGroups groups={emojiGroups} labels={emojiGroupLabels} />}
             </Box>
           </Scroll>
diff --git a/src/app/components/image-editor/ImageEditor.css.ts b/src/app/components/image-editor/ImageEditor.css.ts
new file mode 100644 (file)
index 0000000..6b42c22
--- /dev/null
@@ -0,0 +1,35 @@
+import { style } from '@vanilla-extract/css';
+import { DefaultReset, color, config } from 'folds';
+
+export const ImageEditor = style([
+  DefaultReset,
+  {
+    height: '100%',
+  },
+]);
+
+export const ImageEditorHeader = style([
+  DefaultReset,
+  {
+    paddingLeft: config.space.S200,
+    paddingRight: config.space.S200,
+    borderBottomWidth: config.borderWidth.B300,
+    flexShrink: 0,
+    gap: config.space.S200,
+  },
+]);
+
+export const ImageEditorContent = style([
+  DefaultReset,
+  {
+    backgroundColor: color.Background.Container,
+    color: color.Background.OnContainer,
+    overflow: 'hidden',
+  },
+]);
+
+export const Image = style({
+  width: '100%',
+  height: '100%',
+  objectFit: 'contain',
+});
diff --git a/src/app/components/image-editor/ImageEditor.tsx b/src/app/components/image-editor/ImageEditor.tsx
new file mode 100644 (file)
index 0000000..1ec474e
--- /dev/null
@@ -0,0 +1,51 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Box, Chip, Header, Icon, IconButton, Icons, Text, as } from 'folds';
+import * as css from './ImageEditor.css';
+
+export type ImageEditorProps = {
+  name: string;
+  url: string;
+  requestClose: () => void;
+};
+
+export const ImageEditor = as<'div', ImageEditorProps>(
+  ({ className, name, url, requestClose, ...props }, ref) => {
+    const handleApply = () => {
+      //
+    };
+
+    return (
+      <Box
+        className={classNames(css.ImageEditor, className)}
+        direction="Column"
+        {...props}
+        ref={ref}
+      >
+        <Header className={css.ImageEditorHeader} size="400">
+          <Box grow="Yes" alignItems="Center" gap="200">
+            <IconButton size="300" radii="300" onClick={requestClose}>
+              <Icon size="50" src={Icons.ArrowLeft} />
+            </IconButton>
+            <Text size="T300" truncate>
+              Image Editor
+            </Text>
+          </Box>
+          <Box shrink="No" alignItems="Center" gap="200">
+            <Chip variant="Primary" radii="300" onClick={handleApply}>
+              <Text size="B300">Save</Text>
+            </Chip>
+          </Box>
+        </Header>
+        <Box
+          grow="Yes"
+          className={css.ImageEditorContent}
+          justifyContent="Center"
+          alignItems="Center"
+        >
+          <img className={css.Image} src={url} alt={name} />
+        </Box>
+      </Box>
+    );
+  }
+);
diff --git a/src/app/components/image-editor/index.ts b/src/app/components/image-editor/index.ts
new file mode 100644 (file)
index 0000000..51907cb
--- /dev/null
@@ -0,0 +1 @@
+export * from './ImageEditor';
diff --git a/src/app/components/image-pack-view/ImagePackContent.tsx b/src/app/components/image-pack-view/ImagePackContent.tsx
new file mode 100644 (file)
index 0000000..a696ebe
--- /dev/null
@@ -0,0 +1,388 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { as, Box, Text, config, Button, Menu, Spinner } from 'folds';
+import {
+  ImagePack,
+  ImageUsage,
+  PackContent,
+  PackImage,
+  PackImageReader,
+  packMetaEqual,
+  PackMetaReader,
+} from '../../plugins/custom-emoji';
+import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { SequenceCard } from '../sequence-card';
+import { ImageTile, ImageTileEdit, ImageTileUpload } from './ImageTile';
+import { SettingTile } from '../setting-tile';
+import { UsageSwitcher } from './UsageSwitcher';
+import { ImagePackProfile, ImagePackProfileEdit } from './PackMeta';
+import * as css from './style.css';
+import { useFilePicker } from '../../hooks/useFilePicker';
+import { CompactUploadCardRenderer } from '../upload-card';
+import { UploadSuccess } from '../../state/upload';
+import { getImageInfo, TUploadContent } from '../../utils/matrix';
+import { getImageFileUrl, loadImageElement, renameFile } from '../../utils/dom';
+import { replaceSpaceWithDash, suffixRename } from '../../utils/common';
+import { getFileNameWithoutExt } from '../../utils/mimeTypes';
+import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
+
+export type ImagePackContentProps = {
+  imagePack: ImagePack;
+  canEdit?: boolean;
+  onUpdate?: (packContent: PackContent) => Promise<void>;
+};
+
+export const ImagePackContent = as<'div', ImagePackContentProps>(
+  ({ imagePack, canEdit, onUpdate, ...props }, ref) => {
+    const useAuthentication = useMediaAuthentication();
+
+    const [metaEditing, setMetaEditing] = useState(false);
+    const [savedMeta, setSavedMeta] = useState<PackMetaReader>();
+    const currentMeta = savedMeta ?? imagePack.meta;
+
+    const images = useMemo(() => Array.from(imagePack.images.collection.values()), [imagePack]);
+    const [files, setFiles] = useState<File[]>([]);
+    const [uploadedImages, setUploadedImages] = useState<PackImageReader[]>([]);
+    const [imagesEditing, setImagesEditing] = useState<Set<string>>(new Set());
+    const [savedImages, setSavedImages] = useState<Map<string, PackImageReader>>(new Map());
+    const [deleteImages, setDeleteImages] = useState<Set<string>>(new Set());
+
+    const hasImageWithShortcode = useCallback(
+      (shortcode: string): boolean => {
+        const hasInPack = imagePack.images.collection.has(shortcode);
+        if (hasInPack) return true;
+        const hasInUploaded =
+          uploadedImages.find((img) => img.shortcode === shortcode) !== undefined;
+        if (hasInUploaded) return true;
+        const hasInSaved =
+          Array.from(savedImages).find(([, img]) => img.shortcode === shortcode) !== undefined;
+        return hasInSaved;
+      },
+      [imagePack, savedImages, uploadedImages]
+    );
+
+    const pickFiles = useFilePicker(
+      useCallback(
+        (pickedFiles: File[]) => {
+          const uniqueFiles = pickedFiles.map((file) => {
+            const fileName = replaceSpaceWithDash(file.name);
+            if (hasImageWithShortcode(fileName)) {
+              const uniqueName = suffixRename(fileName, hasImageWithShortcode);
+              return renameFile(file, uniqueName);
+            }
+            return fileName !== file.name ? renameFile(file, fileName) : file;
+          });
+
+          setFiles((f) => [...f, ...uniqueFiles]);
+        },
+        [hasImageWithShortcode]
+      ),
+      true
+    );
+
+    const handleMetaSave = useCallback(
+      (editedMeta: PackMetaReader) => {
+        setMetaEditing(false);
+        setSavedMeta(
+          (m) =>
+            new PackMetaReader({
+              ...imagePack.meta.content,
+              ...m?.content,
+              ...editedMeta.content,
+            })
+        );
+      },
+      [imagePack.meta]
+    );
+
+    const handleMetaCancel = () => setMetaEditing(false);
+
+    const handlePackUsageChange = useCallback(
+      (usg: ImageUsage[]) => {
+        setSavedMeta(
+          (m) =>
+            new PackMetaReader({
+              ...imagePack.meta.content,
+              ...m?.content,
+              usage: usg,
+            })
+        );
+      },
+      [imagePack.meta]
+    );
+
+    const handleUploadRemove = useCallback((file: TUploadContent) => {
+      setFiles((fs) => fs.filter((f) => f !== file));
+    }, []);
+
+    const handleUploadComplete = useCallback(
+      async (data: UploadSuccess) => {
+        const imgEl = await loadImageElement(getImageFileUrl(data.file));
+        const packImage: PackImage = {
+          url: data.mxc,
+          info: getImageInfo(imgEl, data.file),
+        };
+        const image = PackImageReader.fromPackImage(
+          getFileNameWithoutExt(data.file.name),
+          packImage
+        );
+        if (!image) return;
+        handleUploadRemove(data.file);
+        setUploadedImages((imgs) => [image, ...imgs]);
+      },
+      [handleUploadRemove]
+    );
+
+    const handleImageEdit = (shortcode: string) => {
+      setImagesEditing((shortcodes) => {
+        const shortcodeSet = new Set(shortcodes);
+        shortcodeSet.add(shortcode);
+        return shortcodeSet;
+      });
+    };
+    const handleDeleteToggle = (shortcode: string) => {
+      setDeleteImages((shortcodes) => {
+        const shortcodeSet = new Set(shortcodes);
+        if (shortcodeSet.has(shortcode)) shortcodeSet.delete(shortcode);
+        else shortcodeSet.add(shortcode);
+        return shortcodeSet;
+      });
+    };
+
+    const handleImageEditCancel = (shortcode: string) => {
+      setImagesEditing((shortcodes) => {
+        const shortcodeSet = new Set(shortcodes);
+        shortcodeSet.delete(shortcode);
+        return shortcodeSet;
+      });
+    };
+
+    const handleImageEditSave = (shortcode: string, image: PackImageReader) => {
+      handleImageEditCancel(shortcode);
+
+      const saveImage =
+        shortcode !== image.shortcode && hasImageWithShortcode(image.shortcode)
+          ? new PackImageReader(
+              suffixRename(image.shortcode, hasImageWithShortcode),
+              image.url,
+              image.content
+            )
+          : image;
+
+      setSavedImages((sImgs) => {
+        const imgs = new Map(sImgs);
+        imgs.set(shortcode, saveImage);
+        return imgs;
+      });
+    };
+
+    const handleResetSavedChanges = () => {
+      setSavedMeta(undefined);
+      setFiles([]);
+      setUploadedImages([]);
+      setSavedImages(new Map());
+      setDeleteImages(new Set());
+    };
+
+    const [applyState, applyChanges] = useAsyncCallback(
+      useCallback(async () => {
+        const pack: PackContent = {
+          pack: savedMeta?.content ?? imagePack.meta.content,
+          images: {},
+        };
+        const pushImage = (img: PackImageReader) => {
+          if (deleteImages.has(img.shortcode)) return;
+          if (!pack.images) return;
+          const imgToPush = savedImages.get(img.shortcode) ?? img;
+          pack.images[imgToPush.shortcode] = imgToPush.content;
+        };
+        uploadedImages.forEach((img) => pushImage(img));
+        images.forEach((img) => pushImage(img));
+
+        return onUpdate?.(pack);
+      }, [imagePack, images, savedMeta, uploadedImages, savedImages, deleteImages, onUpdate])
+    );
+
+    useEffect(() => {
+      if (applyState.status === AsyncStatus.Success) {
+        handleResetSavedChanges();
+      }
+    }, [applyState]);
+
+    const savedChanges =
+      (savedMeta && !packMetaEqual(imagePack.meta, savedMeta)) ||
+      uploadedImages.length > 0 ||
+      savedImages.size > 0 ||
+      deleteImages.size > 0;
+    const canApplyChanges = !metaEditing && imagesEditing.size === 0 && files.length === 0;
+    const applying = applyState.status === AsyncStatus.Loading;
+
+    const renderImage = (image: PackImageReader) => (
+      <SequenceCard
+        key={image.shortcode}
+        style={{ padding: config.space.S300 }}
+        variant={deleteImages.has(image.shortcode) ? 'Critical' : 'SurfaceVariant'}
+        direction="Column"
+        gap="400"
+      >
+        {imagesEditing.has(image.shortcode) ? (
+          <ImageTileEdit
+            defaultShortcode={image.shortcode}
+            image={savedImages.get(image.shortcode) ?? image}
+            packUsage={currentMeta.usage}
+            useAuthentication={useAuthentication}
+            onCancel={handleImageEditCancel}
+            onSave={handleImageEditSave}
+          />
+        ) : (
+          <ImageTile
+            defaultShortcode={image.shortcode}
+            image={savedImages.get(image.shortcode) ?? image}
+            packUsage={currentMeta.usage}
+            useAuthentication={useAuthentication}
+            canEdit={canEdit}
+            onEdit={handleImageEdit}
+            deleted={deleteImages.has(image.shortcode)}
+            onDeleteToggle={handleDeleteToggle}
+          />
+        )}
+      </SequenceCard>
+    );
+
+    return (
+      <Box grow="Yes" direction="Column" gap="700" {...props} ref={ref}>
+        {savedChanges && (
+          <Menu className={css.UnsavedMenu} variant="Success">
+            <Box alignItems="Center" gap="400">
+              <Box grow="Yes" direction="Column">
+                {applyState.status === AsyncStatus.Error ? (
+                  <Text size="T200">
+                    <b>Failed to apply changes! Please try again.</b>
+                  </Text>
+                ) : (
+                  <Text size="T200">
+                    <b>Changes saved! Apply when ready.</b>
+                  </Text>
+                )}
+              </Box>
+              <Box shrink="No" gap="200">
+                <Button
+                  size="300"
+                  variant="Success"
+                  fill="None"
+                  radii="300"
+                  disabled={!canApplyChanges || applying}
+                  onClick={handleResetSavedChanges}
+                >
+                  <Text size="B300">Reset</Text>
+                </Button>
+                <Button
+                  size="300"
+                  variant="Success"
+                  radii="300"
+                  disabled={!canApplyChanges || applying}
+                  before={applying && <Spinner variant="Success" fill="Solid" size="100" />}
+                  onClick={applyChanges}
+                >
+                  <Text size="B300">Apply Changes</Text>
+                </Button>
+              </Box>
+            </Box>
+          </Menu>
+        )}
+        <Box direction="Column" gap="100">
+          <Text size="L400">Pack</Text>
+          <SequenceCard
+            style={{ padding: config.space.S300 }}
+            variant="SurfaceVariant"
+            direction="Column"
+            gap="400"
+          >
+            {metaEditing ? (
+              <ImagePackProfileEdit
+                meta={currentMeta}
+                onCancel={handleMetaCancel}
+                onSave={handleMetaSave}
+              />
+            ) : (
+              <ImagePackProfile
+                meta={currentMeta}
+                canEdit={canEdit}
+                onEdit={() => setMetaEditing(true)}
+              />
+            )}
+          </SequenceCard>
+          <SequenceCard
+            style={{ padding: config.space.S300 }}
+            variant="SurfaceVariant"
+            direction="Column"
+            gap="400"
+          >
+            <SettingTile
+              title="Images Usage"
+              description="Select how the images are being used: as emojis, as stickers, or as both."
+              after={
+                <UsageSwitcher
+                  usage={currentMeta.usage}
+                  canEdit={canEdit}
+                  onChange={handlePackUsageChange}
+                />
+              }
+            />
+          </SequenceCard>
+        </Box>
+        {images.length === 0 && !canEdit ? null : (
+          <Box direction="Column" gap="100">
+            <Text size="L400">Images</Text>
+            {canEdit && (
+              <SequenceCard
+                style={{ padding: config.space.S300 }}
+                variant="SurfaceVariant"
+                direction="Column"
+                gap="400"
+              >
+                <SettingTile
+                  title="Upload Images"
+                  description="Select images from your storage to upload them in pack."
+                  after={
+                    <Button
+                      variant="Secondary"
+                      fill="Soft"
+                      size="300"
+                      radii="300"
+                      type="button"
+                      outlined
+                      onClick={() => pickFiles('image/*')}
+                    >
+                      <Text size="B300">Select</Text>
+                    </Button>
+                  }
+                />
+              </SequenceCard>
+            )}
+            {files.map((file) => (
+              <SequenceCard
+                key={file.name}
+                style={{ padding: config.space.S300 }}
+                variant="SurfaceVariant"
+                direction="Column"
+                gap="400"
+              >
+                <ImageTileUpload file={file}>
+                  {(uploadAtom) => (
+                    <CompactUploadCardRenderer
+                      uploadAtom={uploadAtom}
+                      onRemove={handleUploadRemove}
+                      onComplete={handleUploadComplete}
+                    />
+                  )}
+                </ImageTileUpload>
+              </SequenceCard>
+            ))}
+            {uploadedImages.map(renderImage)}
+            {images.map(renderImage)}
+          </Box>
+        )}
+      </Box>
+    );
+  }
+);
diff --git a/src/app/components/image-pack-view/ImagePackView.tsx b/src/app/components/image-pack-view/ImagePackView.tsx
new file mode 100644 (file)
index 0000000..ab81d50
--- /dev/null
@@ -0,0 +1,51 @@
+import React from 'react';
+import { Box, IconButton, Text, Icon, Icons, Scroll, Chip } from 'folds';
+import { PackAddress } from '../../plugins/custom-emoji';
+import { Page, PageHeader, PageContent } from '../page';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { RoomImagePack } from './RoomImagePack';
+import { UserImagePack } from './UserImagePack';
+
+type ImagePackViewProps = {
+  address: PackAddress | undefined;
+  requestClose: () => void;
+};
+export function ImagePackView({ address, requestClose }: ImagePackViewProps) {
+  const mx = useMatrixClient();
+  const room = address && mx.getRoom(address.roomId);
+
+  return (
+    <Page>
+      <PageHeader outlined={false} balance>
+        <Box alignItems="Center" grow="Yes" gap="200">
+          <Box alignItems="Inherit" grow="Yes" gap="200">
+            <Chip
+              size="500"
+              radii="Pill"
+              onClick={requestClose}
+              before={<Icon size="100" src={Icons.ArrowLeft} />}
+            >
+              <Text size="T300">Emojis & Stickers</Text>
+            </Chip>
+          </Box>
+          <Box shrink="No">
+            <IconButton onClick={requestClose} variant="Surface">
+              <Icon src={Icons.Cross} />
+            </IconButton>
+          </Box>
+        </Box>
+      </PageHeader>
+      <Box grow="Yes">
+        <Scroll hideTrack visibility="Hover">
+          <PageContent>
+            {room && address ? (
+              <RoomImagePack room={room} stateKey={address.stateKey} />
+            ) : (
+              <UserImagePack />
+            )}
+          </PageContent>
+        </Scroll>
+      </Box>
+    </Page>
+  );
+}
diff --git a/src/app/components/image-pack-view/ImageTile.tsx b/src/app/components/image-pack-view/ImageTile.tsx
new file mode 100644 (file)
index 0000000..37337ff
--- /dev/null
@@ -0,0 +1,214 @@
+import React, { FormEventHandler, ReactNode, useMemo, useState } from 'react';
+import { Badge, Box, Button, Chip, Icon, Icons, Input, Text } from 'folds';
+import { UsageSwitcher, useUsageStr } from './UsageSwitcher';
+import { mxcUrlToHttp } from '../../utils/matrix';
+import * as css from './style.css';
+import { ImageUsage, imageUsageEqual, PackImageReader } from '../../plugins/custom-emoji';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { SettingTile } from '../setting-tile';
+import { useObjectURL } from '../../hooks/useObjectURL';
+import { createUploadAtom, TUploadAtom } from '../../state/upload';
+import { replaceSpaceWithDash } from '../../utils/common';
+
+type ImageTileProps = {
+  defaultShortcode: string;
+  useAuthentication: boolean;
+  packUsage: ImageUsage[];
+  image: PackImageReader;
+  canEdit?: boolean;
+  onEdit?: (defaultShortcode: string, image: PackImageReader) => void;
+  deleted?: boolean;
+  onDeleteToggle?: (defaultShortcode: string) => void;
+};
+export function ImageTile({
+  defaultShortcode,
+  image,
+  packUsage,
+  useAuthentication,
+  canEdit,
+  onEdit,
+  onDeleteToggle,
+  deleted,
+}: ImageTileProps) {
+  const mx = useMatrixClient();
+  const getUsageStr = useUsageStr();
+
+  return (
+    <SettingTile
+      before={
+        <img
+          className={css.ImagePackImage}
+          src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
+          alt={image.shortcode}
+          loading="lazy"
+        />
+      }
+      title={
+        deleted ? (
+          <span className={css.DeleteImageShortcode}>{image.shortcode}</span>
+        ) : (
+          image.shortcode
+        )
+      }
+      description={
+        <Box as="span" gap="200">
+          {image.usage && getUsageStr(image.usage) !== getUsageStr(packUsage) && (
+            <Badge as="span" variant="Secondary" size="400" radii="300" outlined>
+              <Text as="span" size="L400">
+                {getUsageStr(image.usage)}
+              </Text>
+            </Badge>
+          )}
+          {image.body}
+        </Box>
+      }
+      after={
+        canEdit ? (
+          <Box shrink="No" alignItems="Center" gap="200">
+            <Chip
+              variant={deleted ? 'Critical' : 'Secondary'}
+              fill="None"
+              radii="Pill"
+              onClick={() => onDeleteToggle?.(defaultShortcode)}
+            >
+              {deleted ? <Text size="B300">Undo</Text> : <Icon size="50" src={Icons.Delete} />}
+            </Chip>
+            {!deleted && (
+              <Chip
+                variant="Secondary"
+                radii="Pill"
+                onClick={() => onEdit?.(defaultShortcode, image)}
+              >
+                <Text size="B300">Edit</Text>
+              </Chip>
+            )}
+          </Box>
+        ) : undefined
+      }
+    />
+  );
+}
+
+type ImageTileUploadProps = {
+  file: File;
+  children: (uploadAtom: TUploadAtom) => ReactNode;
+};
+export function ImageTileUpload({ file, children }: ImageTileUploadProps) {
+  const url = useObjectURL(file);
+  const uploadAtom = useMemo(() => createUploadAtom(file), [file]);
+
+  return (
+    <SettingTile before={<img className={css.ImagePackImage} src={url} alt={file.name} />}>
+      {children(uploadAtom)}
+    </SettingTile>
+  );
+}
+
+type ImageTileEditProps = {
+  defaultShortcode: string;
+  useAuthentication: boolean;
+  packUsage: ImageUsage[];
+  image: PackImageReader;
+  onCancel: (shortcode: string) => void;
+  onSave: (shortcode: string, image: PackImageReader) => void;
+};
+export function ImageTileEdit({
+  defaultShortcode,
+  useAuthentication,
+  packUsage,
+  image,
+  onCancel,
+  onSave,
+}: ImageTileEditProps) {
+  const mx = useMatrixClient();
+  const defaultUsage = image.usage ?? packUsage;
+
+  const [unsavedUsage, setUnsavedUsages] = useState(defaultUsage);
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const shortcodeInput = target?.shortcodeInput as HTMLInputElement | undefined;
+    const bodyInput = target?.bodyInput as HTMLTextAreaElement | undefined;
+    if (!shortcodeInput || !bodyInput) return;
+
+    const shortcode = replaceSpaceWithDash(shortcodeInput.value.trim());
+    const body = bodyInput.value.trim() || undefined;
+    const usage = unsavedUsage;
+
+    if (!shortcode) return;
+
+    if (
+      shortcode === image.shortcode &&
+      body === image.body &&
+      imageUsageEqual(usage, defaultUsage)
+    ) {
+      onCancel(defaultShortcode);
+      return;
+    }
+
+    const imageReader = new PackImageReader(shortcode, image.url, {
+      info: image.info,
+      body,
+      usage: imageUsageEqual(usage, packUsage) ? undefined : usage,
+    });
+
+    onSave(defaultShortcode, imageReader);
+  };
+
+  return (
+    <SettingTile
+      before={
+        <img
+          className={css.ImagePackImage}
+          src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? ''}
+          alt={image.shortcode}
+          loading="lazy"
+        />
+      }
+    >
+      <Box as="form" onSubmit={handleSubmit} direction="Column" gap="200">
+        <Box direction="Column" className={css.ImagePackImageInputs}>
+          <Input
+            before={<Text size="L400">Shortcode:</Text>}
+            defaultValue={image.shortcode}
+            name="shortcodeInput"
+            variant="Secondary"
+            size="300"
+            radii="0"
+            required
+            autoFocus
+          />
+          <Input
+            before={<Text size="L400">Body:</Text>}
+            defaultValue={image.body}
+            name="bodyInput"
+            variant="Secondary"
+            size="300"
+            radii="0"
+          />
+        </Box>
+        <Box gap="200">
+          <Box shrink="No" direction="Column">
+            <UsageSwitcher usage={unsavedUsage} onChange={setUnsavedUsages} canEdit />
+          </Box>
+          <Box grow="Yes" />
+          <Button type="submit" variant="Success" size="300" radii="300">
+            <Text size="B300">Save</Text>
+          </Button>
+          <Button
+            type="reset"
+            variant="Secondary"
+            fill="Soft"
+            size="300"
+            radii="300"
+            onClick={() => onCancel(defaultShortcode)}
+          >
+            <Text size="B300">Cancel</Text>
+          </Button>
+        </Box>
+      </Box>
+    </SettingTile>
+  );
+}
diff --git a/src/app/components/image-pack-view/PackMeta.tsx b/src/app/components/image-pack-view/PackMeta.tsx
new file mode 100644 (file)
index 0000000..f091f30
--- /dev/null
@@ -0,0 +1,232 @@
+import React, { FormEventHandler, useCallback, useMemo, useState } from 'react';
+import {
+  Box,
+  Text,
+  Avatar,
+  AvatarImage,
+  AvatarFallback,
+  Button,
+  Icon,
+  Icons,
+  Input,
+  TextArea,
+  Chip,
+} from 'folds';
+import Linkify from 'linkify-react';
+import { mxcUrlToHttp } from '../../utils/matrix';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { nameInitials } from '../../utils/common';
+import { BreakWord } from '../../styles/Text.css';
+import { LINKIFY_OPTS } from '../../plugins/react-custom-html-parser';
+import { ContainerColor } from '../../styles/ContainerColor.css';
+import { useFilePicker } from '../../hooks/useFilePicker';
+import { useObjectURL } from '../../hooks/useObjectURL';
+import { createUploadAtom, UploadSuccess } from '../../state/upload';
+import { CompactUploadCardRenderer } from '../upload-card';
+import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { PackMetaReader } from '../../plugins/custom-emoji';
+
+type ImagePackAvatarProps = {
+  url?: string;
+  name?: string;
+};
+function ImagePackAvatar({ url, name }: ImagePackAvatarProps) {
+  return (
+    <Avatar size="500" className={ContainerColor({ variant: 'Secondary' })}>
+      {url ? (
+        <AvatarImage src={url} alt={name ?? 'Unknown'} />
+      ) : (
+        <AvatarFallback>
+          <Text size="H2">{nameInitials(name ?? 'Unknown')}</Text>
+        </AvatarFallback>
+      )}
+    </Avatar>
+  );
+}
+
+type ImagePackProfileProps = {
+  meta: PackMetaReader;
+  canEdit?: boolean;
+  onEdit?: () => void;
+};
+export function ImagePackProfile({ meta, canEdit, onEdit }: ImagePackProfileProps) {
+  const mx = useMatrixClient();
+  const useAuthentication = useMediaAuthentication();
+  const avatarUrl = meta.avatar
+    ? mxcUrlToHttp(mx, meta.avatar, useAuthentication) ?? undefined
+    : undefined;
+
+  return (
+    <Box gap="400">
+      <Box grow="Yes" direction="Column" gap="300">
+        <Box direction="Column" gap="100">
+          <Text className={BreakWord} size="H5">
+            {meta.name ?? 'Unknown'}
+          </Text>
+          {meta.attribution && (
+            <Text className={BreakWord} size="T200">
+              <Linkify options={LINKIFY_OPTS}>{meta.attribution}</Linkify>
+            </Text>
+          )}
+        </Box>
+        {canEdit && (
+          <Box gap="200">
+            <Chip
+              variant="Secondary"
+              fill="Soft"
+              radii="300"
+              before={<Icon size="50" src={Icons.Pencil} />}
+              onClick={onEdit}
+              outlined
+            >
+              <Text size="B300">Edit</Text>
+            </Chip>
+          </Box>
+        )}
+      </Box>
+      <Box shrink="No">
+        <ImagePackAvatar url={avatarUrl} name={meta.name} />
+      </Box>
+    </Box>
+  );
+}
+
+type ImagePackProfileEditProps = {
+  meta: PackMetaReader;
+  onCancel: () => void;
+  onSave: (meta: PackMetaReader) => void;
+};
+export function ImagePackProfileEdit({ meta, onCancel, onSave }: ImagePackProfileEditProps) {
+  const mx = useMatrixClient();
+  const useAuthentication = useMediaAuthentication();
+  const [avatar, setAvatar] = useState(meta.avatar);
+
+  const avatarUrl = avatar ? mxcUrlToHttp(mx, avatar, useAuthentication) ?? undefined : undefined;
+
+  const [imageFile, setImageFile] = useState<File>();
+  const avatarFileUrl = useObjectURL(imageFile);
+  const uploadingAvatar = avatarFileUrl ? avatar === meta.avatar : false;
+  const uploadAtom = useMemo(() => {
+    if (imageFile) return createUploadAtom(imageFile);
+    return undefined;
+  }, [imageFile]);
+
+  const pickFile = useFilePicker(setImageFile, false);
+
+  const handleRemoveUpload = useCallback(() => {
+    setImageFile(undefined);
+    setAvatar(meta.avatar);
+  }, [meta.avatar]);
+
+  const handleUploaded = useCallback((upload: UploadSuccess) => {
+    setAvatar(upload.mxc);
+  }, []);
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+    if (uploadingAvatar) return;
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const nameInput = target?.nameInput as HTMLInputElement | undefined;
+    const attributionTextArea = target?.attributionTextArea as HTMLTextAreaElement | undefined;
+    if (!nameInput || !attributionTextArea) return;
+
+    const name = nameInput.value.trim();
+    const attribution = attributionTextArea.value.trim();
+    if (!name) return;
+
+    const metaReader = new PackMetaReader({
+      avatar_url: avatar,
+      display_name: name,
+      attribution,
+    });
+    onSave(metaReader);
+  };
+
+  return (
+    <Box as="form" onSubmit={handleSubmit} direction="Column" gap="400">
+      <Box gap="400">
+        <Box grow="Yes" direction="Column" gap="100">
+          <Text size="L400">Pack Avatar</Text>
+          {uploadAtom ? (
+            <Box gap="200" direction="Column">
+              <CompactUploadCardRenderer
+                uploadAtom={uploadAtom}
+                onRemove={handleRemoveUpload}
+                onComplete={handleUploaded}
+              />
+            </Box>
+          ) : (
+            <Box gap="200">
+              <Button
+                type="button"
+                size="300"
+                variant="Secondary"
+                fill="Soft"
+                radii="300"
+                onClick={() => pickFile('image/*')}
+              >
+                <Text size="B300">Upload</Text>
+              </Button>
+              {!avatar && meta.avatar && (
+                <Button
+                  type="button"
+                  size="300"
+                  variant="Success"
+                  fill="None"
+                  radii="300"
+                  onClick={() => setAvatar(meta.avatar)}
+                >
+                  <Text size="B300">Reset</Text>
+                </Button>
+              )}
+              {avatar && (
+                <Button
+                  type="button"
+                  size="300"
+                  variant="Critical"
+                  fill="None"
+                  radii="300"
+                  onClick={() => setAvatar(undefined)}
+                >
+                  <Text size="B300">Remove</Text>
+                </Button>
+              )}
+            </Box>
+          )}
+        </Box>
+        <Box shrink="No">
+          <ImagePackAvatar url={avatarFileUrl ?? avatarUrl} name={meta.name} />
+        </Box>
+      </Box>
+      <Box direction="Inherit" gap="100">
+        <Text size="L400">Name</Text>
+        <Input name="nameInput" defaultValue={meta.name} variant="Secondary" radii="300" required />
+      </Box>
+      <Box direction="Inherit" gap="100">
+        <Text size="L400">Attribution</Text>
+        <TextArea
+          name="attributionTextArea"
+          defaultValue={meta.attribution}
+          variant="Secondary"
+          radii="300"
+        />
+      </Box>
+      <Box gap="300">
+        <Button type="submit" variant="Success" size="300" radii="300" disabled={uploadingAvatar}>
+          <Text size="B300">Save</Text>
+        </Button>
+        <Button
+          type="reset"
+          onClick={onCancel}
+          variant="Secondary"
+          fill="Soft"
+          size="300"
+          radii="300"
+        >
+          <Text size="B300">Cancel</Text>
+        </Button>
+      </Box>
+    </Box>
+  );
+}
diff --git a/src/app/components/image-pack-view/RoomImagePack.tsx b/src/app/components/image-pack-view/RoomImagePack.tsx
new file mode 100644 (file)
index 0000000..9dd45c1
--- /dev/null
@@ -0,0 +1,55 @@
+import React, { useCallback, useMemo } from 'react';
+import { Room } from 'matrix-js-sdk';
+import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { ImagePackContent } from './ImagePackContent';
+import { ImagePack, PackContent } from '../../plugins/custom-emoji';
+import { StateEvent } from '../../../types/matrix/room';
+import { useRoomImagePack } from '../../hooks/useImagePacks';
+import { randomStr } from '../../utils/common';
+
+type RoomImagePackProps = {
+  room: Room;
+  stateKey: string;
+};
+
+export function RoomImagePack({ room, stateKey }: RoomImagePackProps) {
+  const mx = useMatrixClient();
+  const userId = mx.getUserId()!;
+  const powerLevels = usePowerLevels(room);
+
+  const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
+  const canEditImagePack = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(userId));
+
+  const fallbackPack = useMemo(() => {
+    const fakePackId = randomStr(4);
+    return new ImagePack(
+      fakePackId,
+      {},
+      {
+        roomId: room.roomId,
+        stateKey,
+      }
+    );
+  }, [room.roomId, stateKey]);
+  const imagePack = useRoomImagePack(room, stateKey) ?? fallbackPack;
+
+  const handleUpdate = useCallback(
+    async (packContent: PackContent) => {
+      const { address } = imagePack;
+      if (!address) return;
+
+      await mx.sendStateEvent(
+        address.roomId,
+        StateEvent.PoniesRoomEmotes,
+        packContent,
+        address.stateKey
+      );
+    },
+    [mx, imagePack]
+  );
+
+  return (
+    <ImagePackContent imagePack={imagePack} canEdit={canEditImagePack} onUpdate={handleUpdate} />
+  );
+}
diff --git a/src/app/components/image-pack-view/UsageSwitcher.tsx b/src/app/components/image-pack-view/UsageSwitcher.tsx
new file mode 100644 (file)
index 0000000..1a1eb43
--- /dev/null
@@ -0,0 +1,116 @@
+import React, { MouseEventHandler, useMemo, useState } from 'react';
+import { Box, Button, config, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text } from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { ImageUsage } from '../../plugins/custom-emoji';
+import { stopPropagation } from '../../utils/keyboard';
+
+export const useUsageStr = (): ((usage: ImageUsage[]) => string) => {
+  const getUsageStr = (usage: ImageUsage[]): string => {
+    const sticker = usage.includes(ImageUsage.Sticker);
+    const emoticon = usage.includes(ImageUsage.Emoticon);
+
+    if (sticker && emoticon) return 'Both';
+    if (sticker) return 'Sticker';
+    if (emoticon) return 'Emoji';
+    return 'Both';
+  };
+  return getUsageStr;
+};
+
+type UsageSelectorProps = {
+  selected: ImageUsage[];
+  onChange: (usage: ImageUsage[]) => void;
+};
+export function UsageSelector({ selected, onChange }: UsageSelectorProps) {
+  const getUsageStr = useUsageStr();
+
+  const selectedUsageStr = getUsageStr(selected);
+  const isSelected = (usage: ImageUsage[]) => getUsageStr(usage) === selectedUsageStr;
+
+  const allUsages: ImageUsage[][] = useMemo(
+    () => [[ImageUsage.Emoticon], [ImageUsage.Sticker], [ImageUsage.Sticker, ImageUsage.Emoticon]],
+    []
+  );
+
+  return (
+    <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+      {allUsages.map((usage) => (
+        <MenuItem
+          key={getUsageStr(usage)}
+          size="300"
+          variant={isSelected(usage) ? 'SurfaceVariant' : 'Surface'}
+          aria-selected={isSelected(usage)}
+          radii="300"
+          onClick={() => onChange(usage)}
+        >
+          <Box grow="Yes">
+            <Text size="T300">{getUsageStr(usage)}</Text>
+          </Box>
+        </MenuItem>
+      ))}
+    </Box>
+  );
+}
+
+type UsageSwitcherProps = {
+  usage: ImageUsage[];
+  canEdit?: boolean;
+  onChange: (usage: ImageUsage[]) => void;
+};
+export function UsageSwitcher({ usage, onChange, canEdit }: UsageSwitcherProps) {
+  const getUsageStr = useUsageStr();
+
+  const [menuCords, setMenuCords] = useState<RectCords>();
+
+  const handleSelectUsage: MouseEventHandler<HTMLButtonElement> = (event) => {
+    setMenuCords(event.currentTarget.getBoundingClientRect());
+  };
+
+  return (
+    <>
+      <Button
+        variant="Secondary"
+        fill="Soft"
+        size="300"
+        radii="300"
+        type="button"
+        outlined
+        aria-disabled={!canEdit}
+        after={canEdit && <Icon src={Icons.ChevronBottom} size="100" />}
+        onClick={canEdit ? handleSelectUsage : undefined}
+      >
+        <Text size="B300">{getUsageStr(usage)}</Text>
+      </Button>
+      <PopOut
+        anchor={menuCords}
+        offset={5}
+        position="Bottom"
+        align="End"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setMenuCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <Menu>
+              <UsageSelector
+                selected={usage}
+                onChange={(usg) => {
+                  setMenuCords(undefined);
+                  onChange(usg);
+                }}
+              />
+            </Menu>
+          </FocusTrap>
+        }
+      />
+    </>
+  );
+}
diff --git a/src/app/components/image-pack-view/UserImagePack.tsx b/src/app/components/image-pack-view/UserImagePack.tsx
new file mode 100644 (file)
index 0000000..4987793
--- /dev/null
@@ -0,0 +1,22 @@
+import React, { useCallback, useMemo } from 'react';
+import { ImagePackContent } from './ImagePackContent';
+import { ImagePack, PackContent } from '../../plugins/custom-emoji';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { AccountDataEvent } from '../../../types/matrix/accountData';
+import { useUserImagePack } from '../../hooks/useImagePacks';
+
+export function UserImagePack() {
+  const mx = useMatrixClient();
+
+  const defaultPack = useMemo(() => new ImagePack(mx.getUserId() ?? '', {}, undefined), [mx]);
+  const imagePack = useUserImagePack();
+
+  const handleUpdate = useCallback(
+    async (packContent: PackContent) => {
+      await mx.setAccountData(AccountDataEvent.PoniesUserEmotes, packContent);
+    },
+    [mx]
+  );
+
+  return <ImagePackContent imagePack={imagePack ?? defaultPack} canEdit onUpdate={handleUpdate} />;
+}
diff --git a/src/app/components/image-pack-view/index.ts b/src/app/components/image-pack-view/index.ts
new file mode 100644 (file)
index 0000000..5b9cd95
--- /dev/null
@@ -0,0 +1 @@
+export * from './ImagePackView';
diff --git a/src/app/components/image-pack-view/style.css.ts b/src/app/components/image-pack-view/style.css.ts
new file mode 100644 (file)
index 0000000..09c2f8e
--- /dev/null
@@ -0,0 +1,37 @@
+import { style } from '@vanilla-extract/css';
+import { color, config, DefaultReset, toRem } from 'folds';
+
+export const ImagePackImage = style([
+  DefaultReset,
+  {
+    width: toRem(36),
+    height: toRem(36),
+    objectFit: 'contain',
+  },
+]);
+
+export const DeleteImageShortcode = style([
+  DefaultReset,
+  {
+    color: color.Critical.Main,
+    textDecoration: 'line-through',
+  },
+]);
+
+export const ImagePackImageInputs = style([
+  DefaultReset,
+  {
+    overflow: 'hidden',
+    borderRadius: config.radii.R300,
+  },
+]);
+
+export const UnsavedMenu = style({
+  position: 'sticky',
+  padding: config.space.S200,
+  paddingLeft: config.space.S400,
+  top: config.space.S400,
+  left: config.space.S400,
+  right: 0,
+  zIndex: 1,
+});
diff --git a/src/app/components/info-card/InfoCard.tsx b/src/app/components/info-card/InfoCard.tsx
new file mode 100644 (file)
index 0000000..2162a10
--- /dev/null
@@ -0,0 +1,53 @@
+import { Box, ContainerColor, Text } from 'folds';
+import React, { ReactNode } from 'react';
+import classNames from 'classnames';
+import { BreakWord } from '../../styles/Text.css';
+import { ContainerColor as ContainerClr } from '../../styles/ContainerColor.css';
+import * as css from './styles.css';
+
+type InfoCardProps = {
+  variant?: ContainerColor;
+  title?: ReactNode;
+  description?: ReactNode;
+  before?: ReactNode;
+  after?: ReactNode;
+  children?: ReactNode;
+};
+export function InfoCard({
+  variant = 'Primary',
+  title,
+  description,
+  before,
+  after,
+  children,
+}: InfoCardProps) {
+  return (
+    <Box
+      direction="Column"
+      className={classNames(css.InfoCard, ContainerClr({ variant }))}
+      gap="300"
+    >
+      <Box gap="200" alignItems="Center">
+        {before && (
+          <Box shrink="No" alignSelf="Start">
+            {before}
+          </Box>
+        )}
+        <Box grow="Yes" direction="Column" gap="100">
+          {title && (
+            <Text size="L400" className={BreakWord}>
+              {title}
+            </Text>
+          )}
+          {description && (
+            <Text size="T200" className={BreakWord}>
+              {description}
+            </Text>
+          )}
+        </Box>
+        {after && <Box shrink="No">{after}</Box>}
+      </Box>
+      {children}
+    </Box>
+  );
+}
diff --git a/src/app/components/info-card/index.ts b/src/app/components/info-card/index.ts
new file mode 100644 (file)
index 0000000..c02f552
--- /dev/null
@@ -0,0 +1 @@
+export * from './InfoCard';
diff --git a/src/app/components/info-card/styles.css.ts b/src/app/components/info-card/styles.css.ts
new file mode 100644 (file)
index 0000000..c852fbc
--- /dev/null
@@ -0,0 +1,10 @@
+import { style } from '@vanilla-extract/css';
+import { config } from 'folds';
+
+export const InfoCard = style([
+  {
+    padding: config.space.S200,
+    borderRadius: config.radii.R300,
+    borderWidth: config.borderWidth.B300,
+  },
+]);
index 2cc4934c92ee53cedef97096ff72fa798499ed18..97ff26f7d1967b954b0681438eeb5941792ee132 100644 (file)
@@ -1,6 +1,7 @@
 import { Box, Icon, IconSrc } from 'folds';
 import React, { ReactNode } from 'react';
 import { CompactLayout, ModernLayout } from '..';
+import { MessageLayout } from '../../../state/settings';
 
 export type EventContentProps = {
   messageLayout: number;
@@ -11,9 +12,9 @@ export type EventContentProps = {
 export function EventContent({ messageLayout, time, iconSrc, content }: EventContentProps) {
   const beforeJSX = (
     <Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
-      {messageLayout === 1 && time}
+      {messageLayout === MessageLayout.Compact && time}
       <Box
-        grow={messageLayout === 1 ? undefined : 'Yes'}
+        grow={messageLayout === MessageLayout.Compact ? undefined : 'Yes'}
         alignItems="Center"
         justifyContent="Center"
       >
@@ -25,11 +26,11 @@ export function EventContent({ messageLayout, time, iconSrc, content }: EventCon
   const msgContentJSX = (
     <Box justifyContent="SpaceBetween" alignItems="Baseline" gap="200">
       {content}
-      {messageLayout !== 1 && time}
+      {messageLayout !== MessageLayout.Compact && time}
     </Box>
   );
 
-  return messageLayout === 1 ? (
+  return messageLayout === MessageLayout.Compact ? (
     <CompactLayout before={beforeJSX}>{msgContentJSX}</CompactLayout>
   ) : (
     <ModernLayout before={beforeJSX}>{msgContentJSX}</ModernLayout>
index c866dab57e73619005d7733288f2c5cdb4080345..ad54c531019bab1a9711cbee9e9e18fad881aa21 100644 (file)
@@ -27,7 +27,6 @@ import {
   getFileNameExt,
   mimeTypeToExt,
 } from '../../../utils/mimeTypes';
-import * as css from './style.css';
 import { stopPropagation } from '../../../utils/keyboard';
 import {
   decryptFile,
@@ -36,6 +35,7 @@ import {
   mxcUrlToHttp,
 } from '../../../utils/matrix';
 import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { ModalWide } from '../../../styles/Modal.css';
 
 const renderErrorButton = (retry: () => void, text: string) => (
   <TooltipProvider
@@ -111,7 +111,7 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
               }}
             >
               <Modal
-                className={css.ModalWide}
+                className={ModalWide}
                 size="500"
                 onContextMenu={(evt: any) => evt.stopPropagation()}
               >
@@ -199,7 +199,7 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
               }}
             >
               <Modal
-                className={css.ModalWide}
+                className={ModalWide}
                 size="500"
                 onContextMenu={(evt: any) => evt.stopPropagation()}
               >
index 0d1d6d3affd388c8ff4114e99f969767b46c0a04..d4241b64c48f99c99c17590ec10c1c4ae4e516fd 100644 (file)
@@ -28,6 +28,7 @@ import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
 import { stopPropagation } from '../../../utils/keyboard';
 import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix';
 import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { ModalWide } from '../../../styles/Modal.css';
 
 type RenderViewerProps = {
   src: string;
@@ -121,7 +122,7 @@ export const ImageContent = as<'div', ImageContentProps>(
                 }}
               >
                 <Modal
-                  className={css.ModalWide}
+                  className={ModalWide}
                   size="500"
                   onContextMenu={(evt: any) => evt.stopPropagation()}
                 >
index 0c496a852f2549a833147e194e6e7dd04be1f966..a2f5b55d8f4a5189128a30693603529f556d7832 100644 (file)
@@ -30,8 +30,3 @@ export const AbsoluteFooter = style([
     right: config.space.S100,
   },
 ]);
-
-export const ModalWide = style({
-  minWidth: '85vw',
-  minHeight: '90vh',
-});
index a8b9ea0446e24fb719f9922d26d4274ce3f88284..55cceceecc6fde22b9a73e1ed2c7dfc3a0f56154 100644 (file)
@@ -27,14 +27,14 @@ export function PageRoot({ nav, children }: PageRootProps) {
 type ClientDrawerLayoutProps = {
   children: ReactNode;
 };
-export function PageNav({ children }: ClientDrawerLayoutProps) {
+export function PageNav({ size, children }: ClientDrawerLayoutProps & css.PageNavVariants) {
   const screenSize = useScreenSizeContext();
   const isMobile = screenSize === ScreenSize.Mobile;
 
   return (
     <Box
       grow={isMobile ? 'Yes' : undefined}
-      className={css.PageNav}
+      className={css.PageNav({ size })}
       shrink={isMobile ? 'Yes' : 'No'}
     >
       <Box grow="Yes" direction="Column">
@@ -44,15 +44,17 @@ export function PageNav({ children }: ClientDrawerLayoutProps) {
   );
 }
 
-export const PageNavHeader = as<'header'>(({ className, ...props }, ref) => (
-  <Header
-    className={classNames(css.PageNavHeader, className)}
-    variant="Background"
-    size="600"
-    {...props}
-    ref={ref}
-  />
-));
+export const PageNavHeader = as<'header', css.PageNavHeaderVariants>(
+  ({ className, outlined, ...props }, ref) => (
+    <Header
+      className={classNames(css.PageNavHeader({ outlined }), className)}
+      variant="Background"
+      size="600"
+      {...props}
+      ref={ref}
+    />
+  )
+);
 
 export function PageNavContent({
   scrollRef,
@@ -88,11 +90,11 @@ export const Page = as<'div'>(({ className, ...props }, ref) => (
 ));
 
 export const PageHeader = as<'div', css.PageHeaderVariants>(
-  ({ className, balance, ...props }, ref) => (
+  ({ className, outlined, balance, ...props }, ref) => (
     <Header
       as="header"
       size="600"
-      className={classNames(css.PageHeader({ balance }), className)}
+      className={classNames(css.PageHeader({ balance, outlined }), className)}
       {...props}
       ref={ref}
     />
index 23f2da4941d51e796e1ccaa6f97e92162ce47d83..0abd4dfa9150cc968c897ef8f3093bf0cecf364e 100644 (file)
@@ -2,30 +2,55 @@ import { style } from '@vanilla-extract/css';
 import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
 import { DefaultReset, color, config, toRem } from 'folds';
 
-export const PageNav = style({
-  width: toRem(256),
+export const PageNav = recipe({
+  variants: {
+    size: {
+      '400': {
+        width: toRem(256),
+      },
+      '300': {
+        width: toRem(222),
+      },
+    },
+  },
+  defaultVariants: {
+    size: '400',
+  },
 });
+export type PageNavVariants = RecipeVariants<typeof PageNav>;
 
-export const PageNavHeader = style({
-  padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
-  flexShrink: 0,
-  borderBottomWidth: 1,
-
-  selectors: {
-    'button&': {
-      cursor: 'pointer',
-    },
-    'button&[aria-pressed=true]': {
-      backgroundColor: color.Background.ContainerActive,
-    },
-    'button&:hover, button&:focus-visible': {
-      backgroundColor: color.Background.ContainerHover,
+export const PageNavHeader = recipe({
+  base: {
+    padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
+    flexShrink: 0,
+    selectors: {
+      'button&': {
+        cursor: 'pointer',
+      },
+      'button&[aria-pressed=true]': {
+        backgroundColor: color.Background.ContainerActive,
+      },
+      'button&:hover, button&:focus-visible': {
+        backgroundColor: color.Background.ContainerHover,
+      },
+      'button&:active': {
+        backgroundColor: color.Background.ContainerActive,
+      },
     },
-    'button&:active': {
-      backgroundColor: color.Background.ContainerActive,
+  },
+
+  variants: {
+    outlined: {
+      true: {
+        borderBottomWidth: 1,
+      },
     },
   },
+  defaultVariants: {
+    outlined: true,
+  },
 });
+export type PageNavHeaderVariants = RecipeVariants<typeof PageNavHeader>;
 
 export const PageNavContent = style({
   minHeight: '100%',
@@ -38,7 +63,6 @@ export const PageHeader = recipe({
   base: {
     paddingLeft: config.space.S400,
     paddingRight: config.space.S200,
-    borderBottomWidth: config.borderWidth.B300,
   },
   variants: {
     balance: {
@@ -46,6 +70,14 @@ export const PageHeader = recipe({
         paddingLeft: config.space.S200,
       },
     },
+    outlined: {
+      true: {
+        borderBottomWidth: config.borderWidth.B300,
+      },
+    },
+  },
+  defaultVariants: {
+    outlined: true,
   },
 });
 export type PageHeaderVariants = RecipeVariants<typeof PageHeader>;
index 184a097c49a4a6477bb4533319ddbbfdc32e36d1..4ffe0d8315afea08d3c76fff6026d1b96dadf904 100644 (file)
@@ -6,7 +6,7 @@ type PasswordInputProps = Omit<ComponentProps<typeof Input>, 'type' | 'size'> &
   size: '400' | '500';
 };
 export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
-  ({ variant, size, style, after, ...props }, ref) => {
+  ({ variant = 'Background', size, style, after, ...props }, ref) => {
     const paddingRight: string = size === '500' ? config.space.S300 : config.space.S200;
 
     return (
diff --git a/src/app/components/password-input/index.ts b/src/app/components/password-input/index.ts
new file mode 100644 (file)
index 0000000..1eed361
--- /dev/null
@@ -0,0 +1 @@
+export * from './PasswordInput';
diff --git a/src/app/components/setting-tile/SettingTile.tsx b/src/app/components/setting-tile/SettingTile.tsx
new file mode 100644 (file)
index 0000000..bcf6334
--- /dev/null
@@ -0,0 +1,32 @@
+import React, { ReactNode } from 'react';
+import { Box, Text } from 'folds';
+import { BreakWord } from '../../styles/Text.css';
+
+type SettingTileProps = {
+  title?: ReactNode;
+  description?: ReactNode;
+  before?: ReactNode;
+  after?: ReactNode;
+  children?: ReactNode;
+};
+export function SettingTile({ title, description, before, after, children }: SettingTileProps) {
+  return (
+    <Box alignItems="Center" gap="300">
+      {before && <Box shrink="No">{before}</Box>}
+      <Box grow="Yes" direction="Column" gap="100">
+        {title && (
+          <Text className={BreakWord} size="T300">
+            {title}
+          </Text>
+        )}
+        {description && (
+          <Text className={BreakWord} size="T200" priority="300">
+            {description}
+          </Text>
+        )}
+        {children}
+      </Box>
+      {after && <Box shrink="No">{after}</Box>}
+    </Box>
+  );
+}
diff --git a/src/app/components/setting-tile/index.ts b/src/app/components/setting-tile/index.ts
new file mode 100644 (file)
index 0000000..49fede7
--- /dev/null
@@ -0,0 +1 @@
+export * from './SettingTile';
diff --git a/src/app/components/uia-stages/PasswordStage.tsx b/src/app/components/uia-stages/PasswordStage.tsx
new file mode 100644 (file)
index 0000000..0164cae
--- /dev/null
@@ -0,0 +1,89 @@
+import { Box, Button, color, config, Dialog, Header, Icon, IconButton, Icons, Text } from 'folds';
+import React, { FormEventHandler } from 'react';
+import { AuthType } from 'matrix-js-sdk';
+import { StageComponentProps } from './types';
+import { ErrorCode } from '../../cs-errorcode';
+import { PasswordInput } from '../password-input';
+
+export function PasswordStage({
+  stageData,
+  submitAuthDict,
+  onCancel,
+  userId,
+}: StageComponentProps & {
+  userId: string;
+}) {
+  const { errorCode, error, session } = stageData;
+
+  const handleFormSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+    const { passwordInput } = evt.target as HTMLFormElement & {
+      passwordInput: HTMLInputElement;
+    };
+    const password = passwordInput.value;
+    if (!password) return;
+    submitAuthDict({
+      type: AuthType.Password,
+      identifier: {
+        type: 'm.id.user',
+        user: userId,
+      },
+      password,
+      session,
+    });
+  };
+
+  return (
+    <Dialog>
+      <Header
+        style={{
+          padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
+        }}
+        variant="Surface"
+        size="500"
+      >
+        <Box grow="Yes">
+          <Text size="H4">Account Password</Text>
+        </Box>
+        <IconButton size="300" onClick={onCancel} radii="300">
+          <Icon src={Icons.Cross} />
+        </IconButton>
+      </Header>
+      <Box
+        as="form"
+        onSubmit={handleFormSubmit}
+        style={{ padding: `0 ${config.space.S400} ${config.space.S400}` }}
+        direction="Column"
+        gap="400"
+      >
+        <Box direction="Column" gap="400">
+          <Text size="T200">
+            To perform this action you need to authenticate yourself by entering you account
+            password.
+          </Text>
+          <Box direction="Column" gap="100">
+            <Text size="L400">Password</Text>
+            <PasswordInput size="400" name="passwordInput" outlined autoFocus required />
+            {errorCode && (
+              <Box alignItems="Center" gap="100" style={{ color: color.Critical.Main }}>
+                <Icon size="50" src={Icons.Warning} filled />
+                <Text size="T200">
+                  <b>
+                    {errorCode === ErrorCode.M_FORBIDDEN
+                      ? 'Invalid Password!'
+                      : `${errorCode}: ${error}`}
+                  </b>
+                </Text>
+              </Box>
+            )}
+          </Box>
+        </Box>
+        <Button variant="Primary" type="submit">
+          <Text as="span" size="B400">
+            Continue
+          </Text>
+        </Button>
+      </Box>
+    </Dialog>
+  );
+}
diff --git a/src/app/components/uia-stages/SSOStage.tsx b/src/app/components/uia-stages/SSOStage.tsx
new file mode 100644 (file)
index 0000000..f85bcb3
--- /dev/null
@@ -0,0 +1,91 @@
+import { Box, Button, color, config, Dialog, Header, Icon, IconButton, Icons, Text } from 'folds';
+import React, { useCallback, useEffect, useState } from 'react';
+import { StageComponentProps } from './types';
+
+export function SSOStage({
+  ssoRedirectURL,
+  stageData,
+  submitAuthDict,
+  onCancel,
+}: StageComponentProps & {
+  ssoRedirectURL: string;
+}) {
+  const { errorCode, error, session } = stageData;
+  const [ssoWindow, setSSOWindow] = useState<Window>();
+
+  const handleSubmit = useCallback(() => {
+    submitAuthDict({
+      session,
+    });
+  }, [submitAuthDict, session]);
+
+  const handleContinue = () => {
+    const w = window.open(ssoRedirectURL, '_blank');
+    setSSOWindow(w ?? undefined);
+  };
+
+  useEffect(() => {
+    const handleMessage = (evt: MessageEvent) => {
+      if (ssoWindow && evt.data === 'authDone' && evt.source === ssoWindow) {
+        ssoWindow.close();
+        setSSOWindow(undefined);
+        handleSubmit();
+      }
+    };
+
+    window.addEventListener('message', handleMessage);
+    return () => {
+      window.removeEventListener('message', handleMessage);
+    };
+  }, [ssoWindow, handleSubmit]);
+
+  return (
+    <Dialog>
+      <Header
+        style={{
+          padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
+        }}
+        variant="Surface"
+        size="500"
+      >
+        <Box grow="Yes">
+          <Text size="H4">SSO Login</Text>
+        </Box>
+        <IconButton size="300" onClick={onCancel} radii="300">
+          <Icon src={Icons.Cross} />
+        </IconButton>
+      </Header>
+      <Box
+        style={{ padding: `0 ${config.space.S400} ${config.space.S400}` }}
+        direction="Column"
+        gap="400"
+      >
+        <Text size="T200">
+          To perform this action you need to authenticate yourself by SSO login.
+        </Text>
+        {errorCode && (
+          <Box alignItems="Center" gap="100" style={{ color: color.Critical.Main }}>
+            <Icon size="50" src={Icons.Warning} filled />
+            <Text size="T200">
+              <b>{`${errorCode}: ${error}`}</b>
+            </Text>
+          </Box>
+        )}
+
+        {ssoWindow ? (
+          <Button variant="Primary" onClick={handleSubmit}>
+            <Text as="span" size="B400">
+              Continue
+            </Text>
+          </Button>
+        ) : (
+          <Button variant="Primary" onClick={handleContinue}>
+            <Text as="span" size="B400">
+              Continue with SSO
+            </Text>
+          </Button>
+        )}
+      </Box>
+    </Dialog>
+  );
+}
index 95c19a79c4986ef1a920c8ffa26cb49f17d2a7ba..c422d988a0f7df6d264a2aa75a75550837cbbbdd 100644 (file)
@@ -1,6 +1,8 @@
 export * from './types';
 export * from './DummyStage';
 export * from './EmailStage';
+export * from './PasswordStage';
 export * from './ReCaptchaStage';
 export * from './RegistrationTokenStage';
+export * from './SSOStage';
 export * from './TermsStage';
diff --git a/src/app/components/upload-card/CompactUploadCardRenderer.tsx b/src/app/components/upload-card/CompactUploadCardRenderer.tsx
new file mode 100644 (file)
index 0000000..998b517
--- /dev/null
@@ -0,0 +1,94 @@
+import React, { useEffect } from 'react';
+import { Chip, Icon, IconButton, Icons, Text, color } from 'folds';
+import { UploadCard, UploadCardError, CompactUploadCardProgress } from './UploadCard';
+import { TUploadAtom, UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { TUploadContent } from '../../utils/matrix';
+import { getFileTypeIcon } from '../../utils/common';
+
+type CompactUploadCardRendererProps = {
+  isEncrypted?: boolean;
+  uploadAtom: TUploadAtom;
+  onRemove: (file: TUploadContent) => void;
+  onComplete?: (upload: UploadSuccess) => void;
+};
+export function CompactUploadCardRenderer({
+  isEncrypted,
+  uploadAtom,
+  onRemove,
+  onComplete,
+}: CompactUploadCardRendererProps) {
+  const mx = useMatrixClient();
+  const { upload, startUpload, cancelUpload } = useBindUploadAtom(mx, uploadAtom, isEncrypted);
+  const { file } = upload;
+
+  if (upload.status === UploadStatus.Idle) startUpload();
+
+  const removeUpload = () => {
+    cancelUpload();
+    onRemove(file);
+  };
+
+  useEffect(() => {
+    if (upload.status === UploadStatus.Success) {
+      onComplete?.(upload);
+    }
+  }, [upload, onComplete]);
+
+  return (
+    <UploadCard
+      compact
+      outlined
+      radii="300"
+      before={<Icon src={getFileTypeIcon(Icons, file.type)} />}
+      after={
+        <>
+          {upload.status === UploadStatus.Error && (
+            <Chip
+              as="button"
+              onClick={startUpload}
+              aria-label="Retry Upload"
+              variant="Critical"
+              radii="Pill"
+              outlined
+            >
+              <Text size="B300">Retry</Text>
+            </Chip>
+          )}
+          <IconButton
+            onClick={removeUpload}
+            aria-label="Cancel Upload"
+            variant="SurfaceVariant"
+            radii="Pill"
+            size="300"
+          >
+            <Icon src={Icons.Cross} size="200" />
+          </IconButton>
+        </>
+      }
+    >
+      {upload.status === UploadStatus.Success ? (
+        <>
+          <Text size="H6" truncate>
+            {file.name}
+          </Text>
+          <Icon style={{ color: color.Success.Main }} src={Icons.Check} size="100" />
+        </>
+      ) : (
+        <>
+          {upload.status === UploadStatus.Idle && (
+            <CompactUploadCardProgress sentBytes={0} totalBytes={file.size} />
+          )}
+          {upload.status === UploadStatus.Loading && (
+            <CompactUploadCardProgress sentBytes={upload.progress.loaded} totalBytes={file.size} />
+          )}
+          {upload.status === UploadStatus.Error && (
+            <UploadCardError>
+              <Text size="T200">{upload.error.message}</Text>
+            </UploadCardError>
+          )}
+        </>
+      )}
+    </UploadCard>
+  );
+}
index 20ac00e3cce7be8340d4575ac9ac3a6ff4b1aeb8..ad3caf10eed8a791be50e8b4670e428174d80da5 100644 (file)
@@ -7,9 +7,21 @@ export const UploadCard = recipe({
     padding: config.space.S300,
     backgroundColor: color.SurfaceVariant.Container,
     color: color.SurfaceVariant.OnContainer,
+    borderColor: color.SurfaceVariant.ContainerLine,
   },
   variants: {
     radii: RadiiVariant,
+    outlined: {
+      true: {
+        borderStyle: 'solid',
+        borderWidth: config.borderWidth.B300,
+      },
+    },
+    compact: {
+      true: {
+        padding: config.space.S100,
+      },
+    },
   },
   defaultVariants: {
     radii: '400',
index ae7b71b888346ea5e501509ef4d89d390a5a943e..1e2edb96fc2307bda1ec02b61042bef39a958ac2 100644 (file)
@@ -12,8 +12,13 @@ type UploadCardProps = {
 };
 
 export const UploadCard = forwardRef<HTMLDivElement, UploadCardProps & css.UploadCardVariant>(
-  ({ before, after, children, bottom, radii }, ref) => (
-    <Box className={css.UploadCard({ radii })} direction="Column" gap="200" ref={ref}>
+  ({ before, after, children, bottom, radii, outlined, compact }, ref) => (
+    <Box
+      className={css.UploadCard({ radii, outlined, compact })}
+      direction="Column"
+      gap="200"
+      ref={ref}
+    >
       <Box alignItems="Center" gap="200">
         {before}
         <Box alignItems="Center" grow="Yes" gap="200">
@@ -33,7 +38,7 @@ type UploadCardProgressProps = {
 
 export function UploadCardProgress({ sentBytes, totalBytes }: UploadCardProgressProps) {
   return (
-    <Box direction="Column" gap="200">
+    <Box grow="Yes" direction="Column" gap="200">
       <ProgressBar variant="Secondary" size="300" min={0} max={totalBytes} value={sentBytes} />
       <Box alignItems="Center" justifyContent="SpaceBetween">
         <Badge variant="Secondary" fill="Solid" radii="Pill">
@@ -49,6 +54,24 @@ export function UploadCardProgress({ sentBytes, totalBytes }: UploadCardProgress
   );
 }
 
+export function CompactUploadCardProgress({ sentBytes, totalBytes }: UploadCardProgressProps) {
+  return (
+    <Box grow="Yes" gap="200" alignItems="Center">
+      <Badge variant="Secondary" fill="Solid" radii="Pill">
+        <Text size="L400">{`${Math.round(percent(0, totalBytes, sentBytes))}%`}</Text>
+      </Badge>
+      <Box grow="Yes" direction="Column">
+        <ProgressBar variant="Secondary" size="300" min={0} max={totalBytes} value={sentBytes} />
+      </Box>
+      <Badge variant="Secondary" fill="Soft" radii="Pill">
+        <Text size="L400">
+          {bytesToSize(sentBytes)} / {bytesToSize(totalBytes)}
+        </Text>
+      </Badge>
+    </Box>
+  );
+}
+
 type UploadCardErrorProps = {
   children: ReactNode;
 };
index 949f5d645a0c3b76737804dafa6ea522d6b233b8..5df68f2a28e771adae864fdbc4d9f49a39be2604 100644 (file)
@@ -1,30 +1,26 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 import { Chip, Icon, IconButton, Icons, Text, color } from 'folds';
 import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
-import { TUploadAtom, UploadStatus, useBindUploadAtom } from '../../state/upload';
+import { TUploadAtom, UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
 import { useMatrixClient } from '../../hooks/useMatrixClient';
 import { TUploadContent } from '../../utils/matrix';
 import { getFileTypeIcon } from '../../utils/common';
 
 type UploadCardRendererProps = {
-  file: TUploadContent;
   isEncrypted?: boolean;
   uploadAtom: TUploadAtom;
   onRemove: (file: TUploadContent) => void;
+  onComplete?: (upload: UploadSuccess) => void;
 };
 export function UploadCardRenderer({
-  file,
   isEncrypted,
   uploadAtom,
   onRemove,
+  onComplete,
 }: UploadCardRendererProps) {
   const mx = useMatrixClient();
-  const { upload, startUpload, cancelUpload } = useBindUploadAtom(
-    mx,
-    file,
-    uploadAtom,
-    isEncrypted
-  );
+  const { upload, startUpload, cancelUpload } = useBindUploadAtom(mx, uploadAtom, isEncrypted);
+  const { file } = upload;
 
   if (upload.status === UploadStatus.Idle) startUpload();
 
@@ -33,6 +29,12 @@ export function UploadCardRenderer({
     onRemove(file);
   };
 
+  useEffect(() => {
+    if (upload.status === UploadStatus.Success) {
+      onComplete?.(upload);
+    }
+  }, [upload, onComplete]);
+
   return (
     <UploadCard
       radii="300"
index 3e7a5e39753f4ec978e6675bde88f4c185b8069e..e014ab8a8ed29b1c9bc1b2c0c957763e550657de 100644 (file)
@@ -1,2 +1,3 @@
 export * from './UploadCard';
 export * from './UploadCardRenderer';
+export * from './CompactUploadCardRenderer';
index c963723508433d5ac86157444a7474922a4a3ec9..1ab669d256f6239c70eaa036ace581845410058c 100644 (file)
@@ -3,7 +3,8 @@ import { Box, Icon, IconButton, Icons, Line, Scroll, config } from 'folds';
 import { useVirtualizer } from '@tanstack/react-virtual';
 import { useAtom, useAtomValue } from 'jotai';
 import { useNavigate } from 'react-router-dom';
-import { IJoinRuleEventContent, JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
+import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
+import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
 import { useSpace } from '../../hooks/useSpace';
 import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../components/page';
 import { HierarchyItem, useSpaceHierarchy } from '../../hooks/useSpaceHierarchy';
@@ -258,7 +259,7 @@ export function Lobby() {
         const joinRuleContent = getStateEvent(
           itemRoom,
           StateEvent.RoomJoinRules
-        )?.getContent<IJoinRuleEventContent>();
+        )?.getContent<RoomJoinRulesEventContent>();
 
         if (joinRuleContent) {
           const allow =
index 4d43c7e964842a7f85cca8819e8ee42c5e6c0997..897cdd4837dbe88074963ad991e4ed2f58afffe8 100644 (file)
@@ -56,7 +56,13 @@ import {
 } from '../../components/editor';
 import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
 import { UseStateProvider } from '../../components/UseStateProvider';
-import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
+import {
+  TUploadContent,
+  encryptFile,
+  getImageInfo,
+  getMxIdLocalPart,
+  mxcUrlToHttp,
+} from '../../utils/matrix';
 import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
 import { useFilePicker } from '../../hooks/useFilePicker';
 import { useFilePasteHandler } from '../../hooks/useFilePasteHandler';
@@ -157,7 +163,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
         const safeFiles = files.map(safeFile);
         const fileItems: TUploadItem[] = [];
 
-        if (mx.isRoomEncrypted(roomId)) {
+        if (room.hasEncryptionStateEvent()) {
           const encryptFiles = fulfilledPromiseSettledResult(
             await Promise.allSettled(safeFiles.map((f) => encryptFile(f)))
           );
@@ -172,7 +178,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
           item: fileItems,
         });
       },
-      [setSelectedFiles, roomId, mx]
+      [setSelectedFiles, room]
     );
     const pickFile = useFilePicker(handleFiles, true);
     const handlePaste = useFilePasteHandler(handleFiles);
@@ -413,7 +419,6 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
                       <UploadCardRenderer
                         // eslint-disable-next-line react/no-array-index-key
                         key={index}
-                        file={fileItem.file}
                         isEncrypted={!!fileItem.encInfo}
                         uploadAtom={roomUploadAtomFamily(fileItem.file)}
                         onRemove={handleRemoveUpload}
index fb743845e59d14784c015277cc92adba88dfba92..38b67baa472c856d2e455d3df7411b6523a041bd 100644 (file)
@@ -85,7 +85,7 @@ import {
   reactionOrEditEvent,
 } from '../../utils/room';
 import { useSetting } from '../../state/hooks/settings';
-import { settingsAtom } from '../../state/settings';
+import { MessageLayout, settingsAtom } from '../../state/settings';
 import { openProfileViewer } from '../../../client/action/navigation';
 import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
 import { Reactions, Message, Event, EncryptedContent } from './message';
@@ -336,7 +336,10 @@ const useTimelinePagination = (
           backwards ? Direction.Backward : Direction.Forward
         ) ?? timelineToPaginate;
       // Decrypt all event ahead of render cycle
-      if (mx.isRoomEncrypted(fetchedTimeline.getRoomId() ?? '')) {
+      const roomId = fetchedTimeline.getRoomId();
+      const room = roomId ? mx.getRoom(roomId) : null;
+
+      if (room?.hasEncryptionStateEvent()) {
         await to(decryptAllTimelineEvent(mx, fetchedTimeline));
       }
 
@@ -421,7 +424,6 @@ const getRoomUnreadInfo = (room: Room, scrollTo = false) => {
 export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
   const mx = useMatrixClient();
   const useAuthentication = useMediaAuthentication();
-  const encryptedRoom = mx.isRoomEncrypted(room.roomId);
   const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
   const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
   const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
@@ -429,7 +431,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
   const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
   const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
   const [encUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
-  const showUrlPreview = encryptedRoom ? encUrlPreview : urlPreview;
+  const showUrlPreview = room.hasEncryptionStateEvent() ? encUrlPreview : urlPreview;
   const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
   const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(room.roomId));
   const powerLevels = usePowerLevelsContext();
@@ -1030,7 +1032,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
                 urlPreview={showUrlPreview}
                 htmlReactParserOptions={htmlReactParserOptions}
                 linkifyOpts={linkifyOpts}
-                outlineAttachment={messageLayout === 2}
+                outlineAttachment={messageLayout === MessageLayout.Bubble}
               />
             )}
           </Message>
@@ -1126,7 +1128,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
                       urlPreview={showUrlPreview}
                       htmlReactParserOptions={htmlReactParserOptions}
                       linkifyOpts={linkifyOpts}
-                      outlineAttachment={messageLayout === 2}
+                      outlineAttachment={messageLayout === MessageLayout.Bubble}
                     />
                   );
                 }
@@ -1211,7 +1213,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
         const highlighted = focusItem?.index === item && focusItem.highlight;
         const parsed = parseMemberEvent(mEvent);
 
-        const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
+        const timeJSX = (
+          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+        );
 
         return (
           <Event
@@ -1244,7 +1248,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
         const senderId = mEvent.getSender() ?? '';
         const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
-        const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
+        const timeJSX = (
+          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+        );
 
         return (
           <Event
@@ -1278,7 +1284,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
         const senderId = mEvent.getSender() ?? '';
         const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
-        const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
+        const timeJSX = (
+          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+        );
 
         return (
           <Event
@@ -1312,7 +1320,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
         const senderId = mEvent.getSender() ?? '';
         const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
-        const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
+        const timeJSX = (
+          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+        );
 
         return (
           <Event
@@ -1348,7 +1358,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
       const senderId = mEvent.getSender() ?? '';
       const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
-      const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
+      const timeJSX = (
+        <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+      );
 
       return (
         <Event
@@ -1389,7 +1401,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
       const senderId = mEvent.getSender() ?? '';
       const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
 
-      const timeJSX = <Time ts={mEvent.getTs()} compact={messageLayout === 1} />;
+      const timeJSX = (
+        <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
+      );
 
       return (
         <Event
@@ -1544,7 +1558,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
             <div
               style={{
                 padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${
-                  messageLayout === 1 ? config.space.S400 : toRem(64)
+                  messageLayout === MessageLayout.Compact ? config.space.S400 : toRem(64)
                 }`,
               }}
             >
@@ -1552,7 +1566,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
             </div>
           )}
           {(canPaginateBack || !rangeAtStart) &&
-            (messageLayout === 1 ? (
+            (messageLayout === MessageLayout.Compact ? (
               <>
                 <MessageBase>
                   <CompactPlaceholder key={getItems().length} />
@@ -1587,7 +1601,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
           {getItems().map(eventRenderer)}
 
           {(!liveTimelineLinked || !rangeAtEnd) &&
-            (messageLayout === 1 ? (
+            (messageLayout === MessageLayout.Compact ? (
               <>
                 <MessageBase ref={observeFrontAnchor}>
                   <CompactPlaceholder key={getItems().length} />
index 21b186422dee13830f289d92172e887ce0cee781..bdf52059406f75b2bcc98c93267205c193108d36 100644 (file)
@@ -716,7 +716,7 @@ export const Message = as<'div', MessageProps>(
     const headerJSX = !collapse && (
       <Box
         gap="300"
-        direction={messageLayout === 1 ? 'RowReverse' : 'Row'}
+        direction={messageLayout === MessageLayout.Compact ? 'RowReverse' : 'Row'}
         justifyContent="SpaceBetween"
         alignItems="Baseline"
         grow="Yes"
@@ -728,12 +728,12 @@ export const Message = as<'div', MessageProps>(
           onContextMenu={onUserClick}
           onClick={onUsernameClick}
         >
-          <Text as="span" size={messageLayout === 2 ? 'T300' : 'T400'} truncate>
+          <Text as="span" size={messageLayout === MessageLayout.Bubble ? 'T300' : 'T400'} truncate>
             <b>{senderDisplayName}</b>
           </Text>
         </Username>
         <Box shrink="No" gap="100">
-          {messageLayout === 0 && hover && (
+          {messageLayout === MessageLayout.Modern && hover && (
             <>
               <Text as="span" size="T200" priority="300">
                 {senderId}
@@ -743,12 +743,12 @@ export const Message = as<'div', MessageProps>(
               </Text>
             </>
           )}
-          <Time ts={mEvent.getTs()} compact={messageLayout === 1} />
+          <Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} />
         </Box>
       </Box>
     );
 
-    const avatarJSX = !collapse && messageLayout !== 1 && (
+    const avatarJSX = !collapse && messageLayout !== MessageLayout.Compact && (
       <AvatarBase>
         <Avatar
           className={css.MessageAvatar}
@@ -1043,18 +1043,18 @@ export const Message = as<'div', MessageProps>(
             </Menu>
           </div>
         )}
-        {messageLayout === 1 && (
+        {messageLayout === MessageLayout.Compact && (
           <CompactLayout before={headerJSX} onContextMenu={handleContextMenu}>
             {msgContentJSX}
           </CompactLayout>
         )}
-        {messageLayout === 2 && (
+        {messageLayout === MessageLayout.Bubble && (
           <BubbleLayout before={avatarJSX} onContextMenu={handleContextMenu}>
             {headerJSX}
             {msgContentJSX}
           </BubbleLayout>
         )}
-        {messageLayout !== 1 && messageLayout !== 2 && (
+        {messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && (
           <ModernLayout before={avatarJSX} onContextMenu={handleContextMenu}>
             {headerJSX}
             {msgContentJSX}
diff --git a/src/app/features/settings/Settings.tsx b/src/app/features/settings/Settings.tsx
new file mode 100644 (file)
index 0000000..0ff1f13
--- /dev/null
@@ -0,0 +1,234 @@
+import React, { useMemo, useState } from 'react';
+import {
+  Avatar,
+  Box,
+  Button,
+  config,
+  Icon,
+  IconButton,
+  Icons,
+  IconSrc,
+  MenuItem,
+  Overlay,
+  OverlayBackdrop,
+  OverlayCenter,
+  Text,
+} from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { General } from './general';
+import { PageNav, PageNavContent, PageNavHeader, PageRoot } from '../../components/page';
+import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
+import { Account } from './account';
+import { useUserProfile } from '../../hooks/useUserProfile';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
+import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { UserAvatar } from '../../components/user-avatar';
+import { nameInitials } from '../../utils/common';
+import { Notifications } from './notifications';
+import { Devices } from './devices';
+import { EmojisStickers } from './emojis-stickers';
+import { DeveloperTools } from './developer-tools';
+import { About } from './about';
+import { UseStateProvider } from '../../components/UseStateProvider';
+import { stopPropagation } from '../../utils/keyboard';
+import { LogoutDialog } from '../../components/LogoutDialog';
+
+export enum SettingsPages {
+  GeneralPage,
+  AccountPage,
+  NotificationPage,
+  DevicesPage,
+  EmojisStickersPage,
+  DeveloperToolsPage,
+  AboutPage,
+}
+
+type SettingsMenuItem = {
+  page: SettingsPages;
+  name: string;
+  icon: IconSrc;
+};
+
+const useSettingsMenuItems = (): SettingsMenuItem[] =>
+  useMemo(
+    () => [
+      {
+        page: SettingsPages.GeneralPage,
+        name: 'General',
+        icon: Icons.Setting,
+      },
+      {
+        page: SettingsPages.AccountPage,
+        name: 'Account',
+        icon: Icons.User,
+      },
+      {
+        page: SettingsPages.NotificationPage,
+        name: 'Notifications',
+        icon: Icons.Bell,
+      },
+      {
+        page: SettingsPages.DevicesPage,
+        name: 'Devices',
+        icon: Icons.Category,
+      },
+      {
+        page: SettingsPages.EmojisStickersPage,
+        name: 'Emojis & Stickers',
+        icon: Icons.Smile,
+      },
+      {
+        page: SettingsPages.DeveloperToolsPage,
+        name: 'Developer Tools',
+        icon: Icons.Terminal,
+      },
+      {
+        page: SettingsPages.AboutPage,
+        name: 'About',
+        icon: Icons.Info,
+      },
+    ],
+    []
+  );
+
+type SettingsProps = {
+  initialPage?: SettingsPages;
+  requestClose: () => void;
+};
+export function Settings({ initialPage, requestClose }: SettingsProps) {
+  const mx = useMatrixClient();
+  const useAuthentication = useMediaAuthentication();
+  const userId = mx.getUserId()!;
+  const profile = useUserProfile(userId);
+  const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
+  const avatarUrl = profile.avatarUrl
+    ? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
+    : undefined;
+
+  const screenSize = useScreenSizeContext();
+  const [activePage, setActivePage] = useState<SettingsPages | undefined>(() => {
+    if (initialPage) return initialPage;
+    return screenSize === ScreenSize.Mobile ? undefined : SettingsPages.GeneralPage;
+  });
+  const menuItems = useSettingsMenuItems();
+
+  const handlePageRequestClose = () => {
+    if (screenSize === ScreenSize.Mobile) {
+      setActivePage(undefined);
+      return;
+    }
+    requestClose();
+  };
+
+  return (
+    <PageRoot
+      nav={
+        screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : (
+          <PageNav size="300">
+            <PageNavHeader outlined={false}>
+              <Box grow="Yes" gap="200">
+                <Avatar size="200" radii="300">
+                  <UserAvatar
+                    userId={userId}
+                    src={avatarUrl}
+                    renderFallback={() => <Text size="H6">{nameInitials(displayName)}</Text>}
+                  />
+                </Avatar>
+                <Text size="H4" truncate>
+                  Settings
+                </Text>
+              </Box>
+              <Box shrink="No">
+                {screenSize === ScreenSize.Mobile && (
+                  <IconButton onClick={requestClose} variant="Background">
+                    <Icon src={Icons.Cross} />
+                  </IconButton>
+                )}
+              </Box>
+            </PageNavHeader>
+            <Box grow="Yes" direction="Column">
+              <PageNavContent>
+                <div style={{ flexGrow: 1 }}>
+                  {menuItems.map((item) => (
+                    <MenuItem
+                      key={item.name}
+                      variant="Background"
+                      radii="400"
+                      aria-pressed={activePage === item.page}
+                      before={<Icon src={item.icon} size="100" filled={activePage === item.page} />}
+                      onClick={() => setActivePage(item.page)}
+                    >
+                      <Text
+                        style={{
+                          fontWeight: activePage === item.page ? config.fontWeight.W600 : undefined,
+                        }}
+                        size="T300"
+                        truncate
+                      >
+                        {item.name}
+                      </Text>
+                    </MenuItem>
+                  ))}
+                </div>
+              </PageNavContent>
+              <Box style={{ padding: config.space.S200 }} shrink="No" direction="Column">
+                <UseStateProvider initial={false}>
+                  {(logout, setLogout) => (
+                    <>
+                      <Button
+                        size="300"
+                        variant="Critical"
+                        fill="None"
+                        radii="Pill"
+                        before={<Icon src={Icons.Power} size="100" />}
+                        onClick={() => setLogout(true)}
+                      >
+                        <Text size="B400">Logout</Text>
+                      </Button>
+                      {logout && (
+                        <Overlay open backdrop={<OverlayBackdrop />}>
+                          <OverlayCenter>
+                            <FocusTrap
+                              focusTrapOptions={{
+                                onDeactivate: () => setLogout(false),
+                                clickOutsideDeactivates: true,
+                                escapeDeactivates: stopPropagation,
+                              }}
+                            >
+                              <LogoutDialog handleClose={() => setLogout(false)} />
+                            </FocusTrap>
+                          </OverlayCenter>
+                        </Overlay>
+                      )}
+                    </>
+                  )}
+                </UseStateProvider>
+              </Box>
+            </Box>
+          </PageNav>
+        )
+      }
+    >
+      {activePage === SettingsPages.GeneralPage && (
+        <General requestClose={handlePageRequestClose} />
+      )}
+      {activePage === SettingsPages.AccountPage && (
+        <Account requestClose={handlePageRequestClose} />
+      )}
+      {activePage === SettingsPages.NotificationPage && (
+        <Notifications requestClose={handlePageRequestClose} />
+      )}
+      {activePage === SettingsPages.DevicesPage && (
+        <Devices requestClose={handlePageRequestClose} />
+      )}
+      {activePage === SettingsPages.EmojisStickersPage && (
+        <EmojisStickers requestClose={handlePageRequestClose} />
+      )}
+      {activePage === SettingsPages.DeveloperToolsPage && (
+        <DeveloperTools requestClose={handlePageRequestClose} />
+      )}
+      {activePage === SettingsPages.AboutPage && <About requestClose={handlePageRequestClose} />}
+    </PageRoot>
+  );
+}
diff --git a/src/app/features/settings/about/About.tsx b/src/app/features/settings/about/About.tsx
new file mode 100644 (file)
index 0000000..5a80e06
--- /dev/null
@@ -0,0 +1,245 @@
+import React from 'react';
+import { Box, Text, IconButton, Icon, Icons, Scroll, Button, config, toRem } from 'folds';
+import { Page, PageContent, PageHeader } from '../../../components/page';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import CinnySVG from '../../../../../public/res/svg/cinny.svg';
+import cons from '../../../../client/state/cons';
+import { clearCacheAndReload } from '../../../../client/initMatrix';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+
+type AboutProps = {
+  requestClose: () => void;
+};
+export function About({ requestClose }: AboutProps) {
+  const mx = useMatrixClient();
+
+  return (
+    <Page>
+      <PageHeader outlined={false}>
+        <Box grow="Yes" gap="200">
+          <Box grow="Yes" alignItems="Center" gap="200">
+            <Text size="H3" truncate>
+              About
+            </Text>
+          </Box>
+          <Box shrink="No">
+            <IconButton onClick={requestClose} variant="Surface">
+              <Icon src={Icons.Cross} />
+            </IconButton>
+          </Box>
+        </Box>
+      </PageHeader>
+      <Box grow="Yes">
+        <Scroll hideTrack visibility="Hover">
+          <PageContent>
+            <Box direction="Column" gap="700">
+              <Box gap="400">
+                <Box shrink="No">
+                  <img
+                    style={{ width: toRem(60), height: toRem(60) }}
+                    src={CinnySVG}
+                    alt="Cinny logo"
+                  />
+                </Box>
+                <Box direction="Column" gap="300">
+                  <Box direction="Column" gap="100">
+                    <Box gap="100" alignItems="End">
+                      <Text size="H3">Cinny</Text>
+                      <Text size="T200">v{cons.version}</Text>
+                    </Box>
+                    <Text>Yet another matrix client.</Text>
+                  </Box>
+
+                  <Box gap="200" wrap="Wrap">
+                    <Button
+                      as="a"
+                      href="https://github.com/cinnyapp/cinny"
+                      rel="noreferrer noopener"
+                      target="_blank"
+                      variant="Secondary"
+                      fill="Soft"
+                      size="300"
+                      radii="300"
+                      before={<Icon src={Icons.Code} size="100" filled />}
+                    >
+                      <Text size="B300">Source Code</Text>
+                    </Button>
+                    <Button
+                      as="a"
+                      href="https://cinny.in/#sponsor"
+                      rel="noreferrer noopener"
+                      target="_blank"
+                      variant="Critical"
+                      fill="Soft"
+                      size="300"
+                      radii="300"
+                      before={<Icon src={Icons.Heart} size="100" filled />}
+                    >
+                      <Text size="B300">Support</Text>
+                    </Button>
+                  </Box>
+                </Box>
+              </Box>
+              <Box direction="Column" gap="100">
+                <Text size="L400">Options</Text>
+                <SequenceCard
+                  className={SequenceCardStyle}
+                  variant="SurfaceVariant"
+                  direction="Column"
+                  gap="400"
+                >
+                  <SettingTile
+                    title="Clear Cache & Reload"
+                    description="Clear all your locally stored data and reload from server."
+                    after={
+                      <Button
+                        onClick={() => clearCacheAndReload(mx)}
+                        variant="Secondary"
+                        fill="Soft"
+                        size="300"
+                        radii="300"
+                        outlined
+                      >
+                        <Text size="B300">Clear Cache</Text>
+                      </Button>
+                    }
+                  />
+                </SequenceCard>
+              </Box>
+              <Box direction="Column" gap="100">
+                <Text size="L400">Credits</Text>
+                <SequenceCard
+                  className={SequenceCardStyle}
+                  variant="SurfaceVariant"
+                  direction="Column"
+                  gap="400"
+                >
+                  <Box
+                    as="ul"
+                    direction="Column"
+                    gap="200"
+                    style={{
+                      margin: 0,
+                      paddingLeft: config.space.S400,
+                    }}
+                  >
+                    <li>
+                      <Text size="T300">
+                        The{' '}
+                        <a
+                          href="https://github.com/matrix-org/matrix-js-sdk"
+                          rel="noreferrer noopener"
+                          target="_blank"
+                        >
+                          matrix-js-sdk
+                        </a>{' '}
+                        is ©{' '}
+                        <a
+                          href="https://matrix.org/foundation"
+                          rel="noreferrer noopener"
+                          target="_blank"
+                        >
+                          The Matrix.org Foundation C.I.C
+                        </a>{' '}
+                        used under the terms of{' '}
+                        <a
+                          href="http://www.apache.org/licenses/LICENSE-2.0"
+                          rel="noreferrer noopener"
+                          target="_blank"
+                        >
+                          Apache 2.0
+                        </a>
+                        .
+                      </Text>
+                    </li>
+                    <li>
+                      <Text size="T300">
+                        The{' '}
+                        <a
+                          href="https://github.com/mozilla/twemoji-colr"
+                          target="_blank"
+                          rel="noreferrer noopener"
+                        >
+                          twemoji-colr
+                        </a>{' '}
+                        font is ©{' '}
+                        <a href="https://mozilla.org/" target="_blank" rel="noreferrer noopener">
+                          Mozilla Foundation
+                        </a>{' '}
+                        used under the terms of{' '}
+                        <a
+                          href="http://www.apache.org/licenses/LICENSE-2.0"
+                          target="_blank"
+                          rel="noreferrer noopener"
+                        >
+                          Apache 2.0
+                        </a>
+                        .
+                      </Text>
+                    </li>
+                    <li>
+                      <Text size="T300">
+                        The{' '}
+                        <a
+                          href="https://twemoji.twitter.com"
+                          target="_blank"
+                          rel="noreferrer noopener"
+                        >
+                          Twemoji
+                        </a>{' '}
+                        emoji art is ©{' '}
+                        <a
+                          href="https://twemoji.twitter.com"
+                          target="_blank"
+                          rel="noreferrer noopener"
+                        >
+                          Twitter, Inc and other contributors
+                        </a>{' '}
+                        used under the terms of{' '}
+                        <a
+                          href="https://creativecommons.org/licenses/by/4.0/"
+                          target="_blank"
+                          rel="noreferrer noopener"
+                        >
+                          CC-BY 4.0
+                        </a>
+                        .
+                      </Text>
+                    </li>
+                    <li>
+                      <Text size="T300">
+                        The{' '}
+                        <a
+                          href="https://material.io/design/sound/sound-resources.html"
+                          target="_blank"
+                          rel="noreferrer noopener"
+                        >
+                          Material sound resources
+                        </a>{' '}
+                        are ©{' '}
+                        <a href="https://google.com" target="_blank" rel="noreferrer noopener">
+                          Google
+                        </a>{' '}
+                        used under the terms of{' '}
+                        <a
+                          href="https://creativecommons.org/licenses/by/4.0/"
+                          target="_blank"
+                          rel="noreferrer noopener"
+                        >
+                          CC-BY 4.0
+                        </a>
+                        .
+                      </Text>
+                    </li>
+                  </Box>
+                </SequenceCard>
+              </Box>
+            </Box>
+          </PageContent>
+        </Scroll>
+      </Box>
+    </Page>
+  );
+}
diff --git a/src/app/features/settings/about/index.ts b/src/app/features/settings/about/index.ts
new file mode 100644 (file)
index 0000000..da0f79e
--- /dev/null
@@ -0,0 +1 @@
+export * from './About';
diff --git a/src/app/features/settings/account/Account.tsx b/src/app/features/settings/account/Account.tsx
new file mode 100644 (file)
index 0000000..ba354af
--- /dev/null
@@ -0,0 +1,428 @@
+import React, {
+  ChangeEventHandler,
+  FormEventHandler,
+  useCallback,
+  useEffect,
+  useMemo,
+  useState,
+} from 'react';
+import {
+  Box,
+  Text,
+  IconButton,
+  Icon,
+  Icons,
+  Scroll,
+  Input,
+  Avatar,
+  Button,
+  Chip,
+  Overlay,
+  OverlayBackdrop,
+  OverlayCenter,
+  Modal,
+  Dialog,
+  Header,
+  config,
+  Spinner,
+} from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { Page, PageContent, PageHeader } from '../../../components/page';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
+import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
+import { UserAvatar } from '../../../components/user-avatar';
+import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { nameInitials } from '../../../utils/common';
+import { copyToClipboard } from '../../../utils/dom';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { useFilePicker } from '../../../hooks/useFilePicker';
+import { useObjectURL } from '../../../hooks/useObjectURL';
+import { stopPropagation } from '../../../utils/keyboard';
+import { ImageEditor } from '../../../components/image-editor';
+import { ModalWide } from '../../../styles/Modal.css';
+import { createUploadAtom, UploadSuccess } from '../../../state/upload';
+import { CompactUploadCardRenderer } from '../../../components/upload-card';
+import { useCapabilities } from '../../../hooks/useCapabilities';
+
+function MatrixId() {
+  const mx = useMatrixClient();
+  const userId = mx.getUserId()!;
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Matrix ID</Text>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title={userId}
+          after={
+            <Chip variant="Secondary" radii="Pill" onClick={() => copyToClipboard(userId)}>
+              <Text size="T200">Copy</Text>
+            </Chip>
+          }
+        />
+      </SequenceCard>
+    </Box>
+  );
+}
+
+type ProfileProps = {
+  profile: UserProfile;
+  userId: string;
+};
+function ProfileAvatar({ profile, userId }: ProfileProps) {
+  const mx = useMatrixClient();
+  const useAuthentication = useMediaAuthentication();
+  const capabilities = useCapabilities();
+  const [alertRemove, setAlertRemove] = useState(false);
+  const disableSetAvatar = capabilities['m.set_avatar_url']?.enabled === false;
+
+  const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
+  const avatarUrl = profile.avatarUrl
+    ? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
+    : undefined;
+
+  const [imageFile, setImageFile] = useState<File>();
+  const imageFileURL = useObjectURL(imageFile);
+  const uploadAtom = useMemo(() => {
+    if (imageFile) return createUploadAtom(imageFile);
+    return undefined;
+  }, [imageFile]);
+
+  const pickFile = useFilePicker(setImageFile, false);
+
+  const handleRemoveUpload = useCallback(() => {
+    setImageFile(undefined);
+  }, []);
+
+  const handleUploaded = useCallback(
+    (upload: UploadSuccess) => {
+      const { mxc } = upload;
+      mx.setAvatarUrl(mxc);
+      handleRemoveUpload();
+    },
+    [mx, handleRemoveUpload]
+  );
+
+  const handleRemoveAvatar = () => {
+    mx.setAvatarUrl('');
+    setAlertRemove(false);
+  };
+
+  return (
+    <SettingTile
+      title={
+        <Text as="span" size="L400">
+          Avatar
+        </Text>
+      }
+      after={
+        <Avatar size="500" radii="300">
+          <UserAvatar
+            userId={userId}
+            src={avatarUrl}
+            renderFallback={() => <Text size="H4">{nameInitials(defaultDisplayName)}</Text>}
+          />
+        </Avatar>
+      }
+    >
+      {uploadAtom ? (
+        <Box gap="200" direction="Column">
+          <CompactUploadCardRenderer
+            uploadAtom={uploadAtom}
+            onRemove={handleRemoveUpload}
+            onComplete={handleUploaded}
+          />
+        </Box>
+      ) : (
+        <Box gap="200">
+          <Button
+            onClick={() => pickFile('image/*')}
+            size="300"
+            variant="Secondary"
+            fill="Soft"
+            outlined
+            radii="300"
+            disabled={disableSetAvatar}
+          >
+            <Text size="B300">Upload</Text>
+          </Button>
+          {avatarUrl && (
+            <Button
+              size="300"
+              variant="Critical"
+              fill="None"
+              radii="300"
+              disabled={disableSetAvatar}
+              onClick={() => setAlertRemove(true)}
+            >
+              <Text size="B300">Remove</Text>
+            </Button>
+          )}
+        </Box>
+      )}
+
+      {imageFileURL && (
+        <Overlay open={false} backdrop={<OverlayBackdrop />}>
+          <OverlayCenter>
+            <FocusTrap
+              focusTrapOptions={{
+                initialFocus: false,
+                onDeactivate: handleRemoveUpload,
+                clickOutsideDeactivates: true,
+                escapeDeactivates: stopPropagation,
+              }}
+            >
+              <Modal className={ModalWide} variant="Surface" size="500">
+                <ImageEditor
+                  name={imageFile?.name ?? 'Unnamed'}
+                  url={imageFileURL}
+                  requestClose={handleRemoveUpload}
+                />
+              </Modal>
+            </FocusTrap>
+          </OverlayCenter>
+        </Overlay>
+      )}
+
+      <Overlay open={alertRemove} backdrop={<OverlayBackdrop />}>
+        <OverlayCenter>
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setAlertRemove(false),
+              clickOutsideDeactivates: true,
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <Dialog variant="Surface">
+              <Header
+                style={{
+                  padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
+                  borderBottomWidth: config.borderWidth.B300,
+                }}
+                variant="Surface"
+                size="500"
+              >
+                <Box grow="Yes">
+                  <Text size="H4">Remove Avatar</Text>
+                </Box>
+                <IconButton size="300" onClick={() => setAlertRemove(false)} radii="300">
+                  <Icon src={Icons.Cross} />
+                </IconButton>
+              </Header>
+              <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+                <Box direction="Column" gap="200">
+                  <Text priority="400">Are you sure you want to remove profile avatar?</Text>
+                </Box>
+                <Button variant="Critical" onClick={handleRemoveAvatar}>
+                  <Text size="B400">Remove</Text>
+                </Button>
+              </Box>
+            </Dialog>
+          </FocusTrap>
+        </OverlayCenter>
+      </Overlay>
+    </SettingTile>
+  );
+}
+
+function ProfileDisplayName({ profile, userId }: ProfileProps) {
+  const mx = useMatrixClient();
+  const capabilities = useCapabilities();
+  const disableSetDisplayname = capabilities['m.set_displayname']?.enabled === false;
+
+  const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
+  const [displayName, setDisplayName] = useState<string>();
+
+  const [changeState, changeDisplayName] = useAsyncCallback(
+    useCallback((name: string) => mx.setDisplayName(name), [mx])
+  );
+  const changingDisplayName = changeState.status === AsyncStatus.Loading;
+
+  useEffect(() => {
+    setDisplayName(defaultDisplayName);
+  }, [defaultDisplayName]);
+
+  const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
+    const name = evt.currentTarget.value;
+    setDisplayName(name);
+  };
+
+  const handleReset = () => {
+    setDisplayName(defaultDisplayName);
+  };
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+    if (changingDisplayName) return;
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const displayNameInput = target?.displayNameInput as HTMLInputElement | undefined;
+    const name = displayNameInput?.value;
+    if (!name) return;
+
+    changeDisplayName(name);
+  };
+
+  const hasChanges = displayName !== defaultDisplayName;
+  return (
+    <SettingTile
+      title={
+        <Text as="span" size="L400">
+          Display Name
+        </Text>
+      }
+    >
+      <Box direction="Column" grow="Yes" gap="100">
+        <Box
+          as="form"
+          onSubmit={handleSubmit}
+          gap="200"
+          aria-disabled={changingDisplayName || disableSetDisplayname}
+        >
+          <Box grow="Yes" direction="Column">
+            <Input
+              required
+              name="displayNameInput"
+              value={displayName}
+              onChange={handleChange}
+              variant="Secondary"
+              radii="300"
+              style={{ paddingRight: config.space.S200 }}
+              readOnly={changingDisplayName || disableSetDisplayname}
+              after={
+                hasChanges &&
+                !changingDisplayName && (
+                  <IconButton
+                    type="reset"
+                    onClick={handleReset}
+                    size="300"
+                    radii="300"
+                    variant="Secondary"
+                  >
+                    <Icon src={Icons.Cross} size="100" />
+                  </IconButton>
+                )
+              }
+            />
+          </Box>
+          <Button
+            size="400"
+            variant={hasChanges ? 'Success' : 'Secondary'}
+            fill={hasChanges ? 'Solid' : 'Soft'}
+            outlined
+            radii="300"
+            disabled={!hasChanges || changingDisplayName}
+            type="submit"
+          >
+            {changingDisplayName && <Spinner variant="Success" fill="Solid" size="300" />}
+            <Text size="B400">Save</Text>
+          </Button>
+        </Box>
+      </Box>
+    </SettingTile>
+  );
+}
+
+function Profile() {
+  const mx = useMatrixClient();
+  const userId = mx.getUserId()!;
+  const profile = useUserProfile(userId);
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Profile</Text>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <ProfileAvatar userId={userId} profile={profile} />
+        <ProfileDisplayName userId={userId} profile={profile} />
+      </SequenceCard>
+    </Box>
+  );
+}
+
+function ContactInformation() {
+  const mx = useMatrixClient();
+  const [threePIdsState, loadThreePIds] = useAsyncCallback(
+    useCallback(() => mx.getThreePids(), [mx])
+  );
+  const threePIds =
+    threePIdsState.status === AsyncStatus.Success ? threePIdsState.data.threepids : undefined;
+
+  const emailIds = threePIds?.filter((id) => id.medium === 'email');
+
+  useEffect(() => {
+    loadThreePIds();
+  }, [loadThreePIds]);
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Contact Information</Text>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile title="Email Address" description="Email address attached to your account.">
+          <Box>
+            {emailIds?.map((email) => (
+              <Chip key={email.address} as="span" variant="Secondary" radii="Pill">
+                <Text size="T200">{email.address}</Text>
+              </Chip>
+            ))}
+          </Box>
+          {/* <Input defaultValue="" variant="Secondary" radii="300" /> */}
+        </SettingTile>
+      </SequenceCard>
+    </Box>
+  );
+}
+
+type AccountProps = {
+  requestClose: () => void;
+};
+export function Account({ requestClose }: AccountProps) {
+  return (
+    <Page>
+      <PageHeader outlined={false}>
+        <Box grow="Yes" gap="200">
+          <Box grow="Yes" alignItems="Center" gap="200">
+            <Text size="H3" truncate>
+              Account
+            </Text>
+          </Box>
+          <Box shrink="No">
+            <IconButton onClick={requestClose} variant="Surface">
+              <Icon src={Icons.Cross} />
+            </IconButton>
+          </Box>
+        </Box>
+      </PageHeader>
+      <Box grow="Yes">
+        <Scroll hideTrack visibility="Hover">
+          <PageContent>
+            <Box direction="Column" gap="700">
+              <Profile />
+              <MatrixId />
+              <ContactInformation />
+            </Box>
+          </PageContent>
+        </Scroll>
+      </Box>
+    </Page>
+  );
+}
diff --git a/src/app/features/settings/account/index.ts b/src/app/features/settings/account/index.ts
new file mode 100644 (file)
index 0000000..4c71833
--- /dev/null
@@ -0,0 +1 @@
+export * from './Account';
diff --git a/src/app/features/settings/developer-tools/AccountDataEditor.tsx b/src/app/features/settings/developer-tools/AccountDataEditor.tsx
new file mode 100644 (file)
index 0000000..52e9870
--- /dev/null
@@ -0,0 +1,211 @@
+import React, {
+  FormEventHandler,
+  KeyboardEventHandler,
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react';
+import {
+  as,
+  Box,
+  Header,
+  Text,
+  Icon,
+  Icons,
+  IconButton,
+  Input,
+  Button,
+  TextArea as TextAreaComponent,
+  color,
+  Spinner,
+} from 'folds';
+import { isKeyHotkey } from 'is-hotkey';
+import { MatrixError } from 'matrix-js-sdk';
+import * as css from './styles.css';
+import { useTextAreaIntentHandler } from '../../../hooks/useTextAreaIntent';
+import { Cursor, Intent, TextArea, TextAreaOperations } from '../../../plugins/text-area';
+import { GetTarget } from '../../../plugins/text-area/type';
+import { syntaxErrorPosition } from '../../../utils/dom';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+
+const EDITOR_INTENT_SPACE_COUNT = 2;
+
+export type AccountDataEditorProps = {
+  type?: string;
+  content?: object;
+  requestClose: () => void;
+};
+
+export const AccountDataEditor = as<'div', AccountDataEditorProps>(
+  ({ type, content, requestClose, ...props }, ref) => {
+    const mx = useMatrixClient();
+    const defaultContent = useMemo(
+      () => JSON.stringify(content, null, EDITOR_INTENT_SPACE_COUNT),
+      [content]
+    );
+    const textAreaRef = useRef<HTMLTextAreaElement>(null);
+    const [jsonError, setJSONError] = useState<SyntaxError>();
+
+    const getTarget: GetTarget = useCallback(() => {
+      const target = textAreaRef.current;
+      if (!target) throw new Error('TextArea element not found!');
+      return target;
+    }, []);
+
+    const { textArea, operations, intent } = useMemo(() => {
+      const ta = new TextArea(getTarget);
+      const op = new TextAreaOperations(getTarget);
+      return {
+        textArea: ta,
+        operations: op,
+        intent: new Intent(EDITOR_INTENT_SPACE_COUNT, ta, op),
+      };
+    }, [getTarget]);
+
+    const intentHandler = useTextAreaIntentHandler(textArea, operations, intent);
+
+    const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (evt) => {
+      intentHandler(evt);
+      if (isKeyHotkey('escape', evt)) {
+        const cursor = Cursor.fromTextAreaElement(getTarget());
+        operations.deselect(cursor);
+      }
+    };
+
+    const [submitState, submit] = useAsyncCallback<object, MatrixError, [string, object]>(
+      useCallback((dataType, data) => mx.setAccountData(dataType, data), [mx])
+    );
+    const submitting = submitState.status === AsyncStatus.Loading;
+
+    const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+      evt.preventDefault();
+      if (submitting) return;
+
+      const target = evt.target as HTMLFormElement | undefined;
+      const typeInput = target?.typeInput as HTMLInputElement | undefined;
+      const contentTextArea = target?.contentTextArea as HTMLTextAreaElement | undefined;
+      if (!typeInput || !contentTextArea) return;
+
+      const typeStr = typeInput.value.trim();
+      const contentStr = contentTextArea.value.trim();
+
+      let parsedContent: object;
+      try {
+        parsedContent = JSON.parse(contentStr);
+      } catch (e) {
+        setJSONError(e as SyntaxError);
+        return;
+      }
+      setJSONError(undefined);
+
+      if (
+        !typeStr ||
+        parsedContent === null ||
+        defaultContent === JSON.stringify(parsedContent, null, EDITOR_INTENT_SPACE_COUNT)
+      ) {
+        return;
+      }
+
+      submit(typeStr, parsedContent);
+    };
+
+    useEffect(() => {
+      if (jsonError) {
+        const errorPosition = syntaxErrorPosition(jsonError) ?? 0;
+        const cursor = new Cursor(errorPosition, errorPosition, 'none');
+        operations.select(cursor);
+        getTarget()?.focus();
+      }
+    }, [jsonError, operations, getTarget]);
+
+    useEffect(() => {
+      if (submitState.status === AsyncStatus.Success) {
+        requestClose();
+      }
+    }, [submitState, requestClose]);
+
+    return (
+      <Box grow="Yes" direction="Column" {...props} ref={ref}>
+        <Header className={css.EditorHeader} size="600">
+          <Box grow="Yes" gap="200">
+            <Box grow="Yes" alignItems="Center" gap="200">
+              <Text size="H3" truncate>
+                Account Data
+              </Text>
+            </Box>
+            <Box shrink="No">
+              <IconButton onClick={requestClose} variant="Surface">
+                <Icon src={Icons.Cross} />
+              </IconButton>
+            </Box>
+          </Box>
+        </Header>
+        <Box
+          as="form"
+          onSubmit={handleSubmit}
+          grow="Yes"
+          className={css.EditorContent}
+          direction="Column"
+          gap="400"
+          aria-disabled={submitting}
+        >
+          <Box shrink="No" direction="Column" gap="100">
+            <Text size="L400">Type</Text>
+            <Box gap="300">
+              <Box grow="Yes" direction="Column">
+                <Input
+                  name="typeInput"
+                  size="400"
+                  readOnly={!!type || submitting}
+                  defaultValue={type}
+                  required
+                />
+              </Box>
+              <Button
+                variant="Primary"
+                size="400"
+                type="submit"
+                disabled={submitting}
+                before={submitting && <Spinner variant="Primary" fill="Solid" size="300" />}
+              >
+                <Text size="B400">Save</Text>
+              </Button>
+            </Box>
+
+            {submitState.status === AsyncStatus.Error && (
+              <Text size="T200" style={{ color: color.Critical.Main }}>
+                <b>{submitState.error.message}</b>
+              </Text>
+            )}
+          </Box>
+          <Box grow="Yes" direction="Column" gap="100">
+            <Box shrink="No">
+              <Text size="L400">JSON Content</Text>
+            </Box>
+            <TextAreaComponent
+              ref={textAreaRef}
+              name="contentTextArea"
+              className={css.EditorTextArea}
+              onKeyDown={handleKeyDown}
+              defaultValue={defaultContent}
+              resize="None"
+              spellCheck="false"
+              required
+              readOnly={submitting}
+            />
+            {jsonError && (
+              <Text size="T200" style={{ color: color.Critical.Main }}>
+                <b>
+                  {jsonError.name}: {jsonError.message}
+                </b>
+              </Text>
+            )}
+          </Box>
+        </Box>
+      </Box>
+    );
+  }
+);
diff --git a/src/app/features/settings/developer-tools/DevelopTools.tsx b/src/app/features/settings/developer-tools/DevelopTools.tsx
new file mode 100644 (file)
index 0000000..081a26e
--- /dev/null
@@ -0,0 +1,302 @@
+import React, { MouseEventHandler, useCallback, useState } from 'react';
+import {
+  Box,
+  Text,
+  IconButton,
+  Icon,
+  Icons,
+  Scroll,
+  Switch,
+  Overlay,
+  OverlayBackdrop,
+  OverlayCenter,
+  Modal,
+  Chip,
+  Button,
+  PopOut,
+  RectCords,
+  Menu,
+  config,
+  MenuItem,
+} from 'folds';
+import { MatrixEvent } from 'matrix-js-sdk';
+import FocusTrap from 'focus-trap-react';
+import { Page, PageContent, PageHeader } from '../../../components/page';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { useAccountDataCallback } from '../../../hooks/useAccountDataCallback';
+import { TextViewer } from '../../../components/text-viewer';
+import { stopPropagation } from '../../../utils/keyboard';
+import { AccountDataEditor } from './AccountDataEditor';
+import { copyToClipboard } from '../../../utils/dom';
+
+function AccountData() {
+  const mx = useMatrixClient();
+  const [view, setView] = useState(false);
+  const [accountData, setAccountData] = useState(() => Array.from(mx.store.accountData.values()));
+  const [selectedEvent, selectEvent] = useState<MatrixEvent>();
+  const [menuCords, setMenuCords] = useState<RectCords>();
+  const [selectedOption, selectOption] = useState<'edit' | 'inspect'>();
+
+  useAccountDataCallback(
+    mx,
+    useCallback(
+      () => setAccountData(Array.from(mx.store.accountData.values())),
+      [mx, setAccountData]
+    )
+  );
+
+  const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    const target = evt.currentTarget;
+    const eventType = target.getAttribute('data-event-type');
+    if (eventType) {
+      const mEvent = accountData.find((mEvt) => mEvt.getType() === eventType);
+      setMenuCords(evt.currentTarget.getBoundingClientRect());
+      selectEvent(mEvent);
+    }
+  };
+
+  const handleMenuClose = () => setMenuCords(undefined);
+
+  const handleEdit = () => {
+    selectOption('edit');
+    setMenuCords(undefined);
+  };
+  const handleInspect = () => {
+    selectOption('inspect');
+    setMenuCords(undefined);
+  };
+  const handleClose = useCallback(() => {
+    selectEvent(undefined);
+    selectOption(undefined);
+  }, []);
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Account Data</Text>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Global"
+          description="Data stored in your global account data."
+          after={
+            <Button
+              onClick={() => setView(!view)}
+              variant="Secondary"
+              fill="Soft"
+              size="300"
+              radii="300"
+              outlined
+              before={
+                <Icon src={view ? Icons.ChevronTop : Icons.ChevronBottom} size="100" filled />
+              }
+            >
+              <Text size="B300">{view ? 'Collapse' : 'Expand'}</Text>
+            </Button>
+          }
+        />
+        {view && (
+          <SettingTile>
+            <Box direction="Column" gap="200">
+              <Text size="L400">Types</Text>
+              <Box gap="200" wrap="Wrap">
+                <Chip
+                  variant="Secondary"
+                  fill="Soft"
+                  radii="Pill"
+                  onClick={handleEdit}
+                  before={<Icon size="50" src={Icons.Plus} />}
+                >
+                  <Text size="T200" truncate>
+                    Add New
+                  </Text>
+                </Chip>
+                {accountData.map((mEvent) => (
+                  <Chip
+                    key={mEvent.getType()}
+                    variant="Secondary"
+                    fill="Soft"
+                    radii="Pill"
+                    aria-pressed={menuCords && selectedEvent?.getType() === mEvent.getType()}
+                    onClick={handleMenu}
+                    data-event-type={mEvent.getType()}
+                  >
+                    <Text size="T200" truncate>
+                      {mEvent.getType()}
+                    </Text>
+                  </Chip>
+                ))}
+              </Box>
+            </Box>
+          </SettingTile>
+        )}
+        <PopOut
+          anchor={menuCords}
+          offset={5}
+          position="Bottom"
+          content={
+            <FocusTrap
+              focusTrapOptions={{
+                initialFocus: false,
+                onDeactivate: handleMenuClose,
+                clickOutsideDeactivates: true,
+                isKeyForward: (evt: KeyboardEvent) =>
+                  evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+                isKeyBackward: (evt: KeyboardEvent) =>
+                  evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+                escapeDeactivates: stopPropagation,
+              }}
+            >
+              <Menu>
+                <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+                  <MenuItem size="300" variant="Surface" radii="300" onClick={handleInspect}>
+                    <Text size="T300">Inspect</Text>
+                  </MenuItem>
+                  <MenuItem size="300" variant="Surface" radii="300" onClick={handleEdit}>
+                    <Text size="T300">Edit</Text>
+                  </MenuItem>
+                </Box>
+              </Menu>
+            </FocusTrap>
+          }
+        />
+      </SequenceCard>
+      {selectedEvent && selectedOption === 'inspect' && (
+        <Overlay open backdrop={<OverlayBackdrop />}>
+          <OverlayCenter>
+            <FocusTrap
+              focusTrapOptions={{
+                initialFocus: false,
+                onDeactivate: handleClose,
+                clickOutsideDeactivates: true,
+                escapeDeactivates: stopPropagation,
+              }}
+            >
+              <Modal variant="Surface" size="500">
+                <TextViewer
+                  name={selectedEvent.getType() ?? 'Source Code'}
+                  langName="json"
+                  text={JSON.stringify(selectedEvent.getContent(), null, 2)}
+                  requestClose={handleClose}
+                />
+              </Modal>
+            </FocusTrap>
+          </OverlayCenter>
+        </Overlay>
+      )}
+      {selectedOption === 'edit' && (
+        <Overlay open backdrop={<OverlayBackdrop />}>
+          <OverlayCenter>
+            <FocusTrap
+              focusTrapOptions={{
+                initialFocus: false,
+                onDeactivate: handleClose,
+                clickOutsideDeactivates: true,
+                escapeDeactivates: stopPropagation,
+              }}
+            >
+              <Modal variant="Surface" size="500">
+                <AccountDataEditor
+                  type={selectedEvent?.getType()}
+                  content={selectedEvent?.getContent()}
+                  requestClose={handleClose}
+                />
+              </Modal>
+            </FocusTrap>
+          </OverlayCenter>
+        </Overlay>
+      )}
+    </Box>
+  );
+}
+
+type DeveloperToolsProps = {
+  requestClose: () => void;
+};
+export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
+  const mx = useMatrixClient();
+  const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
+
+  return (
+    <Page>
+      <PageHeader outlined={false}>
+        <Box grow="Yes" gap="200">
+          <Box grow="Yes" alignItems="Center" gap="200">
+            <Text size="H3" truncate>
+              Developer Tools
+            </Text>
+          </Box>
+          <Box shrink="No">
+            <IconButton onClick={requestClose} variant="Surface">
+              <Icon src={Icons.Cross} />
+            </IconButton>
+          </Box>
+        </Box>
+      </PageHeader>
+      <Box grow="Yes">
+        <Scroll hideTrack visibility="Hover">
+          <PageContent>
+            <Box direction="Column" gap="700">
+              <Box direction="Column" gap="100">
+                <Text size="L400">Options</Text>
+                <SequenceCard
+                  className={SequenceCardStyle}
+                  variant="SurfaceVariant"
+                  direction="Column"
+                  gap="400"
+                >
+                  <SettingTile
+                    title="Enable Developer Tools"
+                    after={
+                      <Switch
+                        variant="Primary"
+                        value={developerTools}
+                        onChange={setDeveloperTools}
+                      />
+                    }
+                  />
+                </SequenceCard>
+                {developerTools && (
+                  <SequenceCard
+                    className={SequenceCardStyle}
+                    variant="SurfaceVariant"
+                    direction="Column"
+                    gap="400"
+                  >
+                    <SettingTile
+                      title="Access Token"
+                      description="Copy access token to clipboard."
+                      after={
+                        <Button
+                          onClick={() =>
+                            copyToClipboard(mx.getAccessToken() ?? '<NO_ACCESS_TOKEN_FOUND>')
+                          }
+                          variant="Secondary"
+                          fill="Soft"
+                          size="300"
+                          radii="300"
+                          outlined
+                        >
+                          <Text size="B300">Copy</Text>
+                        </Button>
+                      }
+                    />
+                  </SequenceCard>
+                )}
+              </Box>
+              {developerTools && <AccountData />}
+            </Box>
+          </PageContent>
+        </Scroll>
+      </Box>
+    </Page>
+  );
+}
diff --git a/src/app/features/settings/developer-tools/index.ts b/src/app/features/settings/developer-tools/index.ts
new file mode 100644 (file)
index 0000000..1fcceff
--- /dev/null
@@ -0,0 +1 @@
+export * from './DevelopTools';
diff --git a/src/app/features/settings/developer-tools/styles.css.ts b/src/app/features/settings/developer-tools/styles.css.ts
new file mode 100644 (file)
index 0000000..9ff0294
--- /dev/null
@@ -0,0 +1,24 @@
+import { style } from '@vanilla-extract/css';
+import { DefaultReset, config } from 'folds';
+
+export const EditorHeader = style([
+  DefaultReset,
+  {
+    paddingLeft: config.space.S400,
+    paddingRight: config.space.S200,
+    borderBottomWidth: config.borderWidth.B300,
+    flexShrink: 0,
+    gap: config.space.S200,
+  },
+]);
+
+export const EditorContent = style([
+  DefaultReset,
+  {
+    padding: config.space.S400,
+  },
+]);
+
+export const EditorTextArea = style({
+  fontFamily: 'monospace',
+});
diff --git a/src/app/features/settings/devices/DeviceTile.tsx b/src/app/features/settings/devices/DeviceTile.tsx
new file mode 100644 (file)
index 0000000..b4bc9fc
--- /dev/null
@@ -0,0 +1,332 @@
+import React, { FormEventHandler, ReactNode, useCallback, useEffect, useState } from 'react';
+import {
+  Box,
+  Text,
+  IconButton,
+  Icon,
+  Icons,
+  Chip,
+  Input,
+  Button,
+  color,
+  Spinner,
+  toRem,
+  Overlay,
+  OverlayBackdrop,
+  OverlayCenter,
+} from 'folds';
+import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api';
+import FocusTrap from 'focus-trap-react';
+import { IMyDevice, MatrixError } from 'matrix-js-sdk';
+import { SettingTile } from '../../../components/setting-tile';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../../utils/time';
+import { BreakWord } from '../../../styles/Text.css';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { LogoutDialog } from '../../../components/LogoutDialog';
+import { stopPropagation } from '../../../utils/keyboard';
+
+export function DeviceTilePlaceholder() {
+  return (
+    <SequenceCard
+      className={SequenceCardStyle}
+      style={{ height: toRem(66) }}
+      variant="SurfaceVariant"
+      direction="Column"
+      gap="400"
+    />
+  );
+}
+
+function DeviceActiveTime({ ts }: { ts: number }) {
+  return (
+    <Text className={BreakWord} size="T200">
+      <Text size="Inherit" as="span" priority="300">
+        {'Last activity: '}
+      </Text>
+      <>
+        {today(ts) && 'Today'}
+        {yesterday(ts) && 'Yesterday'}
+        {!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)}
+      </>
+    </Text>
+  );
+}
+
+function DeviceDetails({ device }: { device: IMyDevice }) {
+  return (
+    <>
+      {typeof device.device_id === 'string' && (
+        <Text className={BreakWord} size="T200" priority="300">
+          Device ID: <i>{device.device_id}</i>
+        </Text>
+      )}
+      {typeof device.last_seen_ip === 'string' && (
+        <Text className={BreakWord} size="T200" priority="300">
+          IP Address: <i>{device.last_seen_ip}</i>
+        </Text>
+      )}
+    </>
+  );
+}
+
+type DeviceKeyDetailsProps = {
+  crypto: CryptoApi;
+};
+export function DeviceKeyDetails({ crypto }: DeviceKeyDetailsProps) {
+  const [keysState, loadKeys] = useAsyncCallback(
+    useCallback(() => {
+      const keys = crypto.getOwnDeviceKeys();
+      return keys;
+    }, [crypto])
+  );
+
+  useEffect(() => {
+    loadKeys();
+  }, [loadKeys]);
+
+  if (keysState.status === AsyncStatus.Error) return null;
+
+  return (
+    <Text className={BreakWord} size="T200" priority="300">
+      Device Key:{' '}
+      <i>{keysState.status === AsyncStatus.Success ? keysState.data.ed25519 : 'loading...'}</i>
+    </Text>
+  );
+}
+
+type DeviceRenameProps = {
+  device: IMyDevice;
+  onCancel: () => void;
+  onRename: () => void;
+  refreshDeviceList: () => Promise<void>;
+};
+function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceRenameProps) {
+  const mx = useMatrixClient();
+
+  const [renameState, rename] = useAsyncCallback<void, MatrixError, [string]>(
+    useCallback(
+      async (name: string) => {
+        await mx.setDeviceDetails(device.device_id, { display_name: name });
+        await refreshDeviceList();
+      },
+      [mx, device.device_id, refreshDeviceList]
+    )
+  );
+
+  const renaming = renameState.status === AsyncStatus.Loading;
+
+  useEffect(() => {
+    if (renameState.status === AsyncStatus.Success) {
+      onRename();
+    }
+  }, [renameState, onRename]);
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+    if (renaming) return;
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const nameInput = target?.nameInput as HTMLInputElement | undefined;
+    if (!nameInput) return;
+    const deviceName = nameInput.value.trim();
+    if (!deviceName || deviceName === device.display_name) return;
+
+    rename(deviceName);
+  };
+
+  return (
+    <Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
+      <Text size="L400">Device Name</Text>
+      <Box gap="200">
+        <Box grow="Yes" direction="Column">
+          <Input
+            name="nameInput"
+            size="300"
+            variant="Secondary"
+            radii="300"
+            defaultValue={device.display_name}
+            autoFocus
+            required
+            readOnly={renaming}
+          />
+        </Box>
+        <Box shrink="No" gap="200">
+          <Button
+            type="submit"
+            size="300"
+            variant="Success"
+            radii="300"
+            fill="Solid"
+            disabled={renaming}
+            before={renaming && <Spinner size="100" variant="Success" fill="Solid" />}
+          >
+            <Text size="B300">Save</Text>
+          </Button>
+          <Button
+            type="button"
+            size="300"
+            variant="Secondary"
+            radii="300"
+            fill="Soft"
+            onClick={onCancel}
+            disabled={renaming}
+          >
+            <Text size="B300">Cancel</Text>
+          </Button>
+        </Box>
+      </Box>
+      {renameState.status === AsyncStatus.Error ? (
+        <Text size="T200" style={{ color: color.Critical.Main }}>
+          {renameState.error.message}
+        </Text>
+      ) : (
+        <Text size="T200">Device names are visible to public.</Text>
+      )}
+    </Box>
+  );
+}
+
+export function DeviceLogoutBtn() {
+  const [prompt, setPrompt] = useState(false);
+
+  const handleClose = () => setPrompt(false);
+
+  return (
+    <>
+      <Chip variant="Secondary" fill="Soft" radii="Pill" onClick={() => setPrompt(true)}>
+        <Text size="B300">Logout</Text>
+      </Chip>
+      {prompt && (
+        <Overlay open backdrop={<OverlayBackdrop />}>
+          <OverlayCenter>
+            <FocusTrap
+              focusTrapOptions={{
+                onDeactivate: handleClose,
+                clickOutsideDeactivates: true,
+                escapeDeactivates: stopPropagation,
+              }}
+            >
+              <LogoutDialog handleClose={handleClose} />
+            </FocusTrap>
+          </OverlayCenter>
+        </Overlay>
+      )}
+    </>
+  );
+}
+
+type DeviceDeleteBtnProps = {
+  deviceId: string;
+  deleted: boolean;
+  onDeleteToggle: (deviceId: string) => void;
+  disabled?: boolean;
+};
+export function DeviceDeleteBtn({
+  deviceId,
+  deleted,
+  onDeleteToggle,
+  disabled,
+}: DeviceDeleteBtnProps) {
+  return deleted ? (
+    <Chip
+      variant="Critical"
+      fill="None"
+      radii="Pill"
+      onClick={() => onDeleteToggle(deviceId)}
+      disabled={disabled}
+    >
+      <Text size="B300">Undo</Text>
+    </Chip>
+  ) : (
+    <Chip
+      variant="Secondary"
+      fill="None"
+      radii="Pill"
+      onClick={() => onDeleteToggle(deviceId)}
+      disabled={disabled}
+    >
+      <Icon size="50" src={Icons.Delete} />
+    </Chip>
+  );
+}
+
+type DeviceTileProps = {
+  device: IMyDevice;
+  deleted?: boolean;
+  refreshDeviceList: () => Promise<void>;
+  disabled?: boolean;
+  options?: ReactNode;
+  children?: ReactNode;
+};
+export function DeviceTile({
+  device,
+  deleted,
+  refreshDeviceList,
+  disabled,
+  options,
+  children,
+}: DeviceTileProps) {
+  const activeTs = device.last_seen_ts;
+  const [details, setDetails] = useState(false);
+  const [edit, setEdit] = useState(false);
+
+  const handleRename = useCallback(() => {
+    setEdit(false);
+  }, []);
+
+  return (
+    <>
+      <SettingTile
+        before={
+          <IconButton
+            variant={deleted ? 'Critical' : 'Secondary'}
+            outlined={deleted}
+            radii="300"
+            onClick={() => setDetails(!details)}
+          >
+            <Icon size="50" src={details ? Icons.ChevronBottom : Icons.ChevronRight} />
+          </IconButton>
+        }
+        after={
+          !edit && (
+            <Box shrink="No" alignItems="Center" gap="200">
+              {options}
+              {!deleted && (
+                <Chip
+                  variant="Secondary"
+                  radii="Pill"
+                  onClick={() => setEdit(true)}
+                  disabled={disabled}
+                >
+                  <Text size="B300">Edit</Text>
+                </Chip>
+              )}
+            </Box>
+          )
+        }
+      >
+        <Text size="T300">{device.display_name ?? device.device_id}</Text>
+        <Box direction="Column">
+          {typeof activeTs === 'number' && <DeviceActiveTime ts={activeTs} />}
+          {details && (
+            <>
+              <DeviceDetails device={device} />
+              {children}
+            </>
+          )}
+        </Box>
+      </SettingTile>
+      {edit && (
+        <DeviceRename
+          device={device}
+          onCancel={() => setEdit(false)}
+          onRename={handleRename}
+          refreshDeviceList={refreshDeviceList}
+        />
+      )}
+    </>
+  );
+}
diff --git a/src/app/features/settings/devices/Devices.tsx b/src/app/features/settings/devices/Devices.tsx
new file mode 100644 (file)
index 0000000..c957ab3
--- /dev/null
@@ -0,0 +1,165 @@
+import React from 'react';
+import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
+import { Page, PageContent, PageHeader } from '../../../components/page';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { useDeviceIds, useDeviceList, useSplitCurrentDevice } from '../../../hooks/useDeviceList';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { LocalBackup } from './LocalBackup';
+import { DeviceLogoutBtn, DeviceKeyDetails, DeviceTile, DeviceTilePlaceholder } from './DeviceTile';
+import { OtherDevices } from './OtherDevices';
+import {
+  DeviceVerificationOptions,
+  EnableVerification,
+  VerificationStatusBadge,
+  VerifyCurrentDeviceTile,
+} from './Verification';
+import {
+  useDeviceVerificationStatus,
+  useUnverifiedDeviceCount,
+  VerificationStatus,
+} from '../../../hooks/useDeviceVerificationStatus';
+import {
+  useSecretStorageDefaultKeyId,
+  useSecretStorageKeyContent,
+} from '../../../hooks/useSecretStorage';
+import { useCrossSigningActive } from '../../../hooks/useCrossSigning';
+import { BackupRestoreTile } from '../../../components/BackupRestore';
+
+function DevicesPlaceholder() {
+  return (
+    <Box direction="Column" gap="100">
+      <DeviceTilePlaceholder />
+      <DeviceTilePlaceholder />
+    </Box>
+  );
+}
+
+type DevicesProps = {
+  requestClose: () => void;
+};
+export function Devices({ requestClose }: DevicesProps) {
+  const mx = useMatrixClient();
+  const crypto = mx.getCrypto();
+  const crossSigningActive = useCrossSigningActive();
+  const [devices, refreshDeviceList] = useDeviceList();
+
+  const [currentDevice, otherDevices] = useSplitCurrentDevice(devices);
+  const verificationStatus = useDeviceVerificationStatus(
+    crypto,
+    mx.getSafeUserId(),
+    currentDevice?.device_id
+  );
+
+  const otherDevicesId = useDeviceIds(otherDevices);
+  const unverifiedDeviceCount = useUnverifiedDeviceCount(
+    crypto,
+    mx.getSafeUserId(),
+    otherDevicesId
+  );
+
+  const defaultSecretStorageKeyId = useSecretStorageDefaultKeyId();
+  const defaultSecretStorageKeyContent = useSecretStorageKeyContent(
+    defaultSecretStorageKeyId ?? ''
+  );
+
+  return (
+    <Page>
+      <PageHeader outlined={false}>
+        <Box grow="Yes" gap="200">
+          <Box grow="Yes" alignItems="Center" gap="200">
+            <Text size="H3" truncate>
+              Devices
+            </Text>
+          </Box>
+          <Box shrink="No">
+            <IconButton onClick={requestClose} variant="Surface">
+              <Icon src={Icons.Cross} />
+            </IconButton>
+          </Box>
+        </Box>
+      </PageHeader>
+      <Box grow="Yes">
+        <Scroll hideTrack visibility="Hover">
+          <PageContent>
+            <Box direction="Column" gap="700">
+              <Box direction="Column" gap="100">
+                <Text size="L400">Security</Text>
+                <SequenceCard
+                  className={SequenceCardStyle}
+                  variant="SurfaceVariant"
+                  direction="Column"
+                  gap="400"
+                >
+                  <SettingTile
+                    title="Device Verification"
+                    description="To verify device identity and grant access to encrypted messages."
+                    after={
+                      <>
+                        <EnableVerification visible={!crossSigningActive} />
+                        {crossSigningActive && (
+                          <Box gap="200" alignItems="Center">
+                            <VerificationStatusBadge
+                              verificationStatus={verificationStatus}
+                              otherUnverifiedCount={unverifiedDeviceCount}
+                            />
+                            <DeviceVerificationOptions />
+                          </Box>
+                        )}
+                      </>
+                    }
+                  />
+                </SequenceCard>
+              </Box>
+              <Box direction="Column" gap="100">
+                <Text size="L400">Current</Text>
+                {currentDevice ? (
+                  <SequenceCard
+                    className={SequenceCardStyle}
+                    variant="SurfaceVariant"
+                    direction="Column"
+                    gap="400"
+                  >
+                    <DeviceTile
+                      device={currentDevice}
+                      refreshDeviceList={refreshDeviceList}
+                      options={<DeviceLogoutBtn />}
+                    >
+                      {crypto && <DeviceKeyDetails crypto={crypto} />}
+                    </DeviceTile>
+                    {crossSigningActive &&
+                      verificationStatus === VerificationStatus.Unverified &&
+                      defaultSecretStorageKeyId &&
+                      defaultSecretStorageKeyContent && (
+                        <VerifyCurrentDeviceTile
+                          secretStorageKeyId={defaultSecretStorageKeyId}
+                          secretStorageKeyContent={defaultSecretStorageKeyContent}
+                        />
+                      )}
+                    {crypto && verificationStatus === VerificationStatus.Verified && (
+                      <BackupRestoreTile crypto={crypto} />
+                    )}
+                  </SequenceCard>
+                ) : (
+                  <DeviceTilePlaceholder />
+                )}
+              </Box>
+              {devices === undefined && <DevicesPlaceholder />}
+              {otherDevices && (
+                <OtherDevices
+                  devices={otherDevices}
+                  refreshDeviceList={refreshDeviceList}
+                  showVerification={
+                    crossSigningActive && verificationStatus === VerificationStatus.Verified
+                  }
+                />
+              )}
+              <LocalBackup />
+            </Box>
+          </PageContent>
+        </Scroll>
+      </Box>
+    </Page>
+  );
+}
diff --git a/src/app/features/settings/devices/LocalBackup.tsx b/src/app/features/settings/devices/LocalBackup.tsx
new file mode 100644 (file)
index 0000000..00128c8
--- /dev/null
@@ -0,0 +1,325 @@
+import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
+import { Box, Button, color, Icon, Icons, Spinner, Text, toRem } from 'folds';
+import FileSaver from 'file-saver';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SettingTile } from '../../../components/setting-tile';
+import { SequenceCardStyle } from '../styles.css';
+import { PasswordInput } from '../../../components/password-input';
+import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { decryptMegolmKeyFile, encryptMegolmKeyFile } from '../../../../util/cryptE2ERoomKeys';
+import { useAlive } from '../../../hooks/useAlive';
+import { useFilePicker } from '../../../hooks/useFilePicker';
+
+function ExportKeys() {
+  const mx = useMatrixClient();
+  const alive = useAlive();
+
+  const [exportState, exportKeys] = useAsyncCallback<void, Error, [string]>(
+    useCallback(
+      async (password) => {
+        const crypto = mx.getCrypto();
+        if (!crypto) throw new Error('Unexpected Error! Crypto module not found!');
+        const keysJSON = await crypto.exportRoomKeysAsJson();
+
+        const encKeys = await encryptMegolmKeyFile(keysJSON, password);
+
+        const blob = new Blob([encKeys], {
+          type: 'text/plain;charset=us-ascii',
+        });
+        FileSaver.saveAs(blob, 'cinny-keys.txt');
+      },
+      [mx]
+    )
+  );
+
+  const exporting = exportState.status === AsyncStatus.Loading;
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+    if (exporting) return;
+
+    const { passwordInput, confirmPasswordInput } = evt.target as HTMLFormElement & {
+      passwordInput: HTMLInputElement;
+      confirmPasswordInput: HTMLInputElement;
+    };
+
+    const password = passwordInput.value;
+    const confirmPassword = confirmPasswordInput.value;
+
+    if (password !== confirmPassword) return;
+
+    exportKeys(password).then(() => {
+      if (alive()) {
+        passwordInput.value = '';
+        confirmPasswordInput.value = '';
+      }
+    });
+  };
+
+  return (
+    <SettingTile>
+      <Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
+        <Box gap="200" alignItems="End">
+          <ConfirmPasswordMatch initialValue>
+            {(match, doMatch, passRef, confPassRef) => (
+              <>
+                <Box grow="Yes" direction="Column" gap="100">
+                  <Text size="L400">New Password</Text>
+                  <PasswordInput
+                    ref={passRef}
+                    name="passwordInput"
+                    size="400"
+                    variant="Secondary"
+                    radii="300"
+                    required
+                    onChange={doMatch}
+                    readOnly={exporting}
+                    autoFocus
+                  />
+                </Box>
+                <Box grow="Yes" direction="Column" gap="100">
+                  <Text size="L400">Confirm Password</Text>
+                  <PasswordInput
+                    ref={confPassRef}
+                    style={{ color: match ? undefined : color.Critical.Main }}
+                    name="confirmPasswordInput"
+                    size="400"
+                    variant="Secondary"
+                    radii="300"
+                    required
+                    onChange={doMatch}
+                    readOnly={exporting}
+                  />
+                </Box>
+              </>
+            )}
+          </ConfirmPasswordMatch>
+          <Button
+            type="submit"
+            size="400"
+            variant="Secondary"
+            fill="Soft"
+            outlined
+            radii="300"
+            disabled={exporting}
+            before={exporting ? <Spinner size="200" variant="Secondary" fill="Soft" /> : undefined}
+          >
+            <Text as="span" size="B400">
+              Export
+            </Text>
+          </Button>
+        </Box>
+        {exportState.status === AsyncStatus.Error && (
+          <Text size="T200" style={{ color: color.Critical.Main }}>
+            <b>{exportState.error.message}</b>
+          </Text>
+        )}
+      </Box>
+    </SettingTile>
+  );
+}
+
+function ExportKeysTile() {
+  const [expand, setExpand] = useState(false);
+
+  return (
+    <>
+      <SettingTile
+        title="Export Messages Data"
+        description="Save password protected copy of encryption data on your device to decrypt messages later."
+        after={
+          <Box>
+            <Button
+              type="button"
+              onClick={() => setExpand(!expand)}
+              size="300"
+              variant="Secondary"
+              fill="Soft"
+              outlined
+              radii="300"
+              before={
+                <Icon size="100" src={expand ? Icons.ChevronTop : Icons.ChevronBottom} filled />
+              }
+            >
+              <Text as="span" size="B300" truncate>
+                {expand ? 'Collapse' : 'Expand'}
+              </Text>
+            </Button>
+          </Box>
+        }
+      />
+      {expand && <ExportKeys />}
+    </>
+  );
+}
+
+type ImportKeysProps = {
+  file: File;
+  onDone?: () => void;
+};
+function ImportKeys({ file, onDone }: ImportKeysProps) {
+  const mx = useMatrixClient();
+  const alive = useAlive();
+
+  const [decryptState, decryptFile] = useAsyncCallback<void, Error, [string]>(
+    useCallback(
+      async (password) => {
+        const crypto = mx.getCrypto();
+        if (!crypto) throw new Error('Unexpected Error! Crypto module not found!');
+
+        const arrayBuffer = await file.arrayBuffer();
+        const keys = await decryptMegolmKeyFile(arrayBuffer, password);
+
+        await crypto.importRoomKeysAsJson(keys);
+      },
+      [file, mx]
+    )
+  );
+
+  const decrypting = decryptState.status === AsyncStatus.Loading;
+
+  useEffect(() => {
+    if (decryptState.status === AsyncStatus.Success) {
+      onDone?.();
+    }
+  }, [onDone, decryptState]);
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+    if (decrypting) return;
+
+    const { passwordInput } = evt.target as HTMLFormElement & {
+      passwordInput: HTMLInputElement;
+    };
+
+    const password = passwordInput.value;
+
+    if (!password) return;
+    decryptFile(password).then(() => {
+      if (alive()) {
+        passwordInput.value = '';
+      }
+    });
+  };
+
+  return (
+    <SettingTile>
+      <Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
+        <Box gap="200" alignItems="End">
+          <Box grow="Yes" direction="Column" gap="100">
+            <Text size="L400">Password</Text>
+            <PasswordInput
+              name="passwordInput"
+              size="400"
+              variant="Secondary"
+              radii="300"
+              required
+              autoFocus
+              readOnly={decrypting}
+            />
+          </Box>
+          <Button
+            type="submit"
+            size="400"
+            variant="Secondary"
+            fill="Soft"
+            outlined
+            radii="300"
+            disabled={decrypting}
+            before={decrypting ? <Spinner size="200" variant="Secondary" fill="Soft" /> : undefined}
+          >
+            <Text as="span" size="B400">
+              Decrypt
+            </Text>
+          </Button>
+        </Box>
+        {decryptState.status === AsyncStatus.Error && (
+          <Text size="T200" style={{ color: color.Critical.Main }}>
+            <b>{decryptState.error.message}</b>
+          </Text>
+        )}
+      </Box>
+    </SettingTile>
+  );
+}
+
+function ImportKeysTile() {
+  const [file, setFile] = useState<File>();
+  const pickFile = useFilePicker(setFile);
+
+  const handleDone = useCallback(() => {
+    setFile(undefined);
+  }, []);
+
+  return (
+    <>
+      <SettingTile
+        title="Import Messages Data"
+        description="Load password protected copy of encryption data from device to decrypt your messages."
+        after={
+          <Box>
+            {file ? (
+              <Button
+                style={{ maxWidth: toRem(200) }}
+                type="button"
+                onClick={() => setFile(undefined)}
+                size="300"
+                variant="Warning"
+                fill="Solid"
+                radii="300"
+                before={<Icon size="100" src={Icons.File} filled />}
+                after={<Icon size="100" src={Icons.Cross} />}
+              >
+                <Text as="span" size="B300" truncate>
+                  {file.name}
+                </Text>
+              </Button>
+            ) : (
+              <Button
+                type="button"
+                onClick={() => pickFile('text/plain')}
+                size="300"
+                variant="Secondary"
+                fill="Soft"
+                outlined
+                radii="300"
+                before={<Icon size="100" src={Icons.ArrowRight} />}
+              >
+                <Text as="span" size="B300">
+                  Import
+                </Text>
+              </Button>
+            )}
+          </Box>
+        }
+      />
+      {file && <ImportKeys file={file} onDone={handleDone} />}
+    </>
+  );
+}
+
+export function LocalBackup() {
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Local Backup</Text>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <ExportKeysTile />
+      </SequenceCard>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <ImportKeysTile />
+      </SequenceCard>
+    </Box>
+  );
+}
diff --git a/src/app/features/settings/devices/OtherDevices.tsx b/src/app/features/settings/devices/OtherDevices.tsx
new file mode 100644 (file)
index 0000000..0d879e5
--- /dev/null
@@ -0,0 +1,187 @@
+import React, { useCallback, useState } from 'react';
+import { Box, Button, config, Menu, Spinner, Text } from 'folds';
+import { AuthDict, IMyDevice, MatrixError } from 'matrix-js-sdk';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { ActionUIA, ActionUIAFlowsLoader } from '../../../components/ActionUIA';
+import { DeviceDeleteBtn, DeviceTile } from './DeviceTile';
+import { AsyncState, AsyncStatus, useAsync } from '../../../hooks/useAsyncCallback';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { useUIAMatrixError } from '../../../hooks/useUIAFlows';
+import { DeviceVerificationStatus } from '../../../components/DeviceVerificationStatus';
+import { VerifyOtherDeviceTile } from './Verification';
+import { VerificationStatus } from '../../../hooks/useDeviceVerificationStatus';
+
+type OtherDevicesProps = {
+  devices: IMyDevice[];
+  refreshDeviceList: () => Promise<void>;
+  showVerification?: boolean;
+};
+export function OtherDevices({ devices, refreshDeviceList, showVerification }: OtherDevicesProps) {
+  const mx = useMatrixClient();
+  const crypto = mx.getCrypto();
+  const [deleted, setDeleted] = useState<Set<string>>(new Set());
+
+  const handleToggleDelete = useCallback((deviceId: string) => {
+    setDeleted((deviceIds) => {
+      const newIds = new Set(deviceIds);
+      if (newIds.has(deviceId)) {
+        newIds.delete(deviceId);
+      } else {
+        newIds.add(deviceId);
+      }
+      return newIds;
+    });
+  }, []);
+
+  const [deleteState, setDeleteState] = useState<AsyncState<void, MatrixError>>({
+    status: AsyncStatus.Idle,
+  });
+
+  const deleteDevices = useAsync(
+    useCallback(
+      async (authDict?: AuthDict) => {
+        await mx.deleteMultipleDevices(Array.from(deleted), authDict);
+      },
+      [mx, deleted]
+    ),
+    useCallback(
+      (state: typeof deleteState) => {
+        if (state.status === AsyncStatus.Success) {
+          setDeleted(new Set());
+          refreshDeviceList();
+        }
+        setDeleteState(state);
+      },
+      [refreshDeviceList]
+    )
+  );
+  const [authData, deleteError] = useUIAMatrixError(
+    deleteState.status === AsyncStatus.Error ? deleteState.error : undefined
+  );
+  const deleting = deleteState.status === AsyncStatus.Loading || authData !== undefined;
+
+  const handleCancelDelete = () => setDeleted(new Set());
+  const handleCancelAuth = useCallback(() => {
+    setDeleteState({ status: AsyncStatus.Idle });
+  }, []);
+
+  return devices.length > 0 ? (
+    <>
+      <Box direction="Column" gap="100">
+        <Text size="L400">Others</Text>
+        {devices
+          .sort((d1, d2) => {
+            if (!d1.last_seen_ts || !d2.last_seen_ts) return 0;
+            return d1.last_seen_ts < d2.last_seen_ts ? 1 : -1;
+          })
+          .map((device) => (
+            <SequenceCard
+              key={device.device_id}
+              className={SequenceCardStyle}
+              variant={deleted.has(device.device_id) ? 'Critical' : 'SurfaceVariant'}
+              direction="Column"
+              gap="400"
+            >
+              <DeviceTile
+                device={device}
+                deleted={deleted.has(device.device_id)}
+                refreshDeviceList={refreshDeviceList}
+                disabled={deleting}
+                options={
+                  <DeviceDeleteBtn
+                    deviceId={device.device_id}
+                    deleted={deleted.has(device.device_id)}
+                    onDeleteToggle={handleToggleDelete}
+                    disabled={deleting}
+                  />
+                }
+              />
+              {showVerification && crypto && (
+                <DeviceVerificationStatus
+                  crypto={crypto}
+                  userId={mx.getSafeUserId()}
+                  deviceId={device.device_id}
+                >
+                  {(status) =>
+                    status === VerificationStatus.Unverified && (
+                      <VerifyOtherDeviceTile crypto={crypto} deviceId={device.device_id} />
+                    )
+                  }
+                </DeviceVerificationStatus>
+              )}
+            </SequenceCard>
+          ))}
+      </Box>
+      {deleted.size > 0 && (
+        <Menu
+          style={{
+            position: 'sticky',
+            padding: config.space.S200,
+            paddingLeft: config.space.S400,
+            bottom: config.space.S400,
+            left: config.space.S400,
+            right: 0,
+            zIndex: 1,
+          }}
+          variant="Critical"
+        >
+          <Box alignItems="Center" gap="400">
+            <Box grow="Yes" direction="Column">
+              {deleteError ? (
+                <Text size="T200">
+                  <b>Failed to logout devices! Please try again. {deleteError.message}</b>
+                </Text>
+              ) : (
+                <Text size="T200">
+                  <b>Logout from selected devices. ({deleted.size} selected)</b>
+                </Text>
+              )}
+              {authData && (
+                <ActionUIAFlowsLoader
+                  authData={authData}
+                  unsupported={() => (
+                    <Text size="T200">
+                      Authentication steps to perform this action are not supported by client.
+                    </Text>
+                  )}
+                >
+                  {(ongoingFlow) => (
+                    <ActionUIA
+                      authData={authData}
+                      ongoingFlow={ongoingFlow}
+                      action={deleteDevices}
+                      onCancel={handleCancelAuth}
+                    />
+                  )}
+                </ActionUIAFlowsLoader>
+              )}
+            </Box>
+            <Box shrink="No" gap="200">
+              <Button
+                size="300"
+                variant="Critical"
+                fill="None"
+                radii="300"
+                disabled={deleting}
+                onClick={handleCancelDelete}
+              >
+                <Text size="B300">Cancel</Text>
+              </Button>
+              <Button
+                size="300"
+                variant="Critical"
+                radii="300"
+                disabled={deleting}
+                before={deleting && <Spinner variant="Critical" fill="Solid" size="100" />}
+                onClick={() => deleteDevices()}
+              >
+                <Text size="B300">Logout</Text>
+              </Button>
+            </Box>
+          </Box>
+        </Menu>
+      )}
+    </>
+  ) : null;
+}
diff --git a/src/app/features/settings/devices/Verification.tsx b/src/app/features/settings/devices/Verification.tsx
new file mode 100644 (file)
index 0000000..59fa6b6
--- /dev/null
@@ -0,0 +1,335 @@
+import React, { MouseEventHandler, useCallback, useState } from 'react';
+import {
+  Badge,
+  Box,
+  Button,
+  Chip,
+  config,
+  Icon,
+  Icons,
+  Spinner,
+  Text,
+  Overlay,
+  OverlayBackdrop,
+  OverlayCenter,
+  IconButton,
+  RectCords,
+  PopOut,
+  Menu,
+  MenuItem,
+} from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { CryptoApi, VerificationRequest } from 'matrix-js-sdk/lib/crypto-api';
+import { VerificationStatus } from '../../../hooks/useDeviceVerificationStatus';
+import { InfoCard } from '../../../components/info-card';
+import { ManualVerificationTile } from '../../../components/ManualVerification';
+import { SecretStorageKeyContent } from '../../../../types/matrix/accountData';
+import { AsyncState, AsyncStatus, useAsync } from '../../../hooks/useAsyncCallback';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { DeviceVerification } from '../../../components/DeviceVerification';
+import {
+  DeviceVerificationReset,
+  DeviceVerificationSetup,
+} from '../../../components/DeviceVerificationSetup';
+import { stopPropagation } from '../../../utils/keyboard';
+
+type VerificationStatusBadgeProps = {
+  verificationStatus: VerificationStatus;
+  otherUnverifiedCount?: number;
+};
+export function VerificationStatusBadge({
+  verificationStatus,
+  otherUnverifiedCount,
+}: VerificationStatusBadgeProps) {
+  if (
+    verificationStatus === VerificationStatus.Unknown ||
+    typeof otherUnverifiedCount !== 'number'
+  ) {
+    return <Spinner size="400" variant="Secondary" />;
+  }
+  if (verificationStatus === VerificationStatus.Unverified) {
+    return (
+      <Badge variant="Critical" fill="Solid" size="500">
+        <Text size="L400">Unverified</Text>
+      </Badge>
+    );
+  }
+
+  if (otherUnverifiedCount > 0) {
+    return (
+      <Badge variant="Warning" fill="Solid" size="500">
+        <Text size="L400">{otherUnverifiedCount} Unverified</Text>
+      </Badge>
+    );
+  }
+
+  return (
+    <Badge variant="Success" fill="Solid" size="500">
+      <Text size="L400">Verified</Text>
+    </Badge>
+  );
+}
+
+function LearnStartVerificationFromOtherDevice() {
+  return (
+    <Box direction="Column">
+      <Text size="T200">Steps to verify from other device.</Text>
+      <Text as="div" size="T200">
+        <ul style={{ margin: `${config.space.S100} 0` }}>
+          <li>Open your other verified device.</li>
+          <li>
+            Open <i>Settings</i>.
+          </li>
+          <li>
+            Find this device in <i>Devices/Sessions</i> section.
+          </li>
+          <li>Initiate verification.</li>
+        </ul>
+      </Text>
+      <Text size="T200">
+        If you do not have any verified device press the <i>&quot;Verify Manually&quot;</i> button.
+      </Text>
+    </Box>
+  );
+}
+
+type VerifyCurrentDeviceTileProps = {
+  secretStorageKeyId: string;
+  secretStorageKeyContent: SecretStorageKeyContent;
+};
+export function VerifyCurrentDeviceTile({
+  secretStorageKeyId,
+  secretStorageKeyContent,
+}: VerifyCurrentDeviceTileProps) {
+  const [learnMore, setLearnMore] = useState(false);
+
+  const [manualVerification, setManualVerification] = useState(false);
+  const handleCancelVerification = () => setManualVerification(false);
+
+  return (
+    <>
+      <InfoCard
+        variant="Critical"
+        title="Unverified"
+        description={
+          <>
+            Start verification from other device or verify manually.{' '}
+            <Text as="a" size="T200" onClick={() => setLearnMore(!learnMore)}>
+              <b>{learnMore ? 'View Less' : 'Learn More'}</b>
+            </Text>
+          </>
+        }
+        after={
+          !manualVerification && (
+            <Button
+              size="300"
+              variant="Critical"
+              fill="Soft"
+              radii="300"
+              outlined
+              onClick={() => setManualVerification(true)}
+            >
+              <Text as="span" size="B300">
+                Verify Manually
+              </Text>
+            </Button>
+          )
+        }
+      >
+        {learnMore && <LearnStartVerificationFromOtherDevice />}
+      </InfoCard>
+      {manualVerification && (
+        <ManualVerificationTile
+          secretStorageKeyId={secretStorageKeyId}
+          secretStorageKeyContent={secretStorageKeyContent}
+          options={
+            <Chip
+              type="button"
+              variant="Secondary"
+              fill="Soft"
+              radii="Pill"
+              onClick={handleCancelVerification}
+            >
+              <Icon size="100" src={Icons.Cross} />
+            </Chip>
+          }
+        />
+      )}
+    </>
+  );
+}
+
+type VerifyOtherDeviceTileProps = {
+  crypto: CryptoApi;
+  deviceId: string;
+};
+export function VerifyOtherDeviceTile({ crypto, deviceId }: VerifyOtherDeviceTileProps) {
+  const mx = useMatrixClient();
+  const [requestState, setRequestState] = useState<AsyncState<VerificationRequest, Error>>({
+    status: AsyncStatus.Idle,
+  });
+
+  const requestVerification = useAsync<VerificationRequest, Error, []>(
+    useCallback(() => {
+      const requestPromise = crypto.requestDeviceVerification(mx.getSafeUserId(), deviceId);
+      return requestPromise;
+    }, [mx, crypto, deviceId]),
+    setRequestState
+  );
+
+  const handleExit = useCallback(() => {
+    setRequestState({
+      status: AsyncStatus.Idle,
+    });
+  }, []);
+
+  const requesting = requestState.status === AsyncStatus.Loading;
+  return (
+    <InfoCard
+      variant="Warning"
+      title="Unverified"
+      description="Verify device identity and grant access to encrypted messages."
+      after={
+        <Button
+          size="300"
+          variant="Warning"
+          radii="300"
+          onClick={requestVerification}
+          before={requesting && <Spinner size="100" variant="Warning" fill="Solid" />}
+          disabled={requesting}
+        >
+          <Text as="span" size="B300">
+            Verify
+          </Text>
+        </Button>
+      }
+    >
+      {requestState.status === AsyncStatus.Error && (
+        <Text size="T200">{requestState.error.message}</Text>
+      )}
+      {requestState.status === AsyncStatus.Success && (
+        <DeviceVerification request={requestState.data} onExit={handleExit} />
+      )}
+    </InfoCard>
+  );
+}
+
+type EnableVerificationProps = {
+  visible: boolean;
+};
+export function EnableVerification({ visible }: EnableVerificationProps) {
+  const [open, setOpen] = useState(false);
+
+  const handleCancel = useCallback(() => setOpen(false), []);
+
+  return (
+    <>
+      {visible && (
+        <Button size="300" radii="300" onClick={() => setOpen(true)}>
+          <Text as="span" size="B300">
+            Enable
+          </Text>
+        </Button>
+      )}
+      {open && (
+        <Overlay open backdrop={<OverlayBackdrop />}>
+          <OverlayCenter>
+            <FocusTrap
+              focusTrapOptions={{
+                initialFocus: false,
+                clickOutsideDeactivates: false,
+                escapeDeactivates: false,
+              }}
+            >
+              <DeviceVerificationSetup onCancel={handleCancel} />
+            </FocusTrap>
+          </OverlayCenter>
+        </Overlay>
+      )}
+    </>
+  );
+}
+
+export function DeviceVerificationOptions() {
+  const [menuCords, setMenuCords] = useState<RectCords>();
+
+  const [reset, setReset] = useState(false);
+
+  const handleCancelReset = useCallback(() => {
+    setReset(false);
+  }, []);
+
+  const handleMenu: MouseEventHandler<HTMLButtonElement> = (event) => {
+    setMenuCords(event.currentTarget.getBoundingClientRect());
+  };
+
+  const handleReset = () => {
+    setMenuCords(undefined);
+    setReset(true);
+  };
+
+  return (
+    <>
+      <IconButton
+        aria-pressed={!!menuCords}
+        variant="SurfaceVariant"
+        size="300"
+        radii="300"
+        onClick={handleMenu}
+      >
+        <Icon size="100" src={Icons.VerticalDots} />
+      </IconButton>
+      <PopOut
+        anchor={menuCords}
+        offset={5}
+        position="Bottom"
+        align="Center"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setMenuCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <Menu>
+              <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+                <MenuItem
+                  variant="Critical"
+                  onClick={handleReset}
+                  size="300"
+                  radii="300"
+                  fill="None"
+                >
+                  <Text as="span" size="T300" truncate>
+                    Reset
+                  </Text>
+                </MenuItem>
+              </Box>
+            </Menu>
+          </FocusTrap>
+        }
+      />
+      {reset && (
+        <Overlay open backdrop={<OverlayBackdrop />}>
+          <OverlayCenter>
+            <FocusTrap
+              focusTrapOptions={{
+                initialFocus: false,
+                clickOutsideDeactivates: false,
+                escapeDeactivates: false,
+              }}
+            >
+              <DeviceVerificationReset onCancel={handleCancelReset} />
+            </FocusTrap>
+          </OverlayCenter>
+        </Overlay>
+      )}
+    </>
+  );
+}
diff --git a/src/app/features/settings/devices/index.ts b/src/app/features/settings/devices/index.ts
new file mode 100644 (file)
index 0000000..35b5f86
--- /dev/null
@@ -0,0 +1 @@
+export * from './Devices';
diff --git a/src/app/features/settings/emojis-stickers/EmojisStickers.tsx b/src/app/features/settings/emojis-stickers/EmojisStickers.tsx
new file mode 100644 (file)
index 0000000..9371512
--- /dev/null
@@ -0,0 +1,51 @@
+import React, { useState } from 'react';
+import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
+import { Page, PageContent, PageHeader } from '../../../components/page';
+import { GlobalPacks } from './GlobalPacks';
+import { UserPack } from './UserPack';
+import { ImagePack } from '../../../plugins/custom-emoji';
+import { ImagePackView } from '../../../components/image-pack-view';
+
+type EmojisStickersProps = {
+  requestClose: () => void;
+};
+export function EmojisStickers({ requestClose }: EmojisStickersProps) {
+  const [imagePack, setImagePack] = useState<ImagePack>();
+
+  const handleImagePackViewClose = () => {
+    setImagePack(undefined);
+  };
+
+  if (imagePack) {
+    return <ImagePackView address={imagePack.address} requestClose={handleImagePackViewClose} />;
+  }
+
+  return (
+    <Page>
+      <PageHeader outlined={false}>
+        <Box grow="Yes" gap="200">
+          <Box grow="Yes" alignItems="Center" gap="200">
+            <Text size="H3" truncate>
+              Emojis & Stickers
+            </Text>
+          </Box>
+          <Box shrink="No">
+            <IconButton onClick={requestClose} variant="Surface">
+              <Icon src={Icons.Cross} />
+            </IconButton>
+          </Box>
+        </Box>
+      </PageHeader>
+      <Box grow="Yes">
+        <Scroll hideTrack visibility="Hover">
+          <PageContent>
+            <Box direction="Column" gap="700">
+              <UserPack onViewPack={setImagePack} />
+              <GlobalPacks onViewPack={setImagePack} />
+            </Box>
+          </PageContent>
+        </Scroll>
+      </Box>
+    </Page>
+  );
+}
diff --git a/src/app/features/settings/emojis-stickers/GlobalPacks.tsx b/src/app/features/settings/emojis-stickers/GlobalPacks.tsx
new file mode 100644 (file)
index 0000000..3413ec4
--- /dev/null
@@ -0,0 +1,488 @@
+import React, { MouseEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
+import {
+  Box,
+  Text,
+  Button,
+  Icon,
+  Icons,
+  IconButton,
+  Avatar,
+  AvatarImage,
+  AvatarFallback,
+  config,
+  Spinner,
+  Menu,
+  RectCords,
+  PopOut,
+  Checkbox,
+  toRem,
+  Scroll,
+  Header,
+  Line,
+  Chip,
+} from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { useAtomValue } from 'jotai';
+import { Room } from 'matrix-js-sdk';
+import { useGlobalImagePacks, useRoomsImagePacks } from '../../../hooks/useImagePacks';
+import { SequenceCardStyle } from '../styles.css';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SettingTile } from '../../../components/setting-tile';
+import { mxcUrlToHttp } from '../../../utils/matrix';
+import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import {
+  EmoteRoomsContent,
+  ImagePack,
+  ImageUsage,
+  PackAddress,
+  packAddressEqual,
+} from '../../../plugins/custom-emoji';
+import { LineClamp2 } from '../../../styles/Text.css';
+import { allRoomsAtom } from '../../../state/room-list/roomList';
+import { AccountDataEvent } from '../../../../types/matrix/accountData';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { stopPropagation } from '../../../utils/keyboard';
+
+function GlobalPackSelector({
+  packs,
+  useAuthentication,
+  onSelect,
+}: {
+  packs: ImagePack[];
+  useAuthentication: boolean;
+  onSelect: (addresses: PackAddress[]) => void;
+}) {
+  const mx = useMatrixClient();
+  const roomToPacks = useMemo(() => {
+    const rToP = new Map<string, ImagePack[]>();
+    packs
+      .filter((pack) => !pack.deleted)
+      .forEach((pack) => {
+        if (!pack.address) return;
+        const pks = rToP.get(pack.address.roomId) ?? [];
+        pks.push(pack);
+        rToP.set(pack.address.roomId, pks);
+      });
+    return rToP;
+  }, [packs]);
+
+  const [selected, setSelected] = useState<PackAddress[]>([]);
+  const toggleSelect = (address: PackAddress) => {
+    setSelected((addresses) => {
+      const newAddresses = addresses.filter((addr) => !packAddressEqual(addr, address));
+      if (newAddresses.length !== addresses.length) {
+        return newAddresses;
+      }
+      newAddresses.push(address);
+      return newAddresses;
+    });
+  };
+
+  const hasSelected = selected.length > 0;
+  return (
+    <Box grow="Yes" direction="Column">
+      <Header size="400" variant="Surface" style={{ padding: `0 ${config.space.S300}` }}>
+        <Box grow="Yes">
+          <Text size="L400" truncate>
+            Room Packs
+          </Text>
+        </Box>
+        <Box shrink="No">
+          <Chip
+            radii="Pill"
+            variant={hasSelected ? 'Success' : 'SurfaceVariant'}
+            outlined={hasSelected}
+            onClick={() => onSelect(selected)}
+          >
+            <Text size="B300">{hasSelected ? 'Save' : 'Close'}</Text>
+          </Chip>
+        </Box>
+      </Header>
+      <Line variant="Surface" size="300" />
+      <Box grow="Yes">
+        <Scroll size="300" hideTrack visibility="Hover">
+          <Box
+            direction="Column"
+            gap="400"
+            style={{
+              paddingLeft: config.space.S300,
+              paddingTop: config.space.S300,
+              paddingBottom: config.space.S300,
+              paddingRight: config.space.S100,
+            }}
+          >
+            {Array.from(roomToPacks.entries()).map(([roomId, roomPacks]) => {
+              const room = mx.getRoom(roomId);
+              if (!room) return null;
+              return (
+                <Box key={roomId} direction="Column" gap="100">
+                  <Text size="L400">{room.name}</Text>
+                  {roomPacks.map((pack) => {
+                    const avatarMxc = pack.getAvatarUrl(ImageUsage.Emoticon);
+                    const avatarUrl = avatarMxc
+                      ? mxcUrlToHttp(mx, avatarMxc, useAuthentication)
+                      : undefined;
+                    const { address } = pack;
+                    if (!address) return null;
+
+                    const added = selected.find((addr) => packAddressEqual(addr, address));
+                    return (
+                      <SequenceCard
+                        key={pack.id}
+                        className={SequenceCardStyle}
+                        variant={added ? 'Success' : 'SurfaceVariant'}
+                        direction="Column"
+                        gap="400"
+                      >
+                        <SettingTile
+                          title={pack.meta.name ?? 'Unknown'}
+                          description={<span className={LineClamp2}>{pack.meta.attribution}</span>}
+                          before={
+                            <Box alignItems="Center" gap="300">
+                              <Avatar size="300" radii="300">
+                                {avatarUrl ? (
+                                  <AvatarImage style={{ objectFit: 'contain' }} src={avatarUrl} />
+                                ) : (
+                                  <AvatarFallback>
+                                    <Icon size="400" src={Icons.Sticker} filled />
+                                  </AvatarFallback>
+                                )}
+                              </Avatar>
+                            </Box>
+                          }
+                          after={
+                            <Checkbox variant="Success" onClick={() => toggleSelect(address)} />
+                          }
+                        />
+                      </SequenceCard>
+                    );
+                  })}
+                </Box>
+              );
+            })}
+
+            {roomToPacks.size === 0 && (
+              <SequenceCard
+                className={SequenceCardStyle}
+                variant="SurfaceVariant"
+                direction="Column"
+                gap="400"
+              >
+                <Box
+                  justifyContent="Center"
+                  direction="Column"
+                  gap="200"
+                  style={{
+                    padding: `${config.space.S700} ${config.space.S400}`,
+                    maxWidth: toRem(300),
+                    margin: 'auto',
+                  }}
+                >
+                  <Text size="H5" align="Center">
+                    No Packs
+                  </Text>
+                  <Text size="T200" align="Center">
+                    Pack from rooms will appear here. You do not have any room with packs yet.
+                  </Text>
+                </Box>
+              </SequenceCard>
+            )}
+          </Box>
+        </Scroll>
+      </Box>
+    </Box>
+  );
+}
+
+type GlobalPacksProps = {
+  onViewPack: (imagePack: ImagePack) => void;
+};
+export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
+  const mx = useMatrixClient();
+  const useAuthentication = useMediaAuthentication();
+  const globalPacks = useGlobalImagePacks();
+  const [menuCords, setMenuCords] = useState<RectCords>();
+
+  const roomIds = useAtomValue(allRoomsAtom);
+  const rooms = useMemo(() => {
+    const rs: Room[] = [];
+    roomIds.forEach((rId) => {
+      const r = mx.getRoom(rId);
+      if (r) rs.push(r);
+    });
+    return rs;
+  }, [mx, roomIds]);
+  const roomsImagePack = useRoomsImagePacks(rooms);
+  const nonGlobalPacks = useMemo(
+    () =>
+      roomsImagePack.filter(
+        (pack) => !globalPacks.find((p) => packAddressEqual(pack.address, p.address))
+      ),
+    [roomsImagePack, globalPacks]
+  );
+
+  const [selectedPacks, setSelectedPacks] = useState<PackAddress[]>([]);
+  const [removedPacks, setRemovedPacks] = useState<PackAddress[]>([]);
+
+  const unselectedGlobalPacks = useMemo(
+    () =>
+      nonGlobalPacks.filter(
+        (pack) => !selectedPacks.find((addr) => packAddressEqual(pack.address, addr))
+      ),
+    [selectedPacks, nonGlobalPacks]
+  );
+
+  const handleRemove = (address: PackAddress) => {
+    setRemovedPacks((addresses) => [...addresses, address]);
+  };
+
+  const handleUndoRemove = (address: PackAddress) => {
+    setRemovedPacks((addresses) => addresses.filter((addr) => !packAddressEqual(addr, address)));
+  };
+
+  const handleSelected = (addresses: PackAddress[]) => {
+    setMenuCords(undefined);
+    if (addresses.length > 0) {
+      setSelectedPacks((a) => [...addresses, ...a]);
+    }
+  };
+
+  const [applyState, applyChanges] = useAsyncCallback(
+    useCallback(async () => {
+      const content =
+        mx.getAccountData(AccountDataEvent.PoniesEmoteRooms)?.getContent<EmoteRoomsContent>() ?? {};
+      const updatedContent: EmoteRoomsContent = JSON.parse(JSON.stringify(content));
+
+      selectedPacks.forEach((addr) => {
+        const roomsToState = updatedContent.rooms ?? {};
+        const stateKeyToObj = roomsToState[addr.roomId] ?? {};
+        stateKeyToObj[addr.stateKey] = {};
+        roomsToState[addr.roomId] = stateKeyToObj;
+        updatedContent.rooms = roomsToState;
+      });
+
+      removedPacks.forEach((addr) => {
+        if (updatedContent.rooms?.[addr.roomId]?.[addr.stateKey]) {
+          delete updatedContent.rooms?.[addr.roomId][addr.stateKey];
+        }
+      });
+
+      await mx.setAccountData(AccountDataEvent.PoniesEmoteRooms, updatedContent);
+    }, [mx, selectedPacks, removedPacks])
+  );
+
+  const resetChanges = useCallback(() => {
+    setSelectedPacks([]);
+    setRemovedPacks([]);
+  }, []);
+
+  useEffect(() => {
+    if (applyState.status === AsyncStatus.Success) {
+      resetChanges();
+    }
+  }, [applyState, resetChanges]);
+
+  const handleSelectMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setMenuCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const applyingChanges = applyState.status === AsyncStatus.Loading;
+  const hasChanges = removedPacks.length > 0 || selectedPacks.length > 0;
+
+  const renderPack = (pack: ImagePack) => {
+    const avatarMxc = pack.getAvatarUrl(ImageUsage.Emoticon);
+    const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication) : undefined;
+    const { address } = pack;
+    if (!address) return null;
+    const removed = !!removedPacks.find((addr) => packAddressEqual(addr, address));
+
+    return (
+      <SequenceCard
+        key={pack.id}
+        className={SequenceCardStyle}
+        variant={removed ? 'Critical' : 'SurfaceVariant'}
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title={
+            <span style={{ textDecoration: removed ? 'line-through' : undefined }}>
+              {pack.meta.name ?? 'Unknown'}
+            </span>
+          }
+          description={<span className={LineClamp2}>{pack.meta.attribution}</span>}
+          before={
+            <Box alignItems="Center" gap="300">
+              {removed ? (
+                <IconButton
+                  size="300"
+                  radii="Pill"
+                  variant="Critical"
+                  onClick={() => handleUndoRemove(address)}
+                  disabled={applyingChanges}
+                >
+                  <Icon src={Icons.Plus} size="100" />
+                </IconButton>
+              ) : (
+                <IconButton
+                  size="300"
+                  radii="Pill"
+                  variant="Secondary"
+                  onClick={() => handleRemove(address)}
+                  disabled={applyingChanges}
+                >
+                  <Icon src={Icons.Cross} size="100" />
+                </IconButton>
+              )}
+              <Avatar size="300" radii="300">
+                {avatarUrl ? (
+                  <AvatarImage style={{ objectFit: 'contain' }} src={avatarUrl} />
+                ) : (
+                  <AvatarFallback>
+                    <Icon size="400" src={Icons.Sticker} filled />
+                  </AvatarFallback>
+                )}
+              </Avatar>
+            </Box>
+          }
+          after={
+            !removed && (
+              <Button
+                variant="Secondary"
+                fill="Soft"
+                size="300"
+                radii="300"
+                outlined
+                onClick={() => onViewPack(pack)}
+              >
+                <Text size="B300">View</Text>
+              </Button>
+            )
+          }
+        />
+      </SequenceCard>
+    );
+  };
+
+  return (
+    <>
+      <Box direction="Column" gap="100">
+        <Text size="L400">Favorite Packs</Text>
+        <SequenceCard
+          className={SequenceCardStyle}
+          variant="SurfaceVariant"
+          direction="Column"
+          gap="400"
+        >
+          <SettingTile
+            title="Select Pack"
+            description="Pick emojis and stickers pack from rooms to use in all rooms."
+            after={
+              <>
+                <Button
+                  onClick={handleSelectMenu}
+                  variant="Secondary"
+                  fill="Soft"
+                  size="300"
+                  radii="300"
+                  outlined
+                >
+                  <Text size="B300">Select</Text>
+                </Button>
+                <PopOut
+                  anchor={menuCords}
+                  position="Bottom"
+                  align="End"
+                  content={
+                    <FocusTrap
+                      focusTrapOptions={{
+                        initialFocus: false,
+                        onDeactivate: () => setMenuCords(undefined),
+                        clickOutsideDeactivates: true,
+                        isKeyForward: (evt: KeyboardEvent) =>
+                          evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+                        isKeyBackward: (evt: KeyboardEvent) =>
+                          evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+                        escapeDeactivates: stopPropagation,
+                      }}
+                    >
+                      <Menu
+                        style={{
+                          display: 'flex',
+                          maxWidth: toRem(400),
+                          width: '100vw',
+                          maxHeight: toRem(500),
+                        }}
+                      >
+                        <GlobalPackSelector
+                          packs={unselectedGlobalPacks}
+                          useAuthentication={useAuthentication}
+                          onSelect={handleSelected}
+                        />
+                      </Menu>
+                    </FocusTrap>
+                  }
+                />
+              </>
+            }
+          />
+        </SequenceCard>
+        {globalPacks.map(renderPack)}
+        {nonGlobalPacks
+          .filter((pack) => !!selectedPacks.find((addr) => packAddressEqual(pack.address, addr)))
+          .map(renderPack)}
+      </Box>
+      {hasChanges && (
+        <Menu
+          style={{
+            position: 'sticky',
+            padding: config.space.S200,
+            paddingLeft: config.space.S400,
+            bottom: config.space.S400,
+            left: config.space.S400,
+            right: 0,
+            zIndex: 1,
+          }}
+          variant="Success"
+        >
+          <Box alignItems="Center" gap="400">
+            <Box grow="Yes" direction="Column">
+              {applyState.status === AsyncStatus.Error ? (
+                <Text size="T200">
+                  <b>Failed to apply changes! Please try again.</b>
+                </Text>
+              ) : (
+                <Text size="T200">
+                  <b>Changes saved! Apply when ready.</b>
+                </Text>
+              )}
+            </Box>
+            <Box shrink="No" gap="200">
+              <Button
+                size="300"
+                variant="Success"
+                fill="None"
+                radii="300"
+                disabled={applyingChanges}
+                onClick={resetChanges}
+              >
+                <Text size="B300">Reset</Text>
+              </Button>
+              <Button
+                size="300"
+                variant="Success"
+                radii="300"
+                disabled={applyingChanges}
+                before={applyingChanges && <Spinner variant="Success" fill="Solid" size="100" />}
+                onClick={applyChanges}
+              >
+                <Text size="B300">Apply Changes</Text>
+              </Button>
+            </Box>
+          </Box>
+        </Menu>
+      )}
+    </>
+  );
+}
diff --git a/src/app/features/settings/emojis-stickers/UserPack.tsx b/src/app/features/settings/emojis-stickers/UserPack.tsx
new file mode 100644 (file)
index 0000000..c41c8e1
--- /dev/null
@@ -0,0 +1,71 @@
+import React from 'react';
+import { Avatar, AvatarFallback, AvatarImage, Box, Button, Icon, Icons, Text } from 'folds';
+import { useUserImagePack } from '../../../hooks/useImagePacks';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { ImagePack, ImageUsage } from '../../../plugins/custom-emoji';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { mxcUrlToHttp } from '../../../utils/matrix';
+import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+
+type UserPackProps = {
+  onViewPack: (imagePack: ImagePack) => void;
+};
+export function UserPack({ onViewPack }: UserPackProps) {
+  const mx = useMatrixClient();
+  const useAuthentication = useMediaAuthentication();
+
+  const userPack = useUserImagePack();
+  const avatarMxc = userPack?.getAvatarUrl(ImageUsage.Emoticon);
+  const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication) : undefined;
+
+  const handleView = () => {
+    if (userPack) {
+      onViewPack(userPack);
+    } else {
+      const defaultPack = new ImagePack(mx.getUserId() ?? '', {}, undefined);
+      onViewPack(defaultPack);
+    }
+  };
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Default Pack</Text>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title={userPack?.meta.name ?? 'Unknown'}
+          description={userPack?.meta.attribution}
+          before={
+            <Avatar size="300" radii="300">
+              {avatarUrl ? (
+                <AvatarImage style={{ objectFit: 'contain' }} src={avatarUrl} />
+              ) : (
+                <AvatarFallback>
+                  <Icon size="400" src={Icons.Sticker} filled />
+                </AvatarFallback>
+              )}
+            </Avatar>
+          }
+          after={
+            <Button
+              variant="Secondary"
+              fill="Soft"
+              size="300"
+              radii="300"
+              outlined
+              onClick={handleView}
+            >
+              <Text size="B300">View</Text>
+            </Button>
+          }
+        />
+      </SequenceCard>
+    </Box>
+  );
+}
diff --git a/src/app/features/settings/emojis-stickers/index.ts b/src/app/features/settings/emojis-stickers/index.ts
new file mode 100644 (file)
index 0000000..9c9e9f5
--- /dev/null
@@ -0,0 +1 @@
+export * from './EmojisStickers';
diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx
new file mode 100644 (file)
index 0000000..abbfda9
--- /dev/null
@@ -0,0 +1,618 @@
+import React, {
+  ChangeEventHandler,
+  KeyboardEventHandler,
+  MouseEventHandler,
+  useState,
+} from 'react';
+import {
+  as,
+  Box,
+  Button,
+  Chip,
+  config,
+  Icon,
+  IconButton,
+  Icons,
+  Input,
+  Menu,
+  MenuItem,
+  PopOut,
+  RectCords,
+  Scroll,
+  Switch,
+  Text,
+  toRem,
+} from 'folds';
+import { isKeyHotkey } from 'is-hotkey';
+import FocusTrap from 'focus-trap-react';
+import { Page, PageContent, PageHeader } from '../../../components/page';
+import { SequenceCard } from '../../../components/sequence-card';
+import { useSetting } from '../../../state/hooks/settings';
+import { MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
+import { SettingTile } from '../../../components/setting-tile';
+import { KeySymbol } from '../../../utils/key-symbol';
+import { isMacOS } from '../../../utils/user-agent';
+import {
+  DarkTheme,
+  LightTheme,
+  Theme,
+  ThemeKind,
+  useSystemThemeKind,
+  useThemeNames,
+  useThemes,
+} from '../../../hooks/useTheme';
+import { stopPropagation } from '../../../utils/keyboard';
+import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
+import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
+import { SequenceCardStyle } from '../styles.css';
+
+type ThemeSelectorProps = {
+  themeNames: Record<string, string>;
+  themes: Theme[];
+  selected: Theme;
+  onSelect: (theme: Theme) => void;
+};
+const ThemeSelector = as<'div', ThemeSelectorProps>(
+  ({ themeNames, themes, selected, onSelect, ...props }, ref) => (
+    <Menu {...props} ref={ref}>
+      <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+        {themes.map((theme) => (
+          <MenuItem
+            key={theme.id}
+            size="300"
+            variant={theme.id === selected.id ? 'Primary' : 'Surface'}
+            radii="300"
+            onClick={() => onSelect(theme)}
+          >
+            <Text size="T300">{themeNames[theme.id] ?? theme.id}</Text>
+          </MenuItem>
+        ))}
+      </Box>
+    </Menu>
+  )
+);
+
+function SelectTheme({ disabled }: { disabled?: boolean }) {
+  const themes = useThemes();
+  const themeNames = useThemeNames();
+  const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId');
+  const [menuCords, setMenuCords] = useState<RectCords>();
+  const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
+
+  const handleThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setMenuCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const handleThemeSelect = (theme: Theme) => {
+    setThemeId(theme.id);
+    setMenuCords(undefined);
+  };
+
+  return (
+    <>
+      <Button
+        size="300"
+        variant="Primary"
+        outlined
+        fill="Soft"
+        radii="300"
+        after={<Icon size="300" src={Icons.ChevronBottom} />}
+        onClick={disabled ? undefined : handleThemeMenu}
+        aria-disabled={disabled}
+      >
+        <Text size="T300">{themeNames[selectedTheme.id] ?? selectedTheme.id}</Text>
+      </Button>
+      <PopOut
+        anchor={menuCords}
+        offset={5}
+        position="Bottom"
+        align="End"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setMenuCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <ThemeSelector
+              themeNames={themeNames}
+              themes={themes}
+              selected={selectedTheme}
+              onSelect={handleThemeSelect}
+            />
+          </FocusTrap>
+        }
+      />
+    </>
+  );
+}
+
+function SystemThemePreferences() {
+  const themeKind = useSystemThemeKind();
+  const themeNames = useThemeNames();
+  const themes = useThemes();
+  const [lightThemeId, setLightThemeId] = useSetting(settingsAtom, 'lightThemeId');
+  const [darkThemeId, setDarkThemeId] = useSetting(settingsAtom, 'darkThemeId');
+
+  const lightThemes = themes.filter((theme) => theme.kind === ThemeKind.Light);
+  const darkThemes = themes.filter((theme) => theme.kind === ThemeKind.Dark);
+
+  const selectedLightTheme = lightThemes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
+  const selectedDarkTheme = darkThemes.find((theme) => theme.id === darkThemeId) ?? DarkTheme;
+
+  const [ltCords, setLTCords] = useState<RectCords>();
+  const [dtCords, setDTCords] = useState<RectCords>();
+
+  const handleLightThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setLTCords(evt.currentTarget.getBoundingClientRect());
+  };
+  const handleDarkThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setDTCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const handleLightThemeSelect = (theme: Theme) => {
+    setLightThemeId(theme.id);
+    setLTCords(undefined);
+  };
+
+  const handleDarkThemeSelect = (theme: Theme) => {
+    setDarkThemeId(theme.id);
+    setDTCords(undefined);
+  };
+
+  return (
+    <Box wrap="Wrap" gap="400">
+      <SettingTile
+        title="Light Theme:"
+        after={
+          <Chip
+            variant={themeKind === ThemeKind.Light ? 'Primary' : 'Secondary'}
+            outlined={themeKind === ThemeKind.Light}
+            radii="Pill"
+            after={<Icon size="200" src={Icons.ChevronBottom} />}
+            onClick={handleLightThemeMenu}
+          >
+            <Text size="B300">{themeNames[selectedLightTheme.id] ?? selectedLightTheme.id}</Text>
+          </Chip>
+        }
+      />
+      <PopOut
+        anchor={ltCords}
+        offset={5}
+        position="Bottom"
+        align="End"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setLTCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <ThemeSelector
+              themeNames={themeNames}
+              themes={lightThemes}
+              selected={selectedLightTheme}
+              onSelect={handleLightThemeSelect}
+            />
+          </FocusTrap>
+        }
+      />
+      <SettingTile
+        title="Dark Theme:"
+        after={
+          <Chip
+            variant={themeKind === ThemeKind.Dark ? 'Primary' : 'Secondary'}
+            outlined={themeKind === ThemeKind.Dark}
+            radii="Pill"
+            after={<Icon size="200" src={Icons.ChevronBottom} />}
+            onClick={handleDarkThemeMenu}
+          >
+            <Text size="B300">{themeNames[selectedDarkTheme.id] ?? selectedDarkTheme.id}</Text>
+          </Chip>
+        }
+      />
+      <PopOut
+        anchor={dtCords}
+        offset={5}
+        position="Bottom"
+        align="End"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setDTCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <ThemeSelector
+              themeNames={themeNames}
+              themes={darkThemes}
+              selected={selectedDarkTheme}
+              onSelect={handleDarkThemeSelect}
+            />
+          </FocusTrap>
+        }
+      />
+    </Box>
+  );
+}
+
+function PageZoomInput() {
+  const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom');
+  const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`);
+
+  const handleZoomChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
+    setCurrentZoom(evt.target.value);
+  };
+
+  const handleZoomEnter: KeyboardEventHandler<HTMLInputElement> = (evt) => {
+    if (isKeyHotkey('escape', evt)) {
+      evt.stopPropagation();
+      setCurrentZoom(pageZoom.toString());
+    }
+    if (
+      isKeyHotkey('enter', evt) &&
+      'value' in evt.target &&
+      typeof evt.target.value === 'string'
+    ) {
+      const newZoom = parseInt(evt.target.value, 10);
+      if (Number.isNaN(newZoom)) return;
+      const safeZoom = Math.max(Math.min(newZoom, 150), 75);
+      setPageZoom(safeZoom);
+      setCurrentZoom(safeZoom.toString());
+    }
+  };
+
+  return (
+    <Input
+      style={{ width: toRem(100) }}
+      variant={pageZoom === parseInt(currentZoom, 10) ? 'Secondary' : 'Success'}
+      size="300"
+      radii="300"
+      type="number"
+      min="75"
+      max="150"
+      value={currentZoom}
+      onChange={handleZoomChange}
+      onKeyDown={handleZoomEnter}
+      after={<Text size="T300">%</Text>}
+      outlined
+    />
+  );
+}
+
+function Appearance() {
+  const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
+  const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Appearance</Text>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="System Theme"
+          description="Choose between light and dark theme based on system preference."
+          after={<Switch variant="Primary" value={systemTheme} onChange={setSystemTheme} />}
+        />
+        {systemTheme && <SystemThemePreferences />}
+      </SequenceCard>
+
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="Theme"
+          description="Theme to use when system theme is not enabled."
+          after={<SelectTheme disabled={systemTheme} />}
+        />
+      </SequenceCard>
+
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="Twitter Emoji"
+          after={<Switch variant="Primary" value={twitterEmoji} onChange={setTwitterEmoji} />}
+        />
+      </SequenceCard>
+
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile title="Page Zoom" after={<PageZoomInput />} />
+      </SequenceCard>
+    </Box>
+  );
+}
+
+function Editor() {
+  const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
+  const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Editor</Text>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="ENTER for Newline"
+          description={`Use ${
+            isMacOS() ? KeySymbol.Command : 'Ctrl'
+          } + ENTER to send message and ENTER for newline.`}
+          after={<Switch variant="Primary" value={enterForNewline} onChange={setEnterForNewline} />}
+        />
+      </SequenceCard>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="Markdown Formatting"
+          after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
+        />
+      </SequenceCard>
+    </Box>
+  );
+}
+
+function SelectMessageLayout() {
+  const [menuCords, setMenuCords] = useState<RectCords>();
+  const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout');
+  const messageLayoutItems = useMessageLayoutItems();
+
+  const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setMenuCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const handleSelect = (layout: MessageLayout) => {
+    setMessageLayout(layout);
+    setMenuCords(undefined);
+  };
+
+  return (
+    <>
+      <Button
+        size="300"
+        variant="Secondary"
+        outlined
+        fill="Soft"
+        radii="300"
+        after={<Icon size="300" src={Icons.ChevronBottom} />}
+        onClick={handleMenu}
+      >
+        <Text size="T300">
+          {messageLayoutItems.find((i) => i.layout === messageLayout)?.name ?? messageLayout}
+        </Text>
+      </Button>
+      <PopOut
+        anchor={menuCords}
+        offset={5}
+        position="Bottom"
+        align="End"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setMenuCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <Menu>
+              <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+                {messageLayoutItems.map((item) => (
+                  <MenuItem
+                    key={item.layout}
+                    size="300"
+                    variant={messageLayout === item.layout ? 'Primary' : 'Surface'}
+                    radii="300"
+                    onClick={() => handleSelect(item.layout)}
+                  >
+                    <Text size="T300">{item.name}</Text>
+                  </MenuItem>
+                ))}
+              </Box>
+            </Menu>
+          </FocusTrap>
+        }
+      />
+    </>
+  );
+}
+
+function SelectMessageSpacing() {
+  const [menuCords, setMenuCords] = useState<RectCords>();
+  const [messageSpacing, setMessageSpacing] = useSetting(settingsAtom, 'messageSpacing');
+  const messageSpacingItems = useMessageSpacingItems();
+
+  const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setMenuCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const handleSelect = (layout: MessageSpacing) => {
+    setMessageSpacing(layout);
+    setMenuCords(undefined);
+  };
+
+  return (
+    <>
+      <Button
+        size="300"
+        variant="Secondary"
+        outlined
+        fill="Soft"
+        radii="300"
+        after={<Icon size="300" src={Icons.ChevronBottom} />}
+        onClick={handleMenu}
+      >
+        <Text size="T300">
+          {messageSpacingItems.find((i) => i.spacing === messageSpacing)?.name ?? messageSpacing}
+        </Text>
+      </Button>
+      <PopOut
+        anchor={menuCords}
+        offset={5}
+        position="Bottom"
+        align="End"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setMenuCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <Menu>
+              <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+                {messageSpacingItems.map((item) => (
+                  <MenuItem
+                    key={item.spacing}
+                    size="300"
+                    variant={messageSpacing === item.spacing ? 'Primary' : 'Surface'}
+                    radii="300"
+                    onClick={() => handleSelect(item.spacing)}
+                  >
+                    <Text size="T300">{item.name}</Text>
+                  </MenuItem>
+                ))}
+              </Box>
+            </Menu>
+          </FocusTrap>
+        }
+      />
+    </>
+  );
+}
+
+function Messages() {
+  const [hideMembershipEvents, setHideMembershipEvents] = useSetting(
+    settingsAtom,
+    'hideMembershipEvents'
+  );
+  const [hideNickAvatarEvents, setHideNickAvatarEvents] = useSetting(
+    settingsAtom,
+    'hideNickAvatarEvents'
+  );
+  const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
+  const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview');
+  const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
+  const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Messages</Text>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile title="Message Layout" after={<SelectMessageLayout />} />
+      </SequenceCard>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile title="Message Spacing" after={<SelectMessageSpacing />} />
+      </SequenceCard>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="Hide Membership Change"
+          after={
+            <Switch
+              variant="Primary"
+              value={hideMembershipEvents}
+              onChange={setHideMembershipEvents}
+            />
+          }
+        />
+      </SequenceCard>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="Hide Profile Change"
+          after={
+            <Switch
+              variant="Primary"
+              value={hideNickAvatarEvents}
+              onChange={setHideNickAvatarEvents}
+            />
+          }
+        />
+      </SequenceCard>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="Disable Media Auto Load"
+          after={<Switch variant="Primary" value={mediaAutoLoad} onChange={setMediaAutoLoad} />}
+        />
+      </SequenceCard>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="Url Preview"
+          after={<Switch variant="Primary" value={urlPreview} onChange={setUrlPreview} />}
+        />
+      </SequenceCard>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="Url Preview in Encrypted Room"
+          after={<Switch variant="Primary" value={encUrlPreview} onChange={setEncUrlPreview} />}
+        />
+      </SequenceCard>
+      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
+        <SettingTile
+          title="Show Hidden Events"
+          after={
+            <Switch variant="Primary" value={showHiddenEvents} onChange={setShowHiddenEvents} />
+          }
+        />
+      </SequenceCard>
+    </Box>
+  );
+}
+
+type GeneralProps = {
+  requestClose: () => void;
+};
+export function General({ requestClose }: GeneralProps) {
+  return (
+    <Page>
+      <PageHeader outlined={false}>
+        <Box grow="Yes" gap="200">
+          <Box grow="Yes" alignItems="Center" gap="200">
+            <Text size="H3" truncate>
+              General
+            </Text>
+          </Box>
+          <Box shrink="No">
+            <IconButton onClick={requestClose} variant="Surface">
+              <Icon src={Icons.Cross} />
+            </IconButton>
+          </Box>
+        </Box>
+      </PageHeader>
+      <Box grow="Yes">
+        <Scroll hideTrack visibility="Hover">
+          <PageContent>
+            <Box direction="Column" gap="700">
+              <Appearance />
+              <Editor />
+              <Messages />
+            </Box>
+          </PageContent>
+        </Scroll>
+      </Box>
+    </Page>
+  );
+}
diff --git a/src/app/features/settings/general/index.ts b/src/app/features/settings/general/index.ts
new file mode 100644 (file)
index 0000000..0ab02c5
--- /dev/null
@@ -0,0 +1 @@
+export * from './General';
diff --git a/src/app/features/settings/index.ts b/src/app/features/settings/index.ts
new file mode 100644 (file)
index 0000000..90e2697
--- /dev/null
@@ -0,0 +1 @@
+export * from './Settings';
diff --git a/src/app/features/settings/notifications/AllMessages.tsx b/src/app/features/settings/notifications/AllMessages.tsx
new file mode 100644 (file)
index 0000000..301cb9a
--- /dev/null
@@ -0,0 +1,152 @@
+import React, { useCallback, useMemo } from 'react';
+import { Badge, Box, Text } from 'folds';
+import { ConditionKind, IPushRules, PushRuleCondition, PushRuleKind, RuleId } from 'matrix-js-sdk';
+import { useAccountData } from '../../../hooks/useAccountData';
+import { AccountDataEvent } from '../../../../types/matrix/accountData';
+import { NotificationModeSwitcher } from './NotificationModeSwitcher';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { PushRuleData, usePushRule } from '../../../hooks/usePushRule';
+import {
+  getNotificationModeActions,
+  NotificationMode,
+  useNotificationModeActions,
+} from '../../../hooks/useNotificationMode';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+
+const getAllMessageDefaultRule = (
+  ruleId: RuleId,
+  encrypted: boolean,
+  oneToOne: boolean
+): PushRuleData => {
+  const conditions: PushRuleCondition[] = [];
+  if (oneToOne)
+    conditions.push({
+      kind: ConditionKind.RoomMemberCount,
+      is: '2',
+    });
+  conditions.push({
+    kind: ConditionKind.EventMatch,
+    key: 'type',
+    pattern: encrypted ? 'm.room.encrypted' : 'm.room.message',
+  });
+
+  return {
+    kind: PushRuleKind.Underride,
+    pushRule: {
+      rule_id: ruleId,
+      default: true,
+      enabled: true,
+      conditions,
+      actions: getNotificationModeActions(NotificationMode.NotifyLoud),
+    },
+  };
+};
+
+type PushRulesProps = {
+  ruleId: RuleId.DM | RuleId.EncryptedDM | RuleId.Message | RuleId.EncryptedMessage;
+  pushRules: IPushRules;
+  encrypted?: boolean;
+  oneToOne?: boolean;
+};
+function AllMessagesModeSwitcher({
+  ruleId,
+  pushRules,
+  encrypted = false,
+  oneToOne = false,
+}: PushRulesProps) {
+  const mx = useMatrixClient();
+  const defaultPushRuleData = getAllMessageDefaultRule(ruleId, encrypted, oneToOne);
+  const { kind, pushRule } = usePushRule(pushRules, ruleId) ?? defaultPushRuleData;
+  const getModeActions = useNotificationModeActions();
+
+  const handleChange = useCallback(
+    async (mode: NotificationMode) => {
+      const actions = getModeActions(mode);
+      await mx.setPushRuleActions('global', kind, ruleId, actions);
+    },
+    [mx, getModeActions, kind, ruleId]
+  );
+
+  return <NotificationModeSwitcher pushRule={pushRule} onChange={handleChange} />;
+}
+
+export function AllMessagesNotifications() {
+  const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
+  const pushRules = useMemo(
+    () => pushRulesEvt?.getContent<IPushRules>() ?? { global: {} },
+    [pushRulesEvt]
+  );
+
+  return (
+    <Box direction="Column" gap="100">
+      <Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
+        <Text size="L400">All Messages</Text>
+        <Box gap="100">
+          <Text size="T200">Badge: </Text>
+          <Badge radii="300" variant="Secondary" fill="Solid">
+            <Text size="L400">1</Text>
+          </Badge>
+        </Box>
+      </Box>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="1-to-1 Chats"
+          after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.DM} oneToOne />}
+        />
+      </SequenceCard>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="1-to-1 Chats (Encrypted)"
+          after={
+            <AllMessagesModeSwitcher
+              pushRules={pushRules}
+              ruleId={RuleId.EncryptedDM}
+              encrypted
+              oneToOne
+            />
+          }
+        />
+      </SequenceCard>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Rooms"
+          after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.Message} />}
+        />
+      </SequenceCard>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Rooms (Encrypted)"
+          after={
+            <AllMessagesModeSwitcher
+              pushRules={pushRules}
+              ruleId={RuleId.EncryptedMessage}
+              encrypted
+            />
+          }
+        />
+      </SequenceCard>
+    </Box>
+  );
+}
diff --git a/src/app/features/settings/notifications/IgnoredUserList.tsx b/src/app/features/settings/notifications/IgnoredUserList.tsx
new file mode 100644 (file)
index 0000000..49264e5
--- /dev/null
@@ -0,0 +1,171 @@
+import React, { ChangeEventHandler, FormEventHandler, useCallback, useMemo, useState } from 'react';
+import { Box, Button, Chip, Icon, IconButton, Icons, Input, Spinner, Text, config } from 'folds';
+import { useAccountData } from '../../../hooks/useAccountData';
+import { AccountDataEvent } from '../../../../types/matrix/accountData';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { isUserId } from '../../../utils/matrix';
+
+type IgnoredUserListContent = {
+  ignored_users?: Record<string, object>;
+};
+
+function IgnoreUserInput({ userList }: { userList: string[] }) {
+  const mx = useMatrixClient();
+  const [userId, setUserId] = useState<string>('');
+
+  const [ignoreState, ignore] = useAsyncCallback(
+    useCallback(
+      async (uId: string) => {
+        mx.setIgnoredUsers([...userList, uId]);
+        setUserId('');
+      },
+      [mx, userList]
+    )
+  );
+  const ignoring = ignoreState.status === AsyncStatus.Loading;
+
+  const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
+    const uId = evt.currentTarget.value;
+    setUserId(uId);
+  };
+
+  const handleReset = () => {
+    setUserId('');
+  };
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+    if (ignoring) return;
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const userIdInput = target?.userIdInput as HTMLInputElement | undefined;
+    const uId = userIdInput?.value.trim();
+    if (!uId) return;
+
+    if (!isUserId(uId)) return;
+
+    ignore(uId);
+  };
+
+  return (
+    <Box as="form" onSubmit={handleSubmit} gap="200" aria-disabled={ignoring}>
+      <Box grow="Yes" direction="Column">
+        <Input
+          required
+          name="userIdInput"
+          value={userId}
+          onChange={handleChange}
+          variant="Secondary"
+          radii="300"
+          style={{ paddingRight: config.space.S200 }}
+          readOnly={ignoring}
+          after={
+            userId &&
+            !ignoring && (
+              <IconButton
+                type="reset"
+                onClick={handleReset}
+                size="300"
+                radii="300"
+                variant="Secondary"
+              >
+                <Icon src={Icons.Cross} size="100" />
+              </IconButton>
+            )
+          }
+        />
+      </Box>
+      <Button
+        size="400"
+        variant="Secondary"
+        fill="Soft"
+        outlined
+        radii="300"
+        type="submit"
+        disabled={ignoring}
+      >
+        {ignoring && <Spinner variant="Secondary" size="300" />}
+        <Text size="B400">Block</Text>
+      </Button>
+    </Box>
+  );
+}
+
+function IgnoredUserChip({ userId, userList }: { userId: string; userList: string[] }) {
+  const mx = useMatrixClient();
+  const [unignoreState, unignore] = useAsyncCallback(
+    useCallback(
+      () => mx.setIgnoredUsers(userList.filter((uId) => uId !== userId)),
+      [mx, userId, userList]
+    )
+  );
+
+  const handleUnignore = () => unignore();
+
+  const unIgnoring = unignoreState.status === AsyncStatus.Loading;
+  return (
+    <Chip
+      variant="Secondary"
+      radii="Pill"
+      after={
+        unIgnoring ? (
+          <Spinner variant="Secondary" size="100" />
+        ) : (
+          <Icon src={Icons.Cross} size="100" />
+        )
+      }
+      onClick={handleUnignore}
+      disabled={unIgnoring}
+    >
+      <Text size="T200" truncate>
+        {userId}
+      </Text>
+    </Chip>
+  );
+}
+
+export function IgnoredUserList() {
+  const ignoredUserListEvt = useAccountData(AccountDataEvent.IgnoredUserList);
+  const ignoredUsers = useMemo(() => {
+    const ignoredUsersRecord =
+      ignoredUserListEvt?.getContent<IgnoredUserListContent>().ignored_users ?? {};
+    return Object.keys(ignoredUsersRecord);
+  }, [ignoredUserListEvt]);
+
+  return (
+    <Box direction="Column" gap="100">
+      <Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
+        <Text size="L400">Block Messages</Text>
+      </Box>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Select User"
+          description="Prevent receiving message by adding userId into blocklist."
+        >
+          <Box direction="Column" gap="300">
+            <IgnoreUserInput userList={ignoredUsers} />
+            {ignoredUsers.length > 0 && (
+              <Box direction="Inherit" gap="100">
+                <Text size="L400">Blocklist</Text>
+                <Box wrap="Wrap" gap="200">
+                  {ignoredUsers.map((userId) => (
+                    <IgnoredUserChip key={userId} userId={userId} userList={ignoredUsers} />
+                  ))}
+                </Box>
+              </Box>
+            )}
+          </Box>
+        </SettingTile>
+      </SequenceCard>
+    </Box>
+  );
+}
diff --git a/src/app/features/settings/notifications/KeywordMessages.tsx b/src/app/features/settings/notifications/KeywordMessages.tsx
new file mode 100644 (file)
index 0000000..7f84d60
--- /dev/null
@@ -0,0 +1,203 @@
+import React, { ChangeEventHandler, FormEventHandler, useCallback, useMemo, useState } from 'react';
+import { IPushRule, IPushRules, PushRuleKind } from 'matrix-js-sdk';
+import { Box, Text, Badge, Button, Input, config, IconButton, Icons, Icon, Spinner } from 'folds';
+import { useAccountData } from '../../../hooks/useAccountData';
+import { AccountDataEvent } from '../../../../types/matrix/accountData';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import {
+  getNotificationModeActions,
+  NotificationMode,
+  NotificationModeOptions,
+  useNotificationModeActions,
+} from '../../../hooks/useNotificationMode';
+import { NotificationModeSwitcher } from './NotificationModeSwitcher';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+
+const NOTIFY_MODE_OPS: NotificationModeOptions = {
+  highlight: true,
+};
+
+function KeywordInput() {
+  const mx = useMatrixClient();
+  const [keyword, setKeyword] = useState<string>('');
+
+  const [keywordState, addKeyword] = useAsyncCallback(
+    useCallback(
+      async (k: string) => {
+        mx.addPushRule('global', PushRuleKind.ContentSpecific, k, {
+          actions: getNotificationModeActions(NotificationMode.Notify, NOTIFY_MODE_OPS),
+          pattern: k,
+        });
+        setKeyword('');
+      },
+      [mx]
+    )
+  );
+  const addingKeyword = keywordState.status === AsyncStatus.Loading;
+
+  const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
+    const k = evt.currentTarget.value;
+    setKeyword(k);
+  };
+
+  const handleReset = () => {
+    setKeyword('');
+  };
+
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+    if (addingKeyword) return;
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const keywordInput = target?.keywordInput as HTMLInputElement | undefined;
+    const k = keywordInput?.value.trim();
+    if (!k) return;
+
+    addKeyword(k);
+  };
+
+  return (
+    <Box as="form" onSubmit={handleSubmit} gap="200" aria-disabled={addingKeyword}>
+      <Box grow="Yes" direction="Column">
+        <Input
+          required
+          name="keywordInput"
+          value={keyword}
+          onChange={handleChange}
+          variant="Secondary"
+          radii="300"
+          style={{ paddingRight: config.space.S200 }}
+          readOnly={addingKeyword}
+          after={
+            keyword &&
+            !addingKeyword && (
+              <IconButton
+                type="reset"
+                onClick={handleReset}
+                size="300"
+                radii="300"
+                variant="Secondary"
+              >
+                <Icon src={Icons.Cross} size="100" />
+              </IconButton>
+            )
+          }
+        />
+      </Box>
+      <Button
+        size="400"
+        variant="Secondary"
+        fill="Soft"
+        outlined
+        radii="300"
+        type="submit"
+        disabled={addingKeyword}
+      >
+        {addingKeyword && <Spinner variant="Secondary" size="300" />}
+        <Text size="B400">Save</Text>
+      </Button>
+    </Box>
+  );
+}
+
+type PushRulesProps = {
+  pushRule: IPushRule;
+};
+
+function KeywordCross({ pushRule }: PushRulesProps) {
+  const mx = useMatrixClient();
+  const [removeState, remove] = useAsyncCallback(
+    useCallback(
+      () => mx.deletePushRule('global', PushRuleKind.ContentSpecific, pushRule.rule_id),
+      [mx, pushRule]
+    )
+  );
+
+  const removing = removeState.status === AsyncStatus.Loading;
+  return (
+    <IconButton onClick={remove} size="300" radii="Pill" variant="Secondary" disabled={removing}>
+      {removing ? <Spinner size="100" /> : <Icon src={Icons.Cross} size="100" />}
+    </IconButton>
+  );
+}
+
+function KeywordModeSwitcher({ pushRule }: PushRulesProps) {
+  const mx = useMatrixClient();
+
+  const getModeActions = useNotificationModeActions(NOTIFY_MODE_OPS);
+
+  const handleChange = useCallback(
+    async (mode: NotificationMode) => {
+      const actions = getModeActions(mode);
+      await mx.setPushRuleActions(
+        'global',
+        PushRuleKind.ContentSpecific,
+        pushRule.rule_id,
+        actions
+      );
+    },
+    [mx, getModeActions, pushRule]
+  );
+
+  return <NotificationModeSwitcher pushRule={pushRule} onChange={handleChange} />;
+}
+
+export function KeywordMessagesNotifications() {
+  const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
+  const pushRules = useMemo(
+    () => pushRulesEvt?.getContent<IPushRules>() ?? { global: {} },
+    [pushRulesEvt]
+  );
+
+  const keywordPushRules = useMemo(() => {
+    const content = pushRules.global.content ?? [];
+    return content.filter(
+      (pushRule) => pushRule.default === false && typeof pushRule.pattern === 'string'
+    );
+  }, [pushRules]);
+
+  return (
+    <Box direction="Column" gap="100">
+      <Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
+        <Text size="L400">Keyword Messages</Text>
+        <Box gap="100">
+          <Text size="T200">Badge: </Text>
+          <Badge radii="300" variant="Success" fill="Solid">
+            <Text size="L400">1</Text>
+          </Badge>
+        </Box>
+      </Box>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Select Keyword"
+          description="Set a notification preference for message containing given keyword."
+        >
+          <KeywordInput />
+        </SettingTile>
+      </SequenceCard>
+      {keywordPushRules.map((pushRule) => (
+        <SequenceCard
+          key={pushRule.rule_id}
+          className={SequenceCardStyle}
+          variant="SurfaceVariant"
+          direction="Column"
+          gap="400"
+        >
+          <SettingTile
+            title={`"${pushRule.pattern}"`}
+            before={<KeywordCross pushRule={pushRule} />}
+            after={<KeywordModeSwitcher pushRule={pushRule} />}
+          />
+        </SequenceCard>
+      ))}
+    </Box>
+  );
+}
diff --git a/src/app/features/settings/notifications/NotificationModeSwitcher.tsx b/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
new file mode 100644 (file)
index 0000000..fe00839
--- /dev/null
@@ -0,0 +1,117 @@
+import {
+  Box,
+  Button,
+  config,
+  Icon,
+  Icons,
+  Menu,
+  MenuItem,
+  PopOut,
+  RectCords,
+  Spinner,
+  Text,
+} from 'folds';
+import { IPushRule } from 'matrix-js-sdk';
+import React, { MouseEventHandler, useMemo, useState } from 'react';
+import FocusTrap from 'focus-trap-react';
+import { NotificationMode, useNotificationActionsMode } from '../../../hooks/useNotificationMode';
+import { stopPropagation } from '../../../utils/keyboard';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+
+export const useNotificationModes = (): NotificationMode[] =>
+  useMemo(() => [NotificationMode.NotifyLoud, NotificationMode.Notify, NotificationMode.OFF], []);
+
+const useNotificationModeStr = (): Record<NotificationMode, string> =>
+  useMemo(
+    () => ({
+      [NotificationMode.OFF]: 'Disable',
+      [NotificationMode.Notify]: 'Notify Silent',
+      [NotificationMode.NotifyLoud]: 'Notify Loud',
+    }),
+    []
+  );
+
+type NotificationModeSwitcherProps = {
+  pushRule: IPushRule;
+  onChange: (mode: NotificationMode) => Promise<void>;
+};
+export function NotificationModeSwitcher({ pushRule, onChange }: NotificationModeSwitcherProps) {
+  const modes = useNotificationModes();
+  const modeToStr = useNotificationModeStr();
+  const selectedMode = useNotificationActionsMode(pushRule.actions);
+  const [changeState, change] = useAsyncCallback(onChange);
+  const changing = changeState.status === AsyncStatus.Loading;
+
+  const [menuCords, setMenuCords] = useState<RectCords>();
+
+  const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setMenuCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const handleSelect = (mode: NotificationMode) => {
+    setMenuCords(undefined);
+    change(mode);
+  };
+
+  return (
+    <>
+      <Button
+        size="300"
+        variant="Secondary"
+        outlined
+        fill="Soft"
+        radii="300"
+        after={
+          changing ? (
+            <Spinner variant="Secondary" size="300" />
+          ) : (
+            <Icon size="300" src={Icons.ChevronBottom} />
+          )
+        }
+        onClick={handleMenu}
+        disabled={changing}
+      >
+        <Text size="T300">{modeToStr[selectedMode]}</Text>
+      </Button>
+      <PopOut
+        anchor={menuCords}
+        offset={5}
+        position="Bottom"
+        align="End"
+        content={
+          <FocusTrap
+            focusTrapOptions={{
+              initialFocus: false,
+              onDeactivate: () => setMenuCords(undefined),
+              clickOutsideDeactivates: true,
+              isKeyForward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+              isKeyBackward: (evt: KeyboardEvent) =>
+                evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+              escapeDeactivates: stopPropagation,
+            }}
+          >
+            <Menu>
+              <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+                {modes.map((mode) => (
+                  <MenuItem
+                    key={mode}
+                    size="300"
+                    variant="Surface"
+                    aria-selected={mode === selectedMode}
+                    radii="300"
+                    onClick={() => handleSelect(mode)}
+                  >
+                    <Box grow="Yes">
+                      <Text size="T300">{modeToStr[mode]}</Text>
+                    </Box>
+                  </MenuItem>
+                ))}
+              </Box>
+            </Menu>
+          </FocusTrap>
+        }
+      />
+    </>
+  );
+}
diff --git a/src/app/features/settings/notifications/Notifications.tsx b/src/app/features/settings/notifications/Notifications.tsx
new file mode 100644 (file)
index 0000000..230d60a
--- /dev/null
@@ -0,0 +1,117 @@
+import React from 'react';
+import { Box, Text, IconButton, Icon, Icons, Scroll, Switch, Button, color } from 'folds';
+import { Page, PageContent, PageHeader } from '../../../components/page';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+import { usePermissionState } from '../../../hooks/usePermission';
+import { AllMessagesNotifications } from './AllMessages';
+import { SpecialMessagesNotifications } from './SpecialMessages';
+import { KeywordMessagesNotifications } from './KeywordMessages';
+import { IgnoredUserList } from './IgnoredUserList';
+
+function SystemNotification() {
+  const notifPermission = usePermissionState(
+    'notifications',
+    window.Notification.permission === 'default' ? 'prompt' : window.Notification.permission
+  );
+  const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications');
+  const [isNotificationSounds, setIsNotificationSounds] = useSetting(
+    settingsAtom,
+    'isNotificationSounds'
+  );
+
+  const requestNotificationPermission = () => {
+    window.Notification.requestPermission();
+  };
+
+  return (
+    <Box direction="Column" gap="100">
+      <Text size="L400">System</Text>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Desktop Notifications"
+          description={
+            notifPermission === 'denied' ? (
+              <Text as="span" style={{ color: color.Critical.Main }} size="T200">
+                Notification permission is blocked. Please allow notification permission from
+                browser address bar.
+              </Text>
+            ) : (
+              <span>Show desktop notifications when message arrive.</span>
+            )
+          }
+          after={
+            notifPermission === 'prompt' ? (
+              <Button size="300" radii="300" onClick={requestNotificationPermission}>
+                <Text size="B300">Enable</Text>
+              </Button>
+            ) : (
+              <Switch
+                disabled={notifPermission !== 'granted'}
+                value={showNotifications}
+                onChange={setShowNotifications}
+              />
+            )
+          }
+        />
+      </SequenceCard>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Notification Sound"
+          description="Play sound when new message arrive."
+          after={<Switch value={isNotificationSounds} onChange={setIsNotificationSounds} />}
+        />
+      </SequenceCard>
+    </Box>
+  );
+}
+
+type NotificationsProps = {
+  requestClose: () => void;
+};
+export function Notifications({ requestClose }: NotificationsProps) {
+  return (
+    <Page>
+      <PageHeader outlined={false}>
+        <Box grow="Yes" gap="200">
+          <Box grow="Yes" alignItems="Center" gap="200">
+            <Text size="H3" truncate>
+              Notifications
+            </Text>
+          </Box>
+          <Box shrink="No">
+            <IconButton onClick={requestClose} variant="Surface">
+              <Icon src={Icons.Cross} />
+            </IconButton>
+          </Box>
+        </Box>
+      </PageHeader>
+      <Box grow="Yes">
+        <Scroll hideTrack visibility="Hover">
+          <PageContent>
+            <Box direction="Column" gap="700">
+              <SystemNotification />
+              <AllMessagesNotifications />
+              <SpecialMessagesNotifications />
+              <KeywordMessagesNotifications />
+              <IgnoredUserList />
+            </Box>
+          </PageContent>
+        </Scroll>
+      </Box>
+    </Page>
+  );
+}
diff --git a/src/app/features/settings/notifications/SpecialMessages.tsx b/src/app/features/settings/notifications/SpecialMessages.tsx
new file mode 100644 (file)
index 0000000..cbebf64
--- /dev/null
@@ -0,0 +1,222 @@
+import React, { useCallback, useMemo } from 'react';
+import { ConditionKind, IPushRules, PushRuleKind, RuleId } from 'matrix-js-sdk';
+import { Box, Text, Badge } from 'folds';
+import { useAccountData } from '../../../hooks/useAccountData';
+import { AccountDataEvent } from '../../../../types/matrix/accountData';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { useUserProfile } from '../../../hooks/useUserProfile';
+import { getMxIdLocalPart } from '../../../utils/matrix';
+import { makePushRuleData, PushRuleData, usePushRule } from '../../../hooks/usePushRule';
+import {
+  getNotificationModeActions,
+  NotificationMode,
+  NotificationModeOptions,
+  useNotificationModeActions,
+} from '../../../hooks/useNotificationMode';
+import { NotificationModeSwitcher } from './NotificationModeSwitcher';
+
+const NOTIFY_MODE_OPS: NotificationModeOptions = {
+  highlight: true,
+};
+const getDefaultIsUserMention = (userId: string): PushRuleData =>
+  makePushRuleData(
+    PushRuleKind.Override,
+    RuleId.IsUserMention,
+    getNotificationModeActions(NotificationMode.NotifyLoud, { highlight: true }),
+    [
+      {
+        kind: ConditionKind.EventPropertyContains,
+        key: 'content.m\\.mentions.user_ids',
+        value: userId,
+      },
+    ]
+  );
+
+const DefaultContainsDisplayName = makePushRuleData(
+  PushRuleKind.Override,
+  RuleId.ContainsDisplayName,
+  getNotificationModeActions(NotificationMode.NotifyLoud, { highlight: true }),
+  [
+    {
+      kind: ConditionKind.ContainsDisplayName,
+    },
+  ]
+);
+
+const getDefaultContainsUsername = (username: string) =>
+  makePushRuleData(
+    PushRuleKind.ContentSpecific,
+    RuleId.ContainsUserName,
+    getNotificationModeActions(NotificationMode.NotifyLoud, { highlight: true }),
+    undefined,
+    username
+  );
+
+const DefaultIsRoomMention = makePushRuleData(
+  PushRuleKind.Override,
+  RuleId.IsRoomMention,
+  getNotificationModeActions(NotificationMode.Notify, { highlight: true }),
+  [
+    {
+      kind: ConditionKind.EventPropertyIs,
+      key: 'content.m\\.mentions.room',
+      value: true,
+    },
+    {
+      kind: ConditionKind.SenderNotificationPermission,
+      key: 'room',
+    },
+  ]
+);
+
+const DefaultAtRoomNotification = makePushRuleData(
+  PushRuleKind.Override,
+  RuleId.AtRoomNotification,
+  getNotificationModeActions(NotificationMode.Notify, { highlight: true }),
+  [
+    {
+      kind: ConditionKind.EventMatch,
+      key: 'content.body',
+      pattern: '@room',
+    },
+    {
+      kind: ConditionKind.SenderNotificationPermission,
+      key: 'room',
+    },
+  ]
+);
+
+type PushRulesProps = {
+  ruleId: RuleId;
+  pushRules: IPushRules;
+  defaultPushRuleData: PushRuleData;
+};
+function MentionModeSwitcher({ ruleId, pushRules, defaultPushRuleData }: PushRulesProps) {
+  const mx = useMatrixClient();
+
+  const { kind, pushRule } = usePushRule(pushRules, ruleId) ?? defaultPushRuleData;
+  const getModeActions = useNotificationModeActions(NOTIFY_MODE_OPS);
+
+  const handleChange = useCallback(
+    async (mode: NotificationMode) => {
+      const actions = getModeActions(mode);
+      await mx.setPushRuleActions('global', kind, ruleId, actions);
+    },
+    [mx, getModeActions, kind, ruleId]
+  );
+
+  return <NotificationModeSwitcher pushRule={pushRule} onChange={handleChange} />;
+}
+
+export function SpecialMessagesNotifications() {
+  const mx = useMatrixClient();
+  const userId = mx.getUserId()!;
+  const { displayName } = useUserProfile(userId);
+  const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
+  const pushRules = useMemo(
+    () => pushRulesEvt?.getContent<IPushRules>() ?? { global: {} },
+    [pushRulesEvt]
+  );
+
+  return (
+    <Box direction="Column" gap="100">
+      <Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
+        <Text size="L400">Special Messages</Text>
+        <Box gap="100">
+          <Text size="T200">Badge: </Text>
+          <Badge radii="300" variant="Success" fill="Solid">
+            <Text size="L400">1</Text>
+          </Badge>
+        </Box>
+      </Box>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title={`Mention User ID ("${userId}")`}
+          after={
+            <MentionModeSwitcher
+              pushRules={pushRules}
+              ruleId={RuleId.IsUserMention}
+              defaultPushRuleData={getDefaultIsUserMention(userId)}
+            />
+          }
+        />
+      </SequenceCard>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title={`Contains Displayname ${displayName ? `("${displayName}")` : ''}`}
+          after={
+            <MentionModeSwitcher
+              pushRules={pushRules}
+              ruleId={RuleId.ContainsDisplayName}
+              defaultPushRuleData={DefaultContainsDisplayName}
+            />
+          }
+        />
+      </SequenceCard>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title={`Contains Username ("${getMxIdLocalPart(userId)}")`}
+          after={
+            <MentionModeSwitcher
+              pushRules={pushRules}
+              ruleId={RuleId.ContainsUserName}
+              defaultPushRuleData={getDefaultContainsUsername(getMxIdLocalPart(userId) ?? userId)}
+            />
+          }
+        />
+      </SequenceCard>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Mention @room"
+          after={
+            <MentionModeSwitcher
+              pushRules={pushRules}
+              ruleId={RuleId.IsRoomMention}
+              defaultPushRuleData={DefaultIsRoomMention}
+            />
+          }
+        />
+      </SequenceCard>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Contains @room"
+          after={
+            <MentionModeSwitcher
+              pushRules={pushRules}
+              ruleId={RuleId.AtRoomNotification}
+              defaultPushRuleData={DefaultAtRoomNotification}
+            />
+          }
+        />
+      </SequenceCard>
+    </Box>
+  );
+}
diff --git a/src/app/features/settings/notifications/index.ts b/src/app/features/settings/notifications/index.ts
new file mode 100644 (file)
index 0000000..e782535
--- /dev/null
@@ -0,0 +1 @@
+export * from './Notifications';
diff --git a/src/app/features/settings/styles.css.ts b/src/app/features/settings/styles.css.ts
new file mode 100644 (file)
index 0000000..ce89c16
--- /dev/null
@@ -0,0 +1,6 @@
+import { style } from '@vanilla-extract/css';
+import { config } from 'folds';
+
+export const SequenceCardStyle = style({
+  padding: config.space.S300,
+});
diff --git a/src/app/hooks/useAccountData.js b/src/app/hooks/useAccountData.js
deleted file mode 100644 (file)
index 43cc11e..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-/* eslint-disable import/prefer-default-export */
-import { useState, useEffect } from 'react';
-import { useMatrixClient } from './useMatrixClient';
-
-export function useAccountData(eventType) {
-  const mx = useMatrixClient();
-  const [event, setEvent] = useState(mx.getAccountData(eventType));
-
-  useEffect(() => {
-    const handleChange = (mEvent) => {
-      if (mEvent.getType() !== eventType) return;
-      setEvent(mEvent);
-    };
-    mx.on('accountData', handleChange);
-    return () => {
-      mx.removeListener('accountData', handleChange);
-    };
-  }, [mx, eventType]);
-
-  return event;
-}
diff --git a/src/app/hooks/useAccountData.ts b/src/app/hooks/useAccountData.ts
new file mode 100644 (file)
index 0000000..30e47b4
--- /dev/null
@@ -0,0 +1,22 @@
+import { useState, useCallback } from 'react';
+import { useMatrixClient } from './useMatrixClient';
+import { useAccountDataCallback } from './useAccountDataCallback';
+
+export function useAccountData(eventType: string) {
+  const mx = useMatrixClient();
+  const [event, setEvent] = useState(() => mx.getAccountData(eventType));
+
+  useAccountDataCallback(
+    mx,
+    useCallback(
+      (evt) => {
+        if (evt.getType() === eventType) {
+          setEvent(evt);
+        }
+      },
+      [eventType, setEvent]
+    )
+  );
+
+  return event;
+}
index 431f0b32f0d485f03fa2d499fd5804efc66027f7..5a0380f009d1d4a0dfd86487532e458bb9f3ceac 100644 (file)
@@ -1,4 +1,4 @@
-import { useCallback, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
 import { flushSync } from 'react-dom';
 import { useAlive } from './useAlive';
 
@@ -31,12 +31,10 @@ export type AsyncState<D, E = unknown> = AsyncIdle | AsyncLoading | AsyncSuccess
 
 export type AsyncCallback<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData>;
 
-export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
-  asyncCallback: AsyncCallback<TArgs, TData>
-): [AsyncState<TData, TError>, AsyncCallback<TArgs, TData>] => {
-  const [state, setState] = useState<AsyncState<TData, TError>>({
-    status: AsyncStatus.Idle,
-  });
+export const useAsync = <TData, TError, TArgs extends unknown[]>(
+  asyncCallback: AsyncCallback<TArgs, TData>,
+  onStateChange: (state: AsyncState<TData, TError>) => void
+): AsyncCallback<TArgs, TData> => {
   const alive = useAlive();
 
   // Tracks the request number.
@@ -53,7 +51,7 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
         flushSync(() => {
           // flushSync because
           // https://github.com/facebook/react/issues/26713#issuecomment-1872085134
-          setState({
+          onStateChange({
             status: AsyncStatus.Loading,
           });
         });
@@ -69,7 +67,7 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
         }
         if (alive()) {
           queueMicrotask(() => {
-            setState({
+            onStateChange({
               status: AsyncStatus.Success,
               data,
             });
@@ -83,7 +81,7 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
 
         if (alive()) {
           queueMicrotask(() => {
-            setState({
+            onStateChange({
               status: AsyncStatus.Error,
               error: e as TError,
             });
@@ -92,8 +90,32 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
         throw e;
       }
     },
-    [asyncCallback, alive]
+    [asyncCallback, alive, onStateChange]
   );
 
+  return callback;
+};
+
+export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
+  asyncCallback: AsyncCallback<TArgs, TData>
+): [AsyncState<TData, TError>, AsyncCallback<TArgs, TData>] => {
+  const [state, setState] = useState<AsyncState<TData, TError>>({
+    status: AsyncStatus.Idle,
+  });
+
+  const callback = useAsync(asyncCallback, setState);
+
   return [state, callback];
 };
+
+export const useAsyncCallbackValue = <TData, TError>(
+  asyncCallback: AsyncCallback<[], TData>
+): [AsyncState<TData, TError>, AsyncCallback<[], TData>] => {
+  const [state, load] = useAsyncCallback<TData, TError, []>(asyncCallback);
+
+  useEffect(() => {
+    load();
+  }, [load]);
+
+  return [state, load];
+};
diff --git a/src/app/hooks/useCrossSigning.ts b/src/app/hooks/useCrossSigning.ts
new file mode 100644 (file)
index 0000000..e3dc2c2
--- /dev/null
@@ -0,0 +1,9 @@
+import { AccountDataEvent, SecretAccountData } from '../../types/matrix/accountData';
+import { useAccountData } from './useAccountData';
+
+export const useCrossSigningActive = (): boolean => {
+  const masterEvent = useAccountData(AccountDataEvent.CrossSigningMaster);
+  const content = masterEvent?.getContent<SecretAccountData>();
+
+  return !!content;
+};
diff --git a/src/app/hooks/useCrossSigningStatus.js b/src/app/hooks/useCrossSigningStatus.js
deleted file mode 100644 (file)
index 990076e..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-/* eslint-disable import/prefer-default-export */
-import { useState, useEffect } from 'react';
-
-import { hasCrossSigningAccountData } from '../../util/matrixUtil';
-import { useMatrixClient } from './useMatrixClient';
-
-export function useCrossSigningStatus() {
-  const mx = useMatrixClient();
-  const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData(mx));
-
-  useEffect(() => {
-    if (isCSEnabled) return undefined;
-    const handleAccountData = (event) => {
-      if (event.getType() === 'm.cross_signing.master') {
-        setIsCSEnabled(true);
-      }
-    };
-
-    mx.on('accountData', handleAccountData);
-    return () => {
-      mx.removeListener('accountData', handleAccountData);
-    };
-  }, [mx, isCSEnabled]);
-  return isCSEnabled;
-}
index daec7cbe613512acfcfde4b3c5cc46b68f6bfa1b..5586ae8db17f413c804b2d80cfd76f10e618c8d0 100644 (file)
@@ -1,35 +1,77 @@
-/* eslint-disable import/prefer-default-export */
-import { useState, useEffect } from 'react';
-import { CryptoEvent, IMyDevice } from 'matrix-js-sdk';
-import { CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
+import { useEffect, useCallback, useMemo } from 'react';
+import { IMyDevice } from 'matrix-js-sdk';
+import { useQuery } from '@tanstack/react-query';
+import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
 import { useMatrixClient } from './useMatrixClient';
 
-export function useDeviceList() {
+export const useDeviceListChange = (
+  onChange: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated]
+) => {
   const mx = useMatrixClient();
-  const [deviceList, setDeviceList] = useState<IMyDevice[] | null>(null);
-
   useEffect(() => {
-    let isMounted = true;
-
-    const updateDevices = () =>
-      mx.getDevices().then((data) => {
-        if (!isMounted) return;
-        setDeviceList(data.devices || []);
-      });
-    updateDevices();
-
-    const handleDevicesUpdate: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated] = (users) => {
-      const userId = mx.getUserId();
-      if (userId && users.includes(userId)) {
-        updateDevices();
-      }
-    };
-
-    mx.on(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
+    mx.on(CryptoEvent.DevicesUpdated, onChange);
     return () => {
-      mx.removeListener(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
-      isMounted = false;
+      mx.removeListener(CryptoEvent.DevicesUpdated, onChange);
     };
+  }, [mx, onChange]);
+};
+
+const DEVICES_QUERY_KEY = ['devices'];
+
+export function useDeviceList(): [undefined | IMyDevice[], () => Promise<void>] {
+  const mx = useMatrixClient();
+
+  const fetchDevices = useCallback(async () => {
+    const data = await mx.getDevices();
+    return data.devices ?? [];
   }, [mx]);
-  return deviceList;
+
+  const { data: deviceList, refetch } = useQuery({
+    queryKey: DEVICES_QUERY_KEY,
+    queryFn: fetchDevices,
+    staleTime: 0,
+    gcTime: Infinity,
+    refetchOnMount: 'always',
+  });
+
+  const refreshDeviceList = useCallback(async () => {
+    await refetch();
+  }, [refetch]);
+
+  useDeviceListChange(
+    useCallback(
+      (users) => {
+        const userId = mx.getUserId();
+        if (userId && users.includes(userId)) {
+          refreshDeviceList();
+        }
+      },
+      [mx, refreshDeviceList]
+    )
+  );
+
+  return [deviceList ?? undefined, refreshDeviceList];
 }
+
+export const useDeviceIds = (devices: IMyDevice[] | undefined): string[] => {
+  const devicesId = useMemo(() => devices?.map((device) => device.device_id) ?? [], [devices]);
+
+  return devicesId;
+};
+
+export const useSplitCurrentDevice = (
+  devices: IMyDevice[] | undefined
+): [IMyDevice | undefined, IMyDevice[] | undefined] => {
+  const mx = useMatrixClient();
+  const currentDeviceId = mx.getDeviceId();
+  const currentDevice = useMemo(
+    () => devices?.find((d) => d.device_id === currentDeviceId),
+    [devices, currentDeviceId]
+  );
+  const otherDevices = useMemo(
+    () => devices?.filter((device) => device.device_id !== currentDeviceId),
+    [devices, currentDeviceId]
+  );
+
+  return [currentDevice, otherDevices];
+};
diff --git a/src/app/hooks/useDeviceVerificationStatus.ts b/src/app/hooks/useDeviceVerificationStatus.ts
new file mode 100644 (file)
index 0000000..9e723bd
--- /dev/null
@@ -0,0 +1,106 @@
+import { useCallback, useEffect, useState } from 'react';
+import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api';
+import { verifiedDevice } from '../utils/matrix-crypto';
+import { useAlive } from './useAlive';
+import { fulfilledPromiseSettledResult } from '../utils/common';
+import { useMatrixClient } from './useMatrixClient';
+import { useDeviceListChange } from './useDeviceList';
+
+export enum VerificationStatus {
+  Unknown,
+  Unverified,
+  Verified,
+  Unsupported,
+}
+
+export const useDeviceVerificationDetect = (
+  crypto: CryptoApi | undefined,
+  userId: string,
+  deviceId: string | undefined,
+  callback: (status: VerificationStatus) => void
+): void => {
+  const mx = useMatrixClient();
+
+  const updateStatus = useCallback(async () => {
+    if (crypto && deviceId) {
+      const data = await verifiedDevice(crypto, userId, deviceId);
+      if (data === null) {
+        callback(VerificationStatus.Unsupported);
+        return;
+      }
+      callback(data ? VerificationStatus.Verified : VerificationStatus.Unverified);
+      return;
+    }
+    callback(VerificationStatus.Unknown);
+  }, [crypto, deviceId, userId, callback]);
+
+  useEffect(() => {
+    updateStatus();
+  }, [mx, updateStatus, userId]);
+
+  useDeviceListChange(
+    useCallback(
+      (userIds) => {
+        if (userIds.includes(userId)) {
+          updateStatus();
+        }
+      },
+      [userId, updateStatus]
+    )
+  );
+};
+
+export const useDeviceVerificationStatus = (
+  crypto: CryptoApi | undefined,
+  userId: string,
+  deviceId: string | undefined
+): VerificationStatus => {
+  const [verificationStatus, setVerificationStatus] = useState(VerificationStatus.Unknown);
+
+  useDeviceVerificationDetect(crypto, userId, deviceId, setVerificationStatus);
+
+  return verificationStatus;
+};
+
+export const useUnverifiedDeviceCount = (
+  crypto: CryptoApi | undefined,
+  userId: string,
+  devices: string[]
+): number | undefined => {
+  const [unverifiedCount, setUnverifiedCount] = useState<number>();
+  const alive = useAlive();
+
+  const updateCount = useCallback(async () => {
+    let count = 0;
+    if (crypto) {
+      const promises = devices.map((deviceId) => verifiedDevice(crypto, userId, deviceId));
+      const result = await Promise.allSettled(promises);
+      const settledResult = fulfilledPromiseSettledResult(result);
+      settledResult.forEach((status) => {
+        if (status === false) {
+          count += 1;
+        }
+      });
+    }
+    if (alive()) {
+      setUnverifiedCount(count);
+    }
+  }, [crypto, userId, devices, alive]);
+
+  useDeviceListChange(
+    useCallback(
+      (userIds) => {
+        if (userIds.includes(userId)) {
+          updateCount();
+        }
+      },
+      [userId, updateCount]
+    )
+  );
+
+  useEffect(() => {
+    updateCount();
+  }, [updateCount]);
+
+  return unverifiedCount;
+};
index ab988e36b7f0cc58c1ce89fe0bbf7a370e7fe313..318d6cbaf03f5a5ea19380c9aa38e91e94b50ff3 100644 (file)
-import { ClientEvent, MatrixClient, MatrixEvent, Room, RoomStateEvent } from 'matrix-js-sdk';
-import { useEffect, useMemo } from 'react';
-import { getRelevantPacks, ImagePack, PackUsage } from '../plugins/custom-emoji';
+import { Room } from 'matrix-js-sdk';
+import { useCallback, useMemo, useState } from 'react';
 import { AccountDataEvent } from '../../types/matrix/accountData';
 import { StateEvent } from '../../types/matrix/room';
-import { useForceUpdate } from './useForceUpdate';
-
-export const useRelevantImagePacks = (
-  mx: MatrixClient,
-  usage: PackUsage,
-  rooms: Room[]
-): ImagePack[] => {
-  const [forceCount, forceUpdate] = useForceUpdate();
-
-  const relevantPacks = useMemo(
-    () => getRelevantPacks(mx, rooms).filter((pack) => pack.getImagesFor(usage).length > 0),
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [mx, usage, rooms, forceCount]
+import {
+  getGlobalImagePacks,
+  getRoomImagePack,
+  getRoomImagePacks,
+  getUserImagePack,
+  ImagePack,
+  ImageUsage,
+} from '../plugins/custom-emoji';
+import { useMatrixClient } from './useMatrixClient';
+import { useAccountDataCallback } from './useAccountDataCallback';
+import { useStateEventCallback } from './useStateEventCallback';
+
+export const useUserImagePack = (): ImagePack | undefined => {
+  const mx = useMatrixClient();
+  const [userPack, setUserPack] = useState(() => getUserImagePack(mx));
+
+  useAccountDataCallback(
+    mx,
+    useCallback(
+      (mEvent) => {
+        if (mEvent.getType() === AccountDataEvent.PoniesUserEmotes) {
+          setUserPack(getUserImagePack(mx));
+        }
+      },
+      [mx]
+    )
+  );
+
+  return userPack;
+};
+
+export const useGlobalImagePacks = (): ImagePack[] => {
+  const mx = useMatrixClient();
+  const [globalPacks, setGlobalPacks] = useState(() => getGlobalImagePacks(mx));
+
+  useAccountDataCallback(
+    mx,
+    useCallback(
+      (mEvent) => {
+        if (mEvent.getType() === AccountDataEvent.PoniesEmoteRooms) {
+          setGlobalPacks(getGlobalImagePacks(mx));
+        }
+      },
+      [mx]
+    )
   );
 
-  useEffect(() => {
-    const handleUpdate = (event: MatrixEvent) => {
-      if (
-        event.getType() === AccountDataEvent.PoniesEmoteRooms ||
-        event.getType() === AccountDataEvent.PoniesUserEmotes
-      ) {
-        forceUpdate();
-      }
-      const eventRoomId = event.getRoomId();
-      if (
-        eventRoomId &&
-        event.getType() === StateEvent.PoniesRoomEmotes &&
-        rooms.find((room) => room.roomId === eventRoomId)
-      ) {
-        forceUpdate();
-      }
-    };
-
-    mx.on(ClientEvent.AccountData, handleUpdate);
-    mx.on(RoomStateEvent.Events, handleUpdate);
-    return () => {
-      mx.removeListener(ClientEvent.AccountData, handleUpdate);
-      mx.removeListener(RoomStateEvent.Events, handleUpdate);
-    };
-  }, [mx, rooms, forceUpdate]);
+  useStateEventCallback(
+    mx,
+    useCallback(
+      (mEvent) => {
+        const eventType = mEvent.getType();
+        const roomId = mEvent.getRoomId();
+        const stateKey = mEvent.getStateKey();
+        if (eventType === StateEvent.PoniesRoomEmotes && roomId && typeof stateKey === 'string') {
+          const global = !!globalPacks.find(
+            (pack) =>
+              pack.address && pack.address.roomId === roomId && pack.address.stateKey === stateKey
+          );
+          if (global) {
+            setGlobalPacks(getGlobalImagePacks(mx));
+          }
+        }
+      },
+      [mx, globalPacks]
+    )
+  );
+
+  return globalPacks;
+};
+
+export const useRoomImagePack = (room: Room, stateKey: string): ImagePack | undefined => {
+  const mx = useMatrixClient();
+  const [roomPack, setRoomPack] = useState(() => getRoomImagePack(room, stateKey));
+
+  useStateEventCallback(
+    mx,
+    useCallback(
+      (mEvent) => {
+        if (
+          mEvent.getRoomId() === room.roomId &&
+          mEvent.getType() === StateEvent.PoniesRoomEmotes &&
+          mEvent.getStateKey() === stateKey
+        ) {
+          setRoomPack(getRoomImagePack(room, stateKey));
+        }
+      },
+      [room, stateKey]
+    )
+  );
+
+  return roomPack;
+};
+
+export const useRoomImagePacks = (room: Room): ImagePack[] => {
+  const mx = useMatrixClient();
+  const [roomPacks, setRoomPacks] = useState(() => getRoomImagePacks(room));
+
+  useStateEventCallback(
+    mx,
+    useCallback(
+      (mEvent) => {
+        if (
+          mEvent.getRoomId() === room.roomId &&
+          mEvent.getType() === StateEvent.PoniesRoomEmotes
+        ) {
+          setRoomPacks(getRoomImagePacks(room));
+        }
+      },
+      [room]
+    )
+  );
+
+  return roomPacks;
+};
+
+export const useRoomsImagePacks = (rooms: Room[]) => {
+  const mx = useMatrixClient();
+  const [roomPacks, setRoomPacks] = useState(() => rooms.flatMap(getRoomImagePacks));
+
+  useStateEventCallback(
+    mx,
+    useCallback(
+      (mEvent) => {
+        if (
+          rooms.find((room) => room.roomId === mEvent.getRoomId()) &&
+          mEvent.getType() === StateEvent.PoniesRoomEmotes
+        ) {
+          setRoomPacks(rooms.flatMap(getRoomImagePacks));
+        }
+      },
+      [rooms]
+    )
+  );
+
+  return roomPacks;
+};
+
+export const useRelevantImagePacks = (usage: ImageUsage, rooms: Room[]): ImagePack[] => {
+  const userPack = useUserImagePack();
+  const globalPacks = useGlobalImagePacks();
+  const roomsPacks = useRoomsImagePacks(rooms);
+
+  const relevantPacks = useMemo(() => {
+    const packs = userPack ? [userPack] : [];
+    const globalPackIds = new Set(globalPacks.map((pack) => pack.id));
+
+    const relPacks = packs.concat(
+      globalPacks,
+      roomsPacks.filter((pack) => !globalPackIds.has(pack.id))
+    );
+
+    return relPacks.filter((pack) => pack.getImages(usage).length > 0);
+  }, [userPack, globalPacks, roomsPacks, usage]);
 
   return relevantPacks;
 };
diff --git a/src/app/hooks/useKeyBackup.ts b/src/app/hooks/useKeyBackup.ts
new file mode 100644 (file)
index 0000000..e736e8d
--- /dev/null
@@ -0,0 +1,160 @@
+import {
+  BackupTrustInfo,
+  CryptoApi,
+  CryptoEvent,
+  CryptoEventHandlerMap,
+  KeyBackupInfo,
+} from 'matrix-js-sdk/lib/crypto-api';
+import { useCallback, useEffect, useState } from 'react';
+import { useMatrixClient } from './useMatrixClient';
+import { useAlive } from './useAlive';
+
+export const useKeyBackupStatusChange = (
+  onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupStatus]
+) => {
+  const mx = useMatrixClient();
+
+  useEffect(() => {
+    mx.on(CryptoEvent.KeyBackupStatus, onChange);
+    return () => {
+      mx.removeListener(CryptoEvent.KeyBackupStatus, onChange);
+    };
+  }, [mx, onChange]);
+};
+
+export const useKeyBackupStatus = (crypto: CryptoApi): boolean => {
+  const alive = useAlive();
+  const [status, setStatus] = useState(false);
+
+  useEffect(() => {
+    crypto.getActiveSessionBackupVersion().then((v) => {
+      if (alive()) {
+        setStatus(typeof v === 'string');
+      }
+    });
+  }, [crypto, alive]);
+
+  useKeyBackupStatusChange(setStatus);
+
+  return status;
+};
+
+export const useKeyBackupSessionsRemainingChange = (
+  onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupSessionsRemaining]
+) => {
+  const mx = useMatrixClient();
+
+  useEffect(() => {
+    mx.on(CryptoEvent.KeyBackupSessionsRemaining, onChange);
+    return () => {
+      mx.removeListener(CryptoEvent.KeyBackupSessionsRemaining, onChange);
+    };
+  }, [mx, onChange]);
+};
+
+export const useKeyBackupFailedChange = (
+  onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupFailed]
+) => {
+  const mx = useMatrixClient();
+
+  useEffect(() => {
+    mx.on(CryptoEvent.KeyBackupFailed, onChange);
+    return () => {
+      mx.removeListener(CryptoEvent.KeyBackupFailed, onChange);
+    };
+  }, [mx, onChange]);
+};
+
+export const useKeyBackupDecryptionKeyCached = (
+  onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupDecryptionKeyCached]
+) => {
+  const mx = useMatrixClient();
+
+  useEffect(() => {
+    mx.on(CryptoEvent.KeyBackupDecryptionKeyCached, onChange);
+    return () => {
+      mx.removeListener(CryptoEvent.KeyBackupDecryptionKeyCached, onChange);
+    };
+  }, [mx, onChange]);
+};
+
+export const useKeyBackupSync = (): [number, string | undefined] => {
+  const [remaining, setRemaining] = useState(0);
+  const [failure, setFailure] = useState<string>();
+
+  useKeyBackupSessionsRemainingChange(
+    useCallback((count) => {
+      setRemaining(count);
+      setFailure(undefined);
+    }, [])
+  );
+
+  useKeyBackupFailedChange(
+    useCallback((f) => {
+      if (typeof f === 'string') {
+        setFailure(f);
+        setRemaining(0);
+      }
+    }, [])
+  );
+
+  return [remaining, failure];
+};
+
+export const useKeyBackupInfo = (crypto: CryptoApi): KeyBackupInfo | undefined | null => {
+  const alive = useAlive();
+  const [info, setInfo] = useState<KeyBackupInfo | null>();
+
+  const fetchInfo = useCallback(() => {
+    crypto.getKeyBackupInfo().then((i) => {
+      if (alive()) {
+        setInfo(i);
+      }
+    });
+  }, [crypto, alive]);
+
+  useEffect(() => {
+    fetchInfo();
+  }, [fetchInfo]);
+
+  useKeyBackupStatusChange(fetchInfo);
+
+  useKeyBackupSessionsRemainingChange(
+    useCallback(
+      (remainingCount) => {
+        if (remainingCount === 0) {
+          fetchInfo();
+        }
+      },
+      [fetchInfo]
+    )
+  );
+
+  return info;
+};
+
+export const useKeyBackupTrust = (
+  crypto: CryptoApi,
+  backupInfo: KeyBackupInfo
+): BackupTrustInfo | undefined => {
+  const alive = useAlive();
+  const [trust, setTrust] = useState<BackupTrustInfo>();
+
+  const fetchTrust = useCallback(() => {
+    crypto.isKeyBackupTrusted(backupInfo).then((t) => {
+      if (alive()) {
+        setTrust(t);
+      }
+    });
+  }, [crypto, alive, backupInfo]);
+
+  useEffect(() => {
+    fetchTrust();
+  }, [fetchTrust]);
+
+  useKeyBackupStatusChange(fetchTrust);
+
+  useKeyBackupDecryptionKeyCached(fetchTrust);
+
+  return trust;
+};
diff --git a/src/app/hooks/useMessageLayout.ts b/src/app/hooks/useMessageLayout.ts
new file mode 100644 (file)
index 0000000..06fb9a5
--- /dev/null
@@ -0,0 +1,26 @@
+import { useMemo } from 'react';
+import { MessageLayout } from '../state/settings';
+
+export type MessageLayoutItem = {
+  name: string;
+  layout: MessageLayout;
+};
+
+export const useMessageLayoutItems = (): MessageLayoutItem[] =>
+  useMemo(
+    () => [
+      {
+        layout: MessageLayout.Modern,
+        name: 'Modern',
+      },
+      {
+        layout: MessageLayout.Compact,
+        name: 'Compact',
+      },
+      {
+        layout: MessageLayout.Bubble,
+        name: 'Bubble',
+      },
+    ],
+    []
+  );
diff --git a/src/app/hooks/useMessageSpacing.ts b/src/app/hooks/useMessageSpacing.ts
new file mode 100644 (file)
index 0000000..62325b1
--- /dev/null
@@ -0,0 +1,38 @@
+import { useMemo } from 'react';
+import { MessageSpacing } from '../state/settings';
+
+export type MessageSpacingItem = {
+  name: string;
+  spacing: MessageSpacing;
+};
+
+export const useMessageSpacingItems = (): MessageSpacingItem[] =>
+  useMemo(
+    () => [
+      {
+        spacing: '0',
+        name: 'None',
+      },
+      {
+        spacing: '100',
+        name: 'Ultra Small',
+      },
+      {
+        spacing: '200',
+        name: 'Extra Small',
+      },
+      {
+        spacing: '300',
+        name: 'Small',
+      },
+      {
+        spacing: '400',
+        name: 'Normal',
+      },
+      {
+        spacing: '500',
+        name: 'Large',
+      },
+    ],
+    []
+  );
diff --git a/src/app/hooks/useNotificationMode.ts b/src/app/hooks/useNotificationMode.ts
new file mode 100644 (file)
index 0000000..1c2267e
--- /dev/null
@@ -0,0 +1,66 @@
+import { PushRuleAction, PushRuleActionName, TweakName } from 'matrix-js-sdk';
+import { useCallback, useMemo } from 'react';
+
+export enum NotificationMode {
+  OFF = 'OFF',
+  Notify = 'Notify',
+  NotifyLoud = 'NotifyLoud',
+}
+
+export type NotificationModeOptions = {
+  soundValue?: string;
+  highlight?: boolean;
+};
+export const getNotificationModeActions = (
+  mode: NotificationMode,
+  options?: NotificationModeOptions
+): PushRuleAction[] => {
+  if (mode === NotificationMode.OFF) return [];
+
+  const actions: PushRuleAction[] = [PushRuleActionName.Notify];
+
+  if (mode === NotificationMode.NotifyLoud) {
+    actions.push({
+      set_tweak: TweakName.Sound,
+      value: options?.soundValue ?? 'default',
+    });
+  }
+
+  if (options?.highlight) {
+    actions.push({
+      set_tweak: TweakName.Highlight,
+      value: true,
+    });
+  }
+
+  return actions;
+};
+
+export type GetNotificationModeCallback = (mode: NotificationMode) => PushRuleAction[];
+export const useNotificationModeActions = (
+  options?: NotificationModeOptions
+): GetNotificationModeCallback => {
+  const getAction: GetNotificationModeCallback = useCallback(
+    (mode) => getNotificationModeActions(mode, options),
+    [options]
+  );
+
+  return getAction;
+};
+
+export const useNotificationActionsMode = (actions: PushRuleAction[]): NotificationMode => {
+  const mode: NotificationMode = useMemo(() => {
+    const soundTweak = actions.find(
+      (action) => typeof action === 'object' && action.set_tweak === TweakName.Sound
+    );
+    const notify = actions.find(
+      (action) => typeof action === 'string' && action === PushRuleActionName.Notify
+    );
+
+    if (notify && soundTweak) return NotificationMode.NotifyLoud;
+    if (notify) return NotificationMode.Notify;
+    return NotificationMode.OFF;
+  }, [actions]);
+
+  return mode;
+};
diff --git a/src/app/hooks/useObjectURL.ts b/src/app/hooks/useObjectURL.ts
new file mode 100644 (file)
index 0000000..8c6cf3f
--- /dev/null
@@ -0,0 +1,17 @@
+import { useEffect, useMemo } from 'react';
+
+export const useObjectURL = (object?: Blob): string | undefined => {
+  const url = useMemo(() => {
+    if (object) return URL.createObjectURL(object);
+    return undefined;
+  }, [object]);
+
+  useEffect(
+    () => () => {
+      if (url) URL.revokeObjectURL(url);
+    },
+    [url]
+  );
+
+  return url;
+};
diff --git a/src/app/hooks/usePushRule.ts b/src/app/hooks/usePushRule.ts
new file mode 100644 (file)
index 0000000..ec87c21
--- /dev/null
@@ -0,0 +1,71 @@
+import {
+  IPushRule,
+  IPushRules,
+  PushRuleAction,
+  PushRuleCondition,
+  PushRuleKind,
+  RuleId,
+} from 'matrix-js-sdk';
+import { useMemo } from 'react';
+
+export type PushRuleData = {
+  kind: PushRuleKind;
+  pushRule: IPushRule;
+};
+
+export const makePushRuleData = (
+  kind: PushRuleKind,
+  ruleId: RuleId,
+  actions: PushRuleAction[],
+  conditions?: PushRuleCondition[],
+  pattern?: string,
+  enabled?: boolean,
+  _default?: boolean
+): PushRuleData => ({
+  kind,
+  pushRule: {
+    rule_id: ruleId,
+    default: _default ?? true,
+    enabled: enabled ?? true,
+    pattern,
+    conditions,
+    actions,
+  },
+});
+
+export const orderedPushRuleKinds: PushRuleKind[] = [
+  PushRuleKind.Override,
+  PushRuleKind.ContentSpecific,
+  PushRuleKind.RoomSpecific,
+  PushRuleKind.SenderSpecific,
+  PushRuleKind.Underride,
+];
+
+export const getPushRule = (
+  pushRules: IPushRules,
+  ruleId: RuleId | string
+): PushRuleData | undefined => {
+  const { global } = pushRules;
+
+  let ruleData: PushRuleData | undefined;
+
+  orderedPushRuleKinds.some((kind) => {
+    const rules = global[kind];
+    const pushRule = rules?.find((r) => r.rule_id === ruleId);
+    if (pushRule) {
+      ruleData = {
+        kind,
+        pushRule,
+      };
+      return true;
+    }
+    return false;
+  });
+
+  return ruleData;
+};
+
+export const usePushRule = (
+  pushRules: IPushRules,
+  ruleId: RuleId | string
+): PushRuleData | undefined => useMemo(() => getPushRule(pushRules, ruleId), [pushRules, ruleId]);
diff --git a/src/app/hooks/useRestoreBackupOnVerification.ts b/src/app/hooks/useRestoreBackupOnVerification.ts
new file mode 100644 (file)
index 0000000..74497b0
--- /dev/null
@@ -0,0 +1,24 @@
+import { useSetAtom } from 'jotai';
+import { useCallback } from 'react';
+import { backupRestoreProgressAtom } from '../state/backupRestore';
+import { useMatrixClient } from './useMatrixClient';
+import { useKeyBackupDecryptionKeyCached } from './useKeyBackup';
+
+export const useRestoreBackupOnVerification = () => {
+  const setRestoreProgress = useSetAtom(backupRestoreProgressAtom);
+
+  const mx = useMatrixClient();
+
+  useKeyBackupDecryptionKeyCached(
+    useCallback(() => {
+      const crypto = mx.getCrypto();
+      if (!crypto) return;
+
+      crypto.restoreKeyBackup({
+        progressCallback(progress) {
+          setRestoreProgress(progress);
+        },
+      });
+    }, [mx, setRestoreProgress])
+  );
+};
diff --git a/src/app/hooks/useSecretStorage.ts b/src/app/hooks/useSecretStorage.ts
new file mode 100644 (file)
index 0000000..10eabeb
--- /dev/null
@@ -0,0 +1,22 @@
+import {
+  AccountDataEvent,
+  SecretStorageDefaultKeyContent,
+  SecretStorageKeyContent,
+} from '../../types/matrix/accountData';
+import { useAccountData } from './useAccountData';
+
+export const getSecretStorageKeyEventType = (key: string): string => `m.secret_storage.key.${key}`;
+
+export const useSecretStorageDefaultKeyId = (): string | undefined => {
+  const defaultKeyEvent = useAccountData(AccountDataEvent.SecretStorageDefaultKey);
+  const defaultKeyId = defaultKeyEvent?.getContent<SecretStorageDefaultKeyContent>().key;
+
+  return defaultKeyId;
+};
+
+export const useSecretStorageKeyContent = (keyId: string): SecretStorageKeyContent | undefined => {
+  const keyEvent = useAccountData(getSecretStorageKeyEventType(keyId));
+  const secretStorageKey = keyEvent?.getContent<SecretStorageKeyContent>();
+
+  return secretStorageKey;
+};
diff --git a/src/app/hooks/useTextAreaIntent.ts b/src/app/hooks/useTextAreaIntent.ts
new file mode 100644 (file)
index 0000000..113316e
--- /dev/null
@@ -0,0 +1,58 @@
+import { isKeyHotkey } from 'is-hotkey';
+import { KeyboardEventHandler, useCallback } from 'react';
+import { Cursor, Intent, Operations, TextArea } from '../plugins/text-area';
+
+export const useTextAreaIntentHandler = (
+  textArea: TextArea,
+  operations: Operations,
+  intent: Intent
+) => {
+  const handler: KeyboardEventHandler<HTMLTextAreaElement> = useCallback(
+    (evt) => {
+      const target = evt.currentTarget;
+
+      if (isKeyHotkey('tab', evt)) {
+        evt.preventDefault();
+
+        const cursor = Cursor.fromTextAreaElement(target);
+        if (textArea.selection(cursor)) {
+          operations.select(intent.moveForward(cursor));
+        } else {
+          operations.deselect(operations.insert(cursor, intent.str));
+        }
+
+        target.focus();
+      }
+      if (isKeyHotkey('shift+tab', evt)) {
+        evt.preventDefault();
+        const cursor = Cursor.fromTextAreaElement(target);
+        const intentCursor = intent.moveBackward(cursor);
+        if (textArea.selection(cursor)) {
+          operations.select(intentCursor);
+        } else {
+          operations.deselect(intentCursor);
+        }
+
+        target.focus();
+      }
+      if (isKeyHotkey('enter', evt) || isKeyHotkey('shift+enter', evt)) {
+        evt.preventDefault();
+        const cursor = Cursor.fromTextAreaElement(target);
+        operations.select(intent.addNewLine(cursor));
+      }
+      if (isKeyHotkey('mod+enter', evt)) {
+        evt.preventDefault();
+        const cursor = Cursor.fromTextAreaElement(target);
+        operations.select(intent.addNextLine(cursor));
+      }
+      if (isKeyHotkey('mod+shift+enter', evt)) {
+        evt.preventDefault();
+        const cursor = Cursor.fromTextAreaElement(target);
+        operations.select(intent.addPreviousLine(cursor));
+      }
+    },
+    [textArea, operations, intent]
+  );
+
+  return handler;
+};
diff --git a/src/app/hooks/useTheme.ts b/src/app/hooks/useTheme.ts
new file mode 100644 (file)
index 0000000..a29af01
--- /dev/null
@@ -0,0 +1,74 @@
+import { lightTheme } from 'folds';
+import { useEffect, useMemo, useState } from 'react';
+import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
+import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
+
+export enum ThemeKind {
+  Light = 'light',
+  Dark = 'dark',
+}
+
+export type Theme = {
+  id: string;
+  kind: ThemeKind;
+  classNames: string[];
+};
+
+export const LightTheme: Theme = {
+  id: 'light-theme',
+  kind: ThemeKind.Light,
+  classNames: [lightTheme, onLightFontWeight, 'prism-light'],
+};
+
+export const SilverTheme: Theme = {
+  id: 'silver-theme',
+  kind: ThemeKind.Light,
+  classNames: ['silver-theme', silverTheme, onLightFontWeight, 'prism-light'],
+};
+export const DarkTheme: Theme = {
+  id: 'dark-theme',
+  kind: ThemeKind.Dark,
+  classNames: ['dark-theme', darkTheme, onDarkFontWeight, 'prism-dark'],
+};
+export const ButterTheme: Theme = {
+  id: 'butter-theme',
+  kind: ThemeKind.Dark,
+  classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'],
+};
+
+export const useThemes = (): Theme[] => {
+  const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
+
+  return themes;
+};
+
+export const useThemeNames = (): Record<string, string> =>
+  useMemo(
+    () => ({
+      [LightTheme.id]: 'Light',
+      [SilverTheme.id]: 'Silver',
+      [DarkTheme.id]: 'Dark',
+      [ButterTheme.id]: 'Butter',
+    }),
+    []
+  );
+
+export const useSystemThemeKind = (): ThemeKind => {
+  const darkModeQueryList = useMemo(() => window.matchMedia('(prefers-color-scheme: dark)'), []);
+  const [themeKind, setThemeKind] = useState<ThemeKind>(
+    darkModeQueryList.matches ? ThemeKind.Dark : ThemeKind.Light
+  );
+
+  useEffect(() => {
+    const handleMediaQueryChange = () => {
+      setThemeKind(darkModeQueryList.matches ? ThemeKind.Dark : ThemeKind.Light);
+    };
+
+    darkModeQueryList.addEventListener('change', handleMediaQueryChange);
+    return () => {
+      darkModeQueryList.removeEventListener('change', handleMediaQueryChange);
+    };
+  }, [darkModeQueryList, setThemeKind]);
+
+  return themeKind;
+};
index 22acd6bac3ebb3370b63df33b1bd026b2f37943d..0e6c0ddcad7a83f63fc6cc9640e6dd87a71cac41 100644 (file)
@@ -1,4 +1,4 @@
-import { AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk';
+import { AuthType, IAuthData, MatrixError, UIAFlow } from 'matrix-js-sdk';
 import { useCallback, useMemo } from 'react';
 import {
   getSupportedUIAFlows,
@@ -94,3 +94,12 @@ export const useUIAFlow = (authData: IAuthData, uiaFlow: UIAFlow): UIAFlowInterf
     getStageInfo,
   };
 };
+
+export const useUIAMatrixError = (
+  error?: MatrixError
+): [undefined, undefined] | [IAuthData, undefined] | [undefined, MatrixError] => {
+  if (!error) return [undefined, undefined];
+  if (error.httpStatus === 401) return [error.data as IAuthData, undefined];
+
+  return [undefined, error];
+};
diff --git a/src/app/hooks/useUserProfile.ts b/src/app/hooks/useUserProfile.ts
new file mode 100644 (file)
index 0000000..c7cb748
--- /dev/null
@@ -0,0 +1,51 @@
+import { useEffect, useState } from 'react';
+import { UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
+import { useMatrixClient } from './useMatrixClient';
+
+export type UserProfile = {
+  avatarUrl?: string;
+  displayName?: string;
+};
+export const useUserProfile = (userId: string): UserProfile => {
+  const mx = useMatrixClient();
+
+  const [profile, setProfile] = useState<UserProfile>(() => {
+    const user = mx.getUser(userId);
+    return {
+      avatarUrl: user?.avatarUrl,
+      displayName: user?.displayName,
+    };
+  });
+
+  useEffect(() => {
+    const user = mx.getUser(userId);
+    const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (event, myUser) => {
+      setProfile((cp) => ({
+        ...cp,
+        avatarUrl: myUser.avatarUrl,
+      }));
+    };
+    const onDisplayNameChange: UserEventHandlerMap[UserEvent.DisplayName] = (event, myUser) => {
+      setProfile((cp) => ({
+        ...cp,
+        displayName: myUser.displayName,
+      }));
+    };
+
+    mx.getProfileInfo(userId).then((info) =>
+      setProfile({
+        avatarUrl: info.avatar_url,
+        displayName: info.displayname,
+      })
+    );
+
+    user?.on(UserEvent.AvatarUrl, onAvatarChange);
+    user?.on(UserEvent.DisplayName, onDisplayNameChange);
+    return () => {
+      user?.removeListener(UserEvent.AvatarUrl, onAvatarChange);
+      user?.removeListener(UserEvent.DisplayName, onDisplayNameChange);
+    };
+  }, [mx, userId]);
+
+  return profile;
+};
diff --git a/src/app/hooks/useUserTrustStatusChange.ts b/src/app/hooks/useUserTrustStatusChange.ts
new file mode 100644 (file)
index 0000000..56a9e38
--- /dev/null
@@ -0,0 +1,16 @@
+import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto-api';
+import { useEffect } from 'react';
+import { useMatrixClient } from './useMatrixClient';
+
+export const useUserTrustStatusChange = (
+  onChange: CryptoEventHandlerMap[CryptoEvent.UserTrustStatusChanged]
+) => {
+  const mx = useMatrixClient();
+
+  useEffect(() => {
+    mx.on(CryptoEvent.UserTrustStatusChanged, onChange);
+    return () => {
+      mx.removeListener(CryptoEvent.UserTrustStatusChanged, onChange);
+    };
+  }, [mx, onChange]);
+};
diff --git a/src/app/hooks/useVerificationRequest.ts b/src/app/hooks/useVerificationRequest.ts
new file mode 100644 (file)
index 0000000..5feb523
--- /dev/null
@@ -0,0 +1,87 @@
+import { useCallback, useEffect, useState } from 'react';
+import {
+  CryptoEvent,
+  CryptoEventHandlerMap,
+  VerificationPhase,
+  VerificationRequest,
+  VerificationRequestEvent,
+  VerificationRequestEventHandlerMap,
+  Verifier,
+  VerifierEvent,
+  VerifierEventHandlerMap,
+} from 'matrix-js-sdk/lib/crypto-api';
+import { useMatrixClient } from './useMatrixClient';
+
+export const useVerificationRequestReceived = (
+  onRequest: CryptoEventHandlerMap[CryptoEvent.VerificationRequestReceived]
+) => {
+  const mx = useMatrixClient();
+
+  useEffect(() => {
+    mx.on(CryptoEvent.VerificationRequestReceived, onRequest);
+    return () => {
+      mx.removeListener(CryptoEvent.VerificationRequestReceived, onRequest);
+    };
+  }, [mx, onRequest]);
+};
+
+export const useVerificationRequestChange = (
+  request: VerificationRequest,
+  onChange: VerificationRequestEventHandlerMap[VerificationRequestEvent.Change]
+) => {
+  useEffect(() => {
+    request.on(VerificationRequestEvent.Change, onChange);
+    return () => {
+      request.removeListener(VerificationRequestEvent.Change, onChange);
+    };
+  }, [request, onChange]);
+};
+
+export const useVerificationRequestPhase = (request: VerificationRequest): VerificationPhase => {
+  const [phase, setPhase] = useState(() => request.phase);
+
+  useVerificationRequestChange(
+    request,
+    useCallback(() => {
+      setPhase(request.phase);
+    }, [request])
+  );
+
+  return phase;
+};
+
+export const useVerifierCancel = (
+  verifier: Verifier,
+  onCallback: VerifierEventHandlerMap[VerifierEvent.Cancel]
+) => {
+  useEffect(() => {
+    verifier.on(VerifierEvent.Cancel, onCallback);
+    return () => {
+      verifier.removeListener(VerifierEvent.Cancel, onCallback);
+    };
+  }, [verifier, onCallback]);
+};
+
+export const useVerifierShowSas = (
+  verifier: Verifier,
+  onCallback: VerifierEventHandlerMap[VerifierEvent.ShowSas]
+) => {
+  useEffect(() => {
+    verifier.on(VerifierEvent.ShowSas, onCallback);
+    return () => {
+      verifier.removeListener(VerifierEvent.ShowSas, onCallback);
+    };
+  }, [verifier, onCallback]);
+};
+
+export const useVerifierShowReciprocateQr = (
+  verifier: Verifier,
+  onCallback: VerifierEventHandlerMap[VerifierEvent.ShowReciprocateQr]
+) => {
+  useEffect(() => {
+    verifier.on(VerifierEvent.ShowReciprocateQr, onCallback);
+    return () => {
+      verifier.removeListener(VerifierEvent.ShowReciprocateQr, onCallback);
+    };
+  }, [verifier, onCallback]);
+};
diff --git a/src/app/molecules/global-notification/GlobalNotification.jsx b/src/app/molecules/global-notification/GlobalNotification.jsx
deleted file mode 100644 (file)
index b115c9d..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-import React from 'react';
-
-import { openReusableContextMenu } from '../../../client/action/navigation';
-import { getEventCords } from '../../../util/common';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
-import SettingTile from '../setting-tile/SettingTile';
-
-import NotificationSelector from './NotificationSelector';
-
-import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
-
-import { useAccountData } from '../../hooks/useAccountData';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-export const notifType = {
-  ON: 'on',
-  OFF: 'off',
-  NOISY: 'noisy',
-};
-export const typeToLabel = {
-  [notifType.ON]: 'On',
-  [notifType.OFF]: 'Off',
-  [notifType.NOISY]: 'Noisy',
-};
-Object.freeze(notifType);
-
-const DM = '.m.rule.room_one_to_one';
-const ENC_DM = '.m.rule.encrypted_room_one_to_one';
-const ROOM = '.m.rule.message';
-const ENC_ROOM = '.m.rule.encrypted';
-
-export function getActionType(rule) {
-  const { actions } = rule;
-  if (actions.find((action) => action?.set_tweak === 'sound')) return notifType.NOISY;
-  if (actions.find((action) => action?.set_tweak === 'highlight')) return notifType.ON;
-  if (actions.find((action) => action === 'dont_notify')) return notifType.OFF;
-  return notifType.OFF;
-}
-
-export function getTypeActions(type, highlightValue = false) {
-  if (type === notifType.OFF) return ['dont_notify'];
-
-  const highlight = { set_tweak: 'highlight' };
-  if (typeof highlightValue === 'boolean') highlight.value = highlightValue;
-  if (type === notifType.ON) return ['notify', highlight];
-
-  const sound = { set_tweak: 'sound', value: 'default' };
-  return ['notify', sound, highlight];
-}
-
-function useGlobalNotif() {
-  const mx = useMatrixClient();
-  const pushRules = useAccountData('m.push_rules')?.getContent();
-  const underride = pushRules?.global?.underride ?? [];
-  const rulesToType = {
-    [DM]: notifType.ON,
-    [ENC_DM]: notifType.ON,
-    [ROOM]: notifType.NOISY,
-    [ENC_ROOM]: notifType.NOISY,
-  };
-
-  const getRuleCondition = (rule) => {
-    const condition = [];
-    if (rule === DM || rule === ENC_DM) {
-      condition.push({ kind: 'room_member_count', is: '2' });
-    }
-    condition.push({
-      kind: 'event_match',
-      key: 'type',
-      pattern: [ENC_DM, ENC_ROOM].includes(rule) ? 'm.room.encrypted' : 'm.room.message',
-    });
-    return condition;
-  };
-
-  const setRule = (rule, type) => {
-    const content = pushRules ?? {};
-    if (!content.global) content.global = {};
-    if (!content.global.underride) content.global.underride = [];
-    const ur = content.global.underride;
-    let ruleContent = ur.find((action) => action?.rule_id === rule);
-    if (!ruleContent) {
-      ruleContent = {
-        conditions: getRuleCondition(type),
-        actions: [],
-        rule_id: rule,
-        default: true,
-        enabled: true,
-      };
-      ur.push(ruleContent);
-    }
-    ruleContent.actions = getTypeActions(type);
-
-    mx.setAccountData('m.push_rules', content);
-  };
-
-  const dmRule = underride.find((rule) => rule.rule_id === DM);
-  const encDmRule = underride.find((rule) => rule.rule_id === ENC_DM);
-  const roomRule = underride.find((rule) => rule.rule_id === ROOM);
-  const encRoomRule = underride.find((rule) => rule.rule_id === ENC_ROOM);
-
-  if (dmRule) rulesToType[DM] = getActionType(dmRule);
-  if (encDmRule) rulesToType[ENC_DM] = getActionType(encDmRule);
-  if (roomRule) rulesToType[ROOM] = getActionType(roomRule);
-  if (encRoomRule) rulesToType[ENC_ROOM] = getActionType(encRoomRule);
-
-  return [rulesToType, setRule];
-}
-
-function GlobalNotification() {
-  const [rulesToType, setRule] = useGlobalNotif();
-
-  const onSelect = (evt, rule) => {
-    openReusableContextMenu(
-      'bottom',
-      getEventCords(evt, '.btn-surface'),
-      (requestClose) => (
-        <NotificationSelector
-          value={rulesToType[rule]}
-          onSelect={(value) => {
-            if (rulesToType[rule] !== value) setRule(rule, value);
-            requestClose();
-          }}
-        />
-      ),
-    );
-  };
-
-  return (
-    <div className="global-notification">
-      <MenuHeader>Global Notifications</MenuHeader>
-      <SettingTile
-        title="Direct messages"
-        options={(
-          <Button onClick={(evt) => onSelect(evt, DM)} iconSrc={ChevronBottomIC}>
-            { typeToLabel[rulesToType[DM]] }
-          </Button>
-        )}
-        content={<Text variant="b3">Default notification settings for all direct message.</Text>}
-      />
-      <SettingTile
-        title="Encrypted direct messages"
-        options={(
-          <Button onClick={(evt) => onSelect(evt, ENC_DM)} iconSrc={ChevronBottomIC}>
-            {typeToLabel[rulesToType[ENC_DM]]}
-          </Button>
-        )}
-        content={<Text variant="b3">Default notification settings for all encrypted direct message.</Text>}
-      />
-      <SettingTile
-        title="Rooms messages"
-        options={(
-          <Button onClick={(evt) => onSelect(evt, ROOM)} iconSrc={ChevronBottomIC}>
-            {typeToLabel[rulesToType[ROOM]]}
-          </Button>
-        )}
-        content={<Text variant="b3">Default notification settings for all room message.</Text>}
-      />
-      <SettingTile
-        title="Encrypted rooms messages"
-        options={(
-          <Button onClick={(evt) => onSelect(evt, ENC_ROOM)} iconSrc={ChevronBottomIC}>
-            {typeToLabel[rulesToType[ENC_ROOM]]}
-          </Button>
-        )}
-        content={<Text variant="b3">Default notification settings for all encrypted room message.</Text>}
-      />
-    </div>
-  );
-}
-
-export default GlobalNotification;
diff --git a/src/app/molecules/global-notification/IgnoreUserList.jsx b/src/app/molecules/global-notification/IgnoreUserList.jsx
deleted file mode 100644 (file)
index 1b44a04..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-import React from 'react';
-import './IgnoreUserList.scss';
-
-import * as roomActions from '../../../client/action/room';
-
-import Text from '../../atoms/text/Text';
-import Chip from '../../atoms/chip/Chip';
-import Input from '../../atoms/input/Input';
-import Button from '../../atoms/button/Button';
-import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
-import SettingTile from '../setting-tile/SettingTile';
-
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-
-import { useAccountData } from '../../hooks/useAccountData';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-function IgnoreUserList() {
-  useAccountData('m.ignored_user_list');
-  const mx = useMatrixClient();
-  const ignoredUsers = mx.getIgnoredUsers();
-
-  const handleSubmit = (evt) => {
-    evt.preventDefault();
-    const { ignoreInput } = evt.target.elements;
-    const value = ignoreInput.value.trim();
-    const userIds = value.split(' ').filter((v) => v.match(/^@\S+:\S+$/));
-    if (userIds.length === 0) return;
-    ignoreInput.value = '';
-    roomActions.ignore(mx, userIds);
-  };
-
-  return (
-    <div className="ignore-user-list">
-      <MenuHeader>Ignored users</MenuHeader>
-      <SettingTile
-        title="Ignore user"
-        content={(
-          <div className="ignore-user-list__users">
-            <Text variant="b3">Ignore userId if you do not want to receive their messages or invites.</Text>
-            <form onSubmit={handleSubmit}>
-              <Input name="ignoreInput" required />
-              <Button variant="primary" type="submit">Ignore</Button>
-            </form>
-            {ignoredUsers.length > 0 && (
-              <div>
-                {ignoredUsers.map((uId) => (
-                  <Chip
-                    iconSrc={CrossIC}
-                    key={uId}
-                    text={uId}
-                    iconColor={CrossIC}
-                    onClick={() => roomActions.unignore(mx, [uId])}
-                  />
-                ))}
-              </div>
-            )}
-          </div>
-        )}
-      />
-    </div>
-  );
-}
-
-export default IgnoreUserList;
diff --git a/src/app/molecules/global-notification/IgnoreUserList.scss b/src/app/molecules/global-notification/IgnoreUserList.scss
deleted file mode 100644 (file)
index 9283155..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-.ignore-user-list {
-  &__users {
-    & form,
-    & > div:last-child {
-      display: flex;
-      flex-wrap: wrap;
-      gap: var(--sp-tight);
-    }
-
-    & form {
-      margin: var(--sp-extra-tight) 0 var(--sp-normal);
-      .input-container {
-        flex-grow: 1;
-      }
-    }
-  }
-}
\ No newline at end of file
diff --git a/src/app/molecules/global-notification/KeywordNotification.jsx b/src/app/molecules/global-notification/KeywordNotification.jsx
deleted file mode 100644 (file)
index 7f7e4de..0000000
+++ /dev/null
@@ -1,239 +0,0 @@
-import React from 'react';
-import './KeywordNotification.scss';
-
-import { openReusableContextMenu } from '../../../client/action/navigation';
-import { getEventCords } from '../../../util/common';
-
-import Text from '../../atoms/text/Text';
-import Chip from '../../atoms/chip/Chip';
-import Input from '../../atoms/input/Input';
-import Button from '../../atoms/button/Button';
-import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
-import SettingTile from '../setting-tile/SettingTile';
-
-import NotificationSelector from './NotificationSelector';
-
-import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-
-import { useAccountData } from '../../hooks/useAccountData';
-import {
-  notifType, typeToLabel, getActionType, getTypeActions,
-} from './GlobalNotification';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-const DISPLAY_NAME = '.m.rule.contains_display_name';
-const ROOM_PING = '.m.rule.roomnotif';
-const USERNAME = '.m.rule.contains_user_name';
-const KEYWORD = 'keyword';
-
-function useKeywordNotif() {
-  const mx = useMatrixClient();
-  const pushRules = useAccountData('m.push_rules')?.getContent();
-  const override = pushRules?.global?.override ?? [];
-  const content = pushRules?.global?.content ?? [];
-
-  const rulesToType = {
-    [DISPLAY_NAME]: notifType.NOISY,
-    [ROOM_PING]: notifType.NOISY,
-    [USERNAME]: notifType.NOISY,
-  };
-
-  const setRule = (rule, type) => {
-    const evtContent = pushRules ?? {};
-    if (!evtContent.global) evtContent.global = {};
-    if (!evtContent.global.override) evtContent.global.override = [];
-    if (!evtContent.global.content) evtContent.global.content = [];
-    const or = evtContent.global.override;
-    const ct = evtContent.global.content;
-
-    if (rule === DISPLAY_NAME || rule === ROOM_PING) {
-      let orRule = or.find((r) => r?.rule_id === rule);
-      if (!orRule) {
-        orRule = {
-          conditions: [],
-          actions: [],
-          rule_id: rule,
-          default: true,
-          enabled: true,
-        };
-        or.push(orRule);
-      }
-      if (rule === DISPLAY_NAME) {
-        orRule.conditions = [{ kind: 'contains_display_name' }];
-        orRule.actions = getTypeActions(type, true);
-      } else {
-        orRule.conditions = [
-          { kind: 'event_match', key: 'content.body', pattern: '@room' },
-          { kind: 'sender_notification_permission', key: 'room' },
-        ];
-        orRule.actions = getTypeActions(type, true);
-      }
-    } else if (rule === USERNAME) {
-      let usernameRule = ct.find((r) => r?.rule_id === rule);
-      if (!usernameRule) {
-        const userId = mx.getUserId();
-        const username = userId.match(/^@?(\S+):(\S+)$/)?.[1] ?? userId;
-        usernameRule = {
-          actions: [],
-          default: true,
-          enabled: true,
-          pattern: username,
-          rule_id: rule,
-        };
-        ct.push(usernameRule);
-      }
-      usernameRule.actions = getTypeActions(type, true);
-    } else {
-      const keyRules = ct.filter((r) => r.rule_id !== USERNAME);
-      keyRules.forEach((r) => {
-        // eslint-disable-next-line no-param-reassign
-        r.actions = getTypeActions(type, true);
-      });
-    }
-
-    mx.setAccountData('m.push_rules', evtContent);
-  };
-
-  const addKeyword = (keyword) => {
-    if (content.find((r) => r.rule_id === keyword)) return;
-    content.push({
-      rule_id: keyword,
-      pattern: keyword,
-      enabled: true,
-      default: false,
-      actions: getTypeActions(rulesToType[KEYWORD] ?? notifType.NOISY, true),
-    });
-    mx.setAccountData('m.push_rules', pushRules);
-  };
-  const removeKeyword = (rule) => {
-    pushRules.global.content = content.filter((r) => r.rule_id !== rule.rule_id);
-    mx.setAccountData('m.push_rules', pushRules);
-  };
-
-  const dsRule = override.find((rule) => rule.rule_id === DISPLAY_NAME);
-  const roomRule = override.find((rule) => rule.rule_id === ROOM_PING);
-  const usernameRule = content.find((rule) => rule.rule_id === USERNAME);
-  const keywordRule = content.find((rule) => rule.rule_id !== USERNAME);
-
-  if (dsRule) rulesToType[DISPLAY_NAME] = getActionType(dsRule);
-  if (roomRule) rulesToType[ROOM_PING] = getActionType(roomRule);
-  if (usernameRule) rulesToType[USERNAME] = getActionType(usernameRule);
-  if (keywordRule) rulesToType[KEYWORD] = getActionType(keywordRule);
-
-  return {
-    rulesToType,
-    pushRules,
-    setRule,
-    addKeyword,
-    removeKeyword,
-  };
-}
-
-function GlobalNotification() {
-  const {
-    rulesToType,
-    pushRules,
-    setRule,
-    addKeyword,
-    removeKeyword,
-  } = useKeywordNotif();
-
-  const keywordRules = pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
-
-  const onSelect = (evt, rule) => {
-    openReusableContextMenu(
-      'bottom',
-      getEventCords(evt, '.btn-surface'),
-      (requestClose) => (
-        <NotificationSelector
-          value={rulesToType[rule]}
-          onSelect={(value) => {
-            if (rulesToType[rule] !== value) setRule(rule, value);
-            requestClose();
-          }}
-        />
-      ),
-    );
-  };
-
-  const handleSubmit = (evt) => {
-    evt.preventDefault();
-    const { keywordInput } = evt.target.elements;
-    const value = keywordInput.value.trim();
-    if (value === '') return;
-    addKeyword(value);
-    keywordInput.value = '';
-  };
-
-  return (
-    <div className="keyword-notification">
-      <MenuHeader>Mentions & keywords</MenuHeader>
-      <SettingTile
-        title="Message containing my display name"
-        options={(
-          <Button onClick={(evt) => onSelect(evt, DISPLAY_NAME)} iconSrc={ChevronBottomIC}>
-            { typeToLabel[rulesToType[DISPLAY_NAME]] }
-          </Button>
-        )}
-        content={<Text variant="b3">Default notification settings for all message containing your display name.</Text>}
-      />
-      <SettingTile
-        title="Message containing my username"
-        options={(
-          <Button onClick={(evt) => onSelect(evt, USERNAME)} iconSrc={ChevronBottomIC}>
-            { typeToLabel[rulesToType[USERNAME]] }
-          </Button>
-        )}
-        content={<Text variant="b3">Default notification settings for all message containing your username.</Text>}
-      />
-      <SettingTile
-        title="Message containing @room"
-        options={(
-          <Button onClick={(evt) => onSelect(evt, ROOM_PING)} iconSrc={ChevronBottomIC}>
-            {typeToLabel[rulesToType[ROOM_PING]]}
-          </Button>
-        )}
-        content={<Text variant="b3">Default notification settings for all messages containing @room.</Text>}
-      />
-      { rulesToType[KEYWORD] && (
-        <SettingTile
-          title="Message containing keywords"
-          options={(
-            <Button onClick={(evt) => onSelect(evt, KEYWORD)} iconSrc={ChevronBottomIC}>
-              {typeToLabel[rulesToType[KEYWORD]]}
-            </Button>
-          )}
-          content={<Text variant="b3">Default notification settings for all message containing keywords.</Text>}
-        />
-      )}
-      <SettingTile
-        title="Keywords"
-        content={(
-          <div className="keyword-notification__keyword">
-            <Text variant="b3">Get notification when a message contains keyword.</Text>
-            <form onSubmit={handleSubmit}>
-              <Input name="keywordInput" required />
-              <Button variant="primary" type="submit">Add</Button>
-            </form>
-            {keywordRules.length > 0 && (
-              <div>
-                {keywordRules.map((rule) => (
-                  <Chip
-                    iconSrc={CrossIC}
-                    key={rule.rule_id}
-                    text={rule.pattern}
-                    iconColor={CrossIC}
-                    onClick={() => removeKeyword(rule)}
-                  />
-                ))}
-              </div>
-            )}
-          </div>
-        )}
-      />
-    </div>
-  );
-}
-
-export default GlobalNotification;
diff --git a/src/app/molecules/global-notification/KeywordNotification.scss b/src/app/molecules/global-notification/KeywordNotification.scss
deleted file mode 100644 (file)
index 4d1bfd4..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-.keyword-notification {
-  &__keyword {
-    & form,
-    & > div:last-child {
-      display: flex;
-      flex-wrap: wrap;
-      gap: var(--sp-tight);
-    }
-
-    & form {
-      margin: var(--sp-extra-tight) 0 var(--sp-normal);
-      .input-container {
-        flex-grow: 1;
-      }
-    }
-  }
-}
\ No newline at end of file
diff --git a/src/app/molecules/global-notification/NotificationSelector.jsx b/src/app/molecules/global-notification/NotificationSelector.jsx
deleted file mode 100644 (file)
index b2a8f4e..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
-
-import CheckIC from '../../../../public/res/ic/outlined/check.svg';
-
-function NotificationSelector({
-  value, onSelect,
-}) {
-  return (
-    <div>
-      <MenuHeader>Notification</MenuHeader>
-      <MenuItem iconSrc={value === 'off' ? CheckIC : null} variant={value === 'off' ? 'positive' : 'surface'} onClick={() => onSelect('off')}>Off</MenuItem>
-      <MenuItem iconSrc={value === 'on' ? CheckIC : null} variant={value === 'on' ? 'positive' : 'surface'} onClick={() => onSelect('on')}>On</MenuItem>
-      <MenuItem iconSrc={value === 'noisy' ? CheckIC : null} variant={value === 'noisy' ? 'positive' : 'surface'} onClick={() => onSelect('noisy')}>Noisy</MenuItem>
-    </div>
-  );
-}
-
-NotificationSelector.propTypes = {
-  value: PropTypes.oneOf(['off', 'on', 'noisy']).isRequired,
-  onSelect: PropTypes.func.isRequired,
-};
-
-export default NotificationSelector;
index e4e6be2aa58c77dc5706dbe36a79810b2e9cd488..93d1d90f340e48958ae24dd3d96f599ee7400636 100644 (file)
@@ -1,5 +1,6 @@
 import React, { useState, useMemo, useReducer, useEffect } from 'react';
 import PropTypes from 'prop-types';
+import { EventTimeline } from 'matrix-js-sdk';
 import './ImagePack.scss';
 
 import { openReusableDialog } from '../../../client/action/navigation';
@@ -18,6 +19,7 @@ import ImagePackItem from './ImagePackItem';
 import ImagePackUpload from './ImagePackUpload';
 import { useMatrixClient } from '../../hooks/useMatrixClient';
 import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { getStateEvent } from '../../utils/room';
 
 const renameImagePackItem = (shortcode) =>
   new Promise((resolve) => {
@@ -76,7 +78,7 @@ function useRoomImagePack(roomId, stateKey) {
   const room = mx.getRoom(roomId);
 
   const pack = useMemo(() => {
-    const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
+    const packEvent = getStateEvent(room, 'im.ponies.room_emotes', stateKey);
     return ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent());
   }, [room, stateKey]);
 
@@ -245,7 +247,10 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
   };
 
   const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
-  const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
+  const canChange = room
+    .getLiveTimeline()
+    .getState(EventTimeline.FORWARDS)
+    ?.hasSufficientPowerLevelFor('state_default', myPowerlevel);
 
   const handleDeletePack = async () => {
     const isConfirmed = await confirmDialog(
@@ -473,7 +478,7 @@ function ImagePackGlobal() {
           [...roomIdToStateKeys].map(([roomId, stateKeys]) => {
             const room = mx.getRoom(roomId);
             return stateKeys.map((stateKey) => {
-              const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
+              const data = getStateEvent(room, 'im.ponies.room_emotes', stateKey);
               const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent());
               if (!pack) return null;
               return (
diff --git a/src/app/molecules/import-export-e2e-room-keys/ExportE2ERoomKeys.jsx b/src/app/molecules/import-export-e2e-room-keys/ExportE2ERoomKeys.jsx
deleted file mode 100644 (file)
index 1ce7e78..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-import './ExportE2ERoomKeys.scss';
-
-import FileSaver from 'file-saver';
-
-import cons from '../../../client/state/cons';
-import { encryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-import Input from '../../atoms/input/Input';
-import Spinner from '../../atoms/spinner/Spinner';
-
-import { useStore } from '../../hooks/useStore';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-function ExportE2ERoomKeys() {
-  const mx = useMatrixClient();
-  const isMountStore = useStore();
-  const [status, setStatus] = useState({
-    isOngoing: false,
-    msg: null,
-    type: cons.status.PRE_FLIGHT,
-  });
-  const passwordRef = useRef(null);
-  const confirmPasswordRef = useRef(null);
-
-  const exportE2ERoomKeys = async () => {
-    const password = passwordRef.current.value;
-    if (password !== confirmPasswordRef.current.value) {
-      setStatus({
-        isOngoing: false,
-        msg: 'Password does not match.',
-        type: cons.status.ERROR,
-      });
-      return;
-    }
-    setStatus({
-      isOngoing: true,
-      msg: 'Getting keys...',
-      type: cons.status.IN_FLIGHT,
-    });
-    try {
-      const keys = await mx.exportRoomKeys();
-      if (isMountStore.getItem()) {
-        setStatus({
-          isOngoing: true,
-          msg: 'Encrypting keys...',
-          type: cons.status.IN_FLIGHT,
-        });
-      }
-      const encKeys = await encryptMegolmKeyFile(JSON.stringify(keys), password);
-      const blob = new Blob([encKeys], {
-        type: 'text/plain;charset=us-ascii',
-      });
-      FileSaver.saveAs(blob, 'cinny-keys.txt');
-      if (isMountStore.getItem()) {
-        setStatus({
-          isOngoing: false,
-          msg: 'Successfully exported all keys.',
-          type: cons.status.SUCCESS,
-        });
-      }
-    } catch (e) {
-      if (isMountStore.getItem()) {
-        setStatus({
-          isOngoing: false,
-          msg: e.friendlyText || 'Failed to export keys. Please try again.',
-          type: cons.status.ERROR,
-        });
-      }
-    }
-  };
-
-  useEffect(() => {
-    isMountStore.setItem(true);
-    return () => {
-      isMountStore.setItem(false);
-    };
-  }, []);
-
-  return (
-    <div className="export-e2e-room-keys">
-      <form className="export-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); exportE2ERoomKeys(); }}>
-        <Input forwardRef={passwordRef} type="password" placeholder="Password" required />
-        <Input forwardRef={confirmPasswordRef} type="password" placeholder="Confirm password" required />
-        <Button disabled={status.isOngoing} variant="primary" type="submit">Export</Button>
-      </form>
-      { status.type === cons.status.IN_FLIGHT && (
-        <div className="import-e2e-room-keys__process">
-          <Spinner size="small" />
-          <Text variant="b2">{status.msg}</Text>
-        </div>
-      )}
-      {status.type === cons.status.SUCCESS && <Text className="import-e2e-room-keys__success" variant="b2">{status.msg}</Text>}
-      {status.type === cons.status.ERROR && <Text className="import-e2e-room-keys__error" variant="b2">{status.msg}</Text>}
-    </div>
-  );
-}
-
-export default ExportE2ERoomKeys;
diff --git a/src/app/molecules/import-export-e2e-room-keys/ExportE2ERoomKeys.scss b/src/app/molecules/import-export-e2e-room-keys/ExportE2ERoomKeys.scss
deleted file mode 100644 (file)
index a8bd029..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-.export-e2e-room-keys {
-  margin-top: var(--sp-extra-tight);
-  &__form {
-    display: flex;
-    & > .input-container {
-      flex: 1;
-      min-width: 0;
-    }
-    & > *:nth-child(2) {
-      margin: 0 var(--sp-tight);
-    }
-  }
-
-  &__process {
-    margin-top: var(--sp-tight);
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    & .text {
-      margin: 0 var(--sp-tight);
-    }
-  }
-  &__error {
-    margin-top: var(--sp-tight);
-    color: var(--tc-danger-high);
-  }
-
-}
\ No newline at end of file
diff --git a/src/app/molecules/import-export-e2e-room-keys/ImportE2ERoomKeys.jsx b/src/app/molecules/import-export-e2e-room-keys/ImportE2ERoomKeys.jsx
deleted file mode 100644 (file)
index 9f0ab79..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-import './ImportE2ERoomKeys.scss';
-
-import cons from '../../../client/state/cons';
-import { decryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
-
-import Text from '../../atoms/text/Text';
-import IconButton from '../../atoms/button/IconButton';
-import Button from '../../atoms/button/Button';
-import Input from '../../atoms/input/Input';
-import Spinner from '../../atoms/spinner/Spinner';
-
-import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
-
-import { useStore } from '../../hooks/useStore';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-function ImportE2ERoomKeys() {
-  const mx = useMatrixClient();
-  const isMountStore = useStore();
-  const [keyFile, setKeyFile] = useState(null);
-  const [status, setStatus] = useState({
-    isOngoing: false,
-    msg: null,
-    type: cons.status.PRE_FLIGHT,
-  });
-  const inputRef = useRef(null);
-  const passwordRef = useRef(null);
-
-  async function tryDecrypt(file, password) {
-    try {
-      const arrayBuffer = await file.arrayBuffer();
-      if (isMountStore.getItem()) {
-        setStatus({
-          isOngoing: true,
-          msg: 'Decrypting file...',
-          type: cons.status.IN_FLIGHT,
-        });
-      }
-
-      const keys = await decryptMegolmKeyFile(arrayBuffer, password);
-      if (isMountStore.getItem()) {
-        setStatus({
-          isOngoing: true,
-          msg: 'Decrypting messages...',
-          type: cons.status.IN_FLIGHT,
-        });
-      }
-      await mx.importRoomKeys(JSON.parse(keys));
-      if (isMountStore.getItem()) {
-        setStatus({
-          isOngoing: false,
-          msg: 'Successfully imported all keys.',
-          type: cons.status.SUCCESS,
-        });
-        inputRef.current.value = null;
-        passwordRef.current.value = null;
-      }
-    } catch (e) {
-      if (isMountStore.getItem()) {
-        setStatus({
-          isOngoing: false,
-          msg: e.friendlyText || 'Failed to decrypt keys. Please try again.',
-          type: cons.status.ERROR,
-        });
-      }
-    }
-  }
-
-  const importE2ERoomKeys = () => {
-    const password = passwordRef.current.value;
-    if (password === '' || keyFile === null) return;
-    if (status.isOngoing) return;
-
-    tryDecrypt(keyFile, password);
-  };
-
-  const handleFileChange = (e) => {
-    const file = e.target.files.item(0);
-    passwordRef.current.value = '';
-    setKeyFile(file);
-    setStatus({
-      isOngoing: false,
-      msg: null,
-      type: cons.status.PRE_FLIGHT,
-    });
-  };
-  const removeImportKeysFile = () => {
-    if (status.isOngoing) return;
-    inputRef.current.value = null;
-    passwordRef.current.value = null;
-    setKeyFile(null);
-    setStatus({
-      isOngoing: false,
-      msg: null,
-      type: cons.status.PRE_FLIGHT,
-    });
-  };
-
-  useEffect(() => {
-    isMountStore.setItem(true);
-    return () => {
-      isMountStore.setItem(false);
-    };
-  }, []);
-
-  return (
-    <div className="import-e2e-room-keys">
-      <input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" />
-
-      <form className="import-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); importE2ERoomKeys(); }}>
-        { keyFile !== null && (
-          <div className="import-e2e-room-keys__file">
-            <IconButton onClick={removeImportKeysFile} src={CirclePlusIC} tooltip="Remove file" />
-            <Text>{keyFile.name}</Text>
-          </div>
-        )}
-        {keyFile === null && <Button onClick={() => inputRef.current.click()}>Import keys</Button>}
-        <Input forwardRef={passwordRef} type="password" placeholder="Password" required />
-        <Button disabled={status.isOngoing} variant="primary" type="submit">Decrypt</Button>
-      </form>
-      { status.type === cons.status.IN_FLIGHT && (
-        <div className="import-e2e-room-keys__process">
-          <Spinner size="small" />
-          <Text variant="b2">{status.msg}</Text>
-        </div>
-      )}
-      {status.type === cons.status.SUCCESS && <Text className="import-e2e-room-keys__success" variant="b2">{status.msg}</Text>}
-      {status.type === cons.status.ERROR && <Text className="import-e2e-room-keys__error" variant="b2">{status.msg}</Text>}
-    </div>
-  );
-}
-
-export default ImportE2ERoomKeys;
diff --git a/src/app/molecules/import-export-e2e-room-keys/ImportE2ERoomKeys.scss b/src/app/molecules/import-export-e2e-room-keys/ImportE2ERoomKeys.scss
deleted file mode 100644 (file)
index 4f27fdf..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-@use '../../partials/text';
-@use '../../partials/dir';
-
-.import-e2e-room-keys {
-  &__file {
-    display: inline-flex;
-    align-items: center;
-    background: var(--bg-surface-low);
-    border-radius: var(--bo-radius);
-    box-shadow: var(--bs-surface-border);
-
-    & button {
-      --parent-height: 46px;
-      width: var(--parent-height);
-      height: 100%;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-    }
-
-    & .ic-raw {
-      background-color: var(--bg-caution);
-      transform: rotate(45deg);
-    }
-    
-    & .text {
-      @extend .cp-txt__ellipsis;
-      @include dir.side(margin, var(--sp-tight), var(--sp-loose));
-      max-width: 86px;
-    }
-  }
-
-  &__form {
-    display: flex;
-    margin-top: var(--sp-extra-tight);
-    
-    
-    & .input-container {
-      flex: 1;
-      margin: 0 var(--sp-tight);
-    }
-  }
-
-  &__process {
-    margin-top: var(--sp-tight);
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    & .text {
-      margin: 0 var(--sp-tight);
-    }
-  }
-  &__error {
-    margin-top: var(--sp-tight);
-    color: var(--tc-danger-high);
-  }
-  &__success {
-    margin-top: var(--sp-tight);
-    color: var(--tc-positive-high);
-  }
-}
\ No newline at end of file
index 876d063fab4b9817d6f6d883dc661df6b23250a2..a96fbb8bc3eea755bd9532c41729475208718882 100644 (file)
@@ -1,6 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import './RoomAliases.scss';
+import { EventTimeline } from 'matrix-js-sdk';
 
 import cons from '../../../client/state/cons';
 import { Debounce } from '../../../util/common';
@@ -108,7 +109,7 @@ function RoomAliases({ roomId }) {
   const [deleteAlias, setDeleteAlias] = useState(null);
   const [validate, setValidateToDefault, handleAliasChange] = useValidate(hsString);
 
-  const canPublishAlias = room.currentState.maySendStateEvent('m.room.canonical_alias', userId);
+  const canPublishAlias = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.maySendStateEvent('m.room.canonical_alias', userId);
 
   useEffect(() => {
     isMountedStore.setItem(true)
index da09ea8646e9b487cebf3010e7d032b488e2b9c0..c45a9afab8d55b0a26e2e9b02cce95c4fdae381e 100644 (file)
@@ -1,6 +1,7 @@
 import React, { useReducer, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import './RoomEmojis.scss';
+import { EventTimeline } from 'matrix-js-sdk';
 
 import { suffixRename } from '../../../util/common';
 
@@ -10,12 +11,13 @@ import Input from '../../atoms/input/Input';
 import Button from '../../atoms/button/Button';
 import ImagePack from '../image-pack/ImagePack';
 import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { getStateEvent, getStateEvents } from '../../utils/room';
 
 function useRoomPacks(room) {
   const mx = useMatrixClient();
   const [, forceUpdate] = useReducer((count) => count + 1, 0);
 
-  const packEvents = room.currentState.getStateEvents('im.ponies.room_emotes');
+  const packEvents = getStateEvents(room, 'im.ponies.room_emotes');
   const unUsablePacks = [];
   const usablePacks = packEvents.filter((mEvent) => {
     if (typeof mEvent.getContent()?.images !== 'object') {
@@ -40,7 +42,7 @@ function useRoomPacks(room) {
     };
   }, [room, mx]);
 
-  const isStateKeyAvailable = (key) => !room.currentState.getStateEvents('im.ponies.room_emotes', key);
+  const isStateKeyAvailable = (key) => !getStateEvent(room, 'im.ponies.room_emotes', key);
 
   const createPack = async (name) => {
     const packContent = {
@@ -80,7 +82,7 @@ function RoomEmojis({ roomId }) {
 
   const { usablePacks, createPack, deletePack } = useRoomPacks(room);
 
-  const canChange = room.currentState.maySendStateEvent('im.ponies.room_emote', mx.getUserId());
+  const canChange = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.maySendStateEvent('im.ponies.room_emote', mx.getUserId());
 
   const handlePackCreate = (e) => {
     e.preventDefault();
index 47250f47233e108bce2ca0531050ab89790da76c..6db87a8e98b4488358046ee2da24e9b3b59dc35a 100644 (file)
@@ -1,7 +1,7 @@
 import React, { useState } from 'react';
 import PropTypes from 'prop-types';
 import './RoomEncryption.scss';
-
+import { EventTimeline } from 'matrix-js-sdk';
 
 import Text from '../../atoms/text/Text';
 import Toggle from '../../atoms/button/Toggle';
@@ -9,13 +9,14 @@ import SettingTile from '../setting-tile/SettingTile';
 
 import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
 import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { getStateEvents } from '../../utils/room';
 
 function RoomEncryption({ roomId }) {
   const mx = useMatrixClient();
   const room = mx.getRoom(roomId);
-  const encryptionEvents = room.currentState.getStateEvents('m.room.encryption');
+  const encryptionEvents = getStateEvents(room, 'm.room.encryption');
   const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
-  const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId());
+  const canEnableEncryption = room.getLiveTimeline().getState(EventTimeline.FORWARDS).maySendStateEvent('m.room.encryption', mx.getUserId());
 
   const handleEncryptionEnable = async () => {
     const joinRule = room.getJoinRule();
index f8cd048f54267107f03be911fb94fbe95c572d2a..7629558fee330499bba4dfb2ff732923c737a654 100644 (file)
@@ -1,6 +1,7 @@
 import React, { useEffect } from 'react';
 import PropTypes from 'prop-types';
 import './RoomPermissions.scss';
+import { EventTimeline } from 'matrix-js-sdk';
 
 import { getPowerLabel } from '../../../util/matrixUtil';
 import { openReusableContextMenu } from '../../../client/action/navigation';
@@ -16,6 +17,7 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
 
 import { useForceUpdate } from '../../hooks/useForceUpdate';
 import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { getStateEvent } from '../../utils/room';
 
 const permissionsInfo = {
   users_default: {
@@ -176,9 +178,9 @@ function RoomPermissions({ roomId }) {
   useRoomStateUpdate(roomId);
   const mx = useMatrixClient();
   const room = mx.getRoom(roomId);
-  const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
+  const pLEvent = getStateEvent(room, 'm.room.power_levels');
   const permissions = pLEvent.getContent();
-  const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
+  const canChangePermission = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.maySendStateEvent('m.room.power_levels', mx.getUserId());
   const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
 
   const handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
index 6579513a86aae4166b329ee9cf2379dcfc7fc686..f9bb815dd521a0fb3fa51ccb4e5f971bb477f153 100644 (file)
@@ -1,7 +1,7 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import PropTypes from 'prop-types';
 import './RoomVisibility.scss';
-
+import { EventTimeline } from 'matrix-js-sdk';
 
 import Text from '../../atoms/text/Text';
 import RadioButton from '../../atoms/button/RadioButton';
@@ -74,7 +74,7 @@ function RoomVisibility({ roomId }) {
   const roomVersion = Number(mCreate?.room_version ?? 0);
 
   const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
-  const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
+  const canChange = room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.hasSufficientPowerLevelFor('state_default', myPowerlevel);
 
   const items = [{
     iconSrc: isSpace ? SpaceLockIC : HashLockIC,
diff --git a/src/app/organisms/emoji-verification/EmojiVerification.jsx b/src/app/organisms/emoji-verification/EmojiVerification.jsx
deleted file mode 100644 (file)
index 21be477..0000000
+++ /dev/null
@@ -1,204 +0,0 @@
-/* eslint-disable react/prop-types */
-import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
-import './EmojiVerification.scss';
-
-import cons from '../../../client/state/cons';
-import navigation from '../../../client/state/navigation';
-import { hasPrivateKey } from '../../../client/state/secretStorageKeys';
-import { getDefaultSSKey, isCrossVerified } from '../../../util/matrixUtil';
-
-import Text from '../../atoms/text/Text';
-import IconButton from '../../atoms/button/IconButton';
-import Button from '../../atoms/button/Button';
-import Spinner from '../../atoms/spinner/Spinner';
-import Dialog from '../../molecules/dialog/Dialog';
-
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-import { useStore } from '../../hooks/useStore';
-import { accessSecretStorage } from '../settings/SecretStorageAccess';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-function EmojiVerificationContent({ data, requestClose }) {
-  const [sas, setSas] = useState(null);
-  const [process, setProcess] = useState(false);
-  const { request, targetDevice } = data;
-  const mx = useMatrixClient();
-  const mountStore = useStore();
-  const beginStore = useStore();
-
-  const beginVerification = async () => {
-    if (
-      isCrossVerified(mx, mx.deviceId) &&
-      (mx.getCrossSigningId() === null ||
-        (await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing')) === false)
-    ) {
-      if (!hasPrivateKey(getDefaultSSKey(mx))) {
-        const keyData = await accessSecretStorage(mx, 'Emoji verification');
-        if (!keyData) {
-          request.cancel();
-          return;
-        }
-      }
-      await mx.checkOwnCrossSigningTrust();
-    }
-    setProcess(true);
-    await request.accept();
-
-    const verifier = request.beginKeyVerification('m.sas.v1', targetDevice);
-
-    const handleVerifier = (sasData) => {
-      verifier.off('show_sas', handleVerifier);
-      if (!mountStore.getItem()) return;
-      setSas(sasData);
-      setProcess(false);
-    };
-    verifier.on('show_sas', handleVerifier);
-    await verifier.verify();
-  };
-
-  const sasMismatch = () => {
-    sas.mismatch();
-    setProcess(true);
-  };
-
-  const sasConfirm = () => {
-    sas.confirm();
-    setProcess(true);
-  };
-
-  useEffect(() => {
-    mountStore.setItem(true);
-    const handleChange = () => {
-      if (request.done || request.cancelled) {
-        requestClose();
-        return;
-      }
-      if (targetDevice && !beginStore.getItem()) {
-        beginStore.setItem(true);
-        beginVerification();
-      }
-    };
-
-    if (request === null) return undefined;
-    const req = request;
-    req.on('change', handleChange);
-    return () => {
-      req.off('change', handleChange);
-      if (req.cancelled === false && req.done === false) {
-        req.cancel();
-      }
-    };
-  }, [request]);
-
-  const renderWait = () => (
-    <>
-      <Spinner size="small" />
-      <Text>Waiting for response from other device...</Text>
-    </>
-  );
-
-  if (sas !== null) {
-    return (
-      <div className="emoji-verification__content">
-        <Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
-        <div className="emoji-verification__emojis">
-          {sas.sas.emoji.map((emoji, i) => (
-            // eslint-disable-next-line react/no-array-index-key
-            <div className="emoji-verification__emoji-block" key={`${emoji[1]}-${i}`}>
-              <Text variant="h1">{emoji[0]}</Text>
-              <Text>{emoji[1]}</Text>
-            </div>
-          ))}
-        </div>
-        <div className="emoji-verification__buttons">
-          {process ? (
-            renderWait()
-          ) : (
-            <>
-              <Button variant="primary" onClick={sasConfirm}>
-                They match
-              </Button>
-              <Button onClick={sasMismatch}>No match</Button>
-            </>
-          )}
-        </div>
-      </div>
-    );
-  }
-
-  if (targetDevice) {
-    return (
-      <div className="emoji-verification__content">
-        <Text>Please accept the request from other device.</Text>
-        <div className="emoji-verification__buttons">{renderWait()}</div>
-      </div>
-    );
-  }
-
-  return (
-    <div className="emoji-verification__content">
-      <Text>Click accept to start the verification process.</Text>
-      <div className="emoji-verification__buttons">
-        {process ? (
-          renderWait()
-        ) : (
-          <Button variant="primary" onClick={beginVerification}>
-            Accept
-          </Button>
-        )}
-      </div>
-    </div>
-  );
-}
-EmojiVerificationContent.propTypes = {
-  data: PropTypes.shape({}).isRequired,
-  requestClose: PropTypes.func.isRequired,
-};
-
-function useVisibilityToggle() {
-  const [data, setData] = useState(null);
-  const mx = useMatrixClient();
-
-  useEffect(() => {
-    const handleOpen = (request, targetDevice) => {
-      setData({ request, targetDevice });
-    };
-    navigation.on(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
-    mx.on('crypto.verification.request', handleOpen);
-    return () => {
-      navigation.removeListener(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
-      mx.removeListener('crypto.verification.request', handleOpen);
-    };
-  }, [mx]);
-
-  const requestClose = () => setData(null);
-
-  return [data, requestClose];
-}
-
-function EmojiVerification() {
-  const [data, requestClose] = useVisibilityToggle();
-
-  return (
-    <Dialog
-      isOpen={data !== null}
-      className="emoji-verification"
-      title={
-        <Text variant="s1" weight="medium" primary>
-          Emoji verification
-        </Text>
-      }
-      contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
-      onRequestClose={requestClose}
-    >
-      {data !== null ? (
-        <EmojiVerificationContent data={data} requestClose={requestClose} />
-      ) : (
-        <div />
-      )}
-    </Dialog>
-  );
-}
-
-export default EmojiVerification;
diff --git a/src/app/organisms/emoji-verification/EmojiVerification.scss b/src/app/organisms/emoji-verification/EmojiVerification.scss
deleted file mode 100644 (file)
index 4e6dc11..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-@use '../../partials/flex';
-@use '../../partials/dir';
-
-.emoji-verification {
-  &__content {
-    padding: var(--sp-normal);
-    @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
-    display: flex;
-    flex-direction: column;
-    gap: var(--sp-normal);
-  }
-
-  &__emojis {
-    margin: var(--sp-loose) 0;
-    display: flex;
-    align-items: center;
-    justify-content: space-around;
-    gap: var(--sp-extra-tight);
-    flex-wrap: wrap;
-  }
-
-  &__emoji-block {
-    @extend .cp-fx__column;
-    flex: 1;
-    align-items: center;
-    gap: var(--sp-extra-tight);
-    white-space: nowrap;
-    text-transform: capitalize;
-  }
-
-  &__buttons {
-    display: flex;
-    gap: var(--sp-normal);
-  }
-}
diff --git a/src/app/organisms/profile-editor/ProfileEditor.jsx b/src/app/organisms/profile-editor/ProfileEditor.jsx
deleted file mode 100644 (file)
index 9612002..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-import PropTypes from 'prop-types';
-
-import colorMXID from '../../../util/colorMXID';
-
-import Text from '../../atoms/text/Text';
-import IconButton from '../../atoms/button/IconButton';
-import Button from '../../atoms/button/Button';
-import ImageUpload from '../../molecules/image-upload/ImageUpload';
-import Input from '../../atoms/input/Input';
-
-import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
-import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
-
-import './ProfileEditor.scss';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
-
-function ProfileEditor({ userId }) {
-  const [isEditing, setIsEditing] = useState(false);
-  const mx = useMatrixClient();
-  const user = mx.getUser(mx.getUserId());
-  const useAuthentication = useMediaAuthentication();
-
-  const displayNameRef = useRef(null);
-  const [avatarSrc, setAvatarSrc] = useState(
-    user.avatarUrl
-      ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop', undefined, undefined, useAuthentication)
-      : null
-  );
-  const [username, setUsername] = useState(user.displayName);
-  const [disabled, setDisabled] = useState(true);
-
-  useEffect(() => {
-    let isMounted = true;
-    mx.getProfileInfo(mx.getUserId()).then((info) => {
-      if (!isMounted) return;
-      setAvatarSrc(
-        info.avatar_url
-          ? mx.mxcUrlToHttp(
-              info.avatar_url,
-              80,
-              80,
-              'crop',
-              undefined,
-              undefined,
-              useAuthentication
-            )
-          : null
-      );
-      setUsername(info.displayname);
-    });
-    return () => {
-      isMounted = false;
-    };
-  }, [mx, userId, useAuthentication]);
-
-  const handleAvatarUpload = async (url) => {
-    if (url === null) {
-      const isConfirmed = await confirmDialog(
-        'Remove avatar',
-        'Are you sure that you want to remove avatar?',
-        'Remove',
-        'caution'
-      );
-      if (isConfirmed) {
-        mx.setAvatarUrl('');
-        setAvatarSrc(null);
-      }
-      return;
-    }
-    mx.setAvatarUrl(url);
-    setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop', undefined, undefined, useAuthentication));
-  };
-
-  const saveDisplayName = () => {
-    const newDisplayName = displayNameRef.current.value;
-    if (newDisplayName !== null && newDisplayName !== username) {
-      mx.setDisplayName(newDisplayName);
-      setUsername(newDisplayName);
-      setDisabled(true);
-      setIsEditing(false);
-    }
-  };
-
-  const onDisplayNameInputChange = () => {
-    setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
-  };
-  const cancelDisplayNameChanges = () => {
-    displayNameRef.current.value = username;
-    onDisplayNameInputChange();
-    setIsEditing(false);
-  };
-
-  const renderForm = () => (
-    <form
-      className="profile-editor__form"
-      style={{ marginBottom: avatarSrc ? '24px' : '0' }}
-      onSubmit={(e) => {
-        e.preventDefault();
-        saveDisplayName();
-      }}
-    >
-      <Input
-        label={`Display name of ${mx.getUserId()}`}
-        onChange={onDisplayNameInputChange}
-        value={mx.getUser(mx.getUserId()).displayName}
-        forwardRef={displayNameRef}
-      />
-      <Button variant="primary" type="submit" disabled={disabled}>
-        Save
-      </Button>
-      <Button onClick={cancelDisplayNameChanges}>Cancel</Button>
-    </form>
-  );
-
-  const renderInfo = () => (
-    <div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
-      <div>
-        <Text variant="h2" primary weight="medium">
-          {username ?? userId}
-        </Text>
-        <IconButton
-          src={PencilIC}
-          size="extra-small"
-          tooltip="Edit"
-          onClick={() => setIsEditing(true)}
-        />
-      </div>
-      <Text variant="b2">{mx.getUserId()}</Text>
-    </div>
-  );
-
-  return (
-    <div className="profile-editor">
-      <ImageUpload
-        text={username ?? userId}
-        bgColor={colorMXID(userId)}
-        imageSrc={avatarSrc}
-        onUpload={handleAvatarUpload}
-        onRequestRemove={() => handleAvatarUpload(null)}
-      />
-      {isEditing ? renderForm() : renderInfo()}
-    </div>
-  );
-}
-
-ProfileEditor.defaultProps = {
-  userId: null,
-};
-
-ProfileEditor.propTypes = {
-  userId: PropTypes.string,
-};
-
-export default ProfileEditor;
diff --git a/src/app/organisms/profile-editor/ProfileEditor.scss b/src/app/organisms/profile-editor/ProfileEditor.scss
deleted file mode 100644 (file)
index 2e2ef91..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-@use '../../partials/dir';
-@use '../../partials/flex';
-
-.profile-editor {
-  display: flex;
-  align-items: flex-end;
-}
-
-.profile-editor__info,
-.profile-editor__form {
-  @extend .cp-fx__item-one;
-  @include dir.side(margin, var(--sp-loose), 0);
-  display: flex;
-}
-
-.profile-editor__info {
-  flex-direction: column;
-  & > div:first-child {
-    display: flex;
-    align-items: center;
-  }
-  .ic-btn {
-    margin: 0 var(--sp-extra-tight);
-  }
-}
-
-.profile-editor__form {
-  margin-top: 10px;
-  flex-wrap: wrap;
-  align-items: flex-end;
-
-  & > .input-container  {
-    @extend .cp-fx__item-one;
-  }
-  & > button {
-    height: 46px;
-    margin-top: var(--sp-normal);
-    @include dir.side(margin, var(--sp-normal), 0);
-  }
-
-}
\ No newline at end of file
index 6ff5fe7c3940de59b14587a3d114c98616fac6b2..1ed896920a63427d54626f050f9039bbb2573d70 100644 (file)
@@ -1,6 +1,7 @@
 import React, { useState, useEffect, useRef } from 'react';
 import PropTypes from 'prop-types';
 import './ProfileViewer.scss';
+import { EventTimeline } from 'matrix-js-sdk';
 
 import cons from '../../../client/state/cons';
 import navigation from '../../../client/state/navigation';
@@ -45,13 +46,14 @@ function ModerationTools({ roomId, userId }) {
 
   const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
   const powerLevel = roomMember?.powerLevel || 0;
+  const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
   const canIKick =
     roomMember?.membership === 'join' &&
-    room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel) &&
+    roomState?.hasSufficientPowerLevelFor('kick', myPowerLevel) &&
     powerLevel < myPowerLevel;
   const canIBan =
     ['join', 'leave'].includes(roomMember?.membership) &&
-    room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel) &&
+    roomState?.hasSufficientPowerLevelFor('ban', myPowerLevel) &&
     powerLevel < myPowerLevel;
 
   const handleKick = (e) => {
@@ -98,8 +100,9 @@ function SessionInfo({ userId }) {
 
     async function loadDevices() {
       try {
-        await mx.downloadKeys([userId], true);
-        const myDevices = mx.getStoredDevicesForUser(userId);
+        const crypto = mx.getCrypto();
+        const userToDevices = await crypto.getUserDeviceInfo([userId], true);
+        const myDevices = Array.from(userToDevices.get(userId).values());
 
         if (isUnmounted) return;
         setDevices(myDevices);
@@ -125,7 +128,7 @@ function SessionInfo({ userId }) {
             <Chip
               key={device.deviceId}
               iconSrc={ShieldEmptyIC}
-              text={device.getDisplayName() || device.deviceId}
+              text={device.displayName || device.deviceId}
             />
           ))}
       </div>
@@ -170,8 +173,10 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
 
   const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
   const userPL = room.getMember(userId)?.powerLevel || 0;
+  const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
+
   const canIKick =
-    room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
+    roomState?.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
 
   const isBanned = member?.membership === 'ban';
 
@@ -347,8 +352,9 @@ function ProfileViewer() {
     const powerLevel = roomMember?.powerLevel || 0;
     const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
 
+    const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
     const canChangeRole =
-      room.currentState.maySendEvent('m.room.power_levels', mx.getUserId()) &&
+      roomState?.maySendEvent('m.room.power_levels', mx.getUserId()) &&
       (powerLevel < myPowerLevel || userId === mx.getUserId());
 
     const handleChangePowerLevel = async (newPowerLevel) => {
index cc77cf18e5f43dd4a8da49610845872813694629..7fb18daa338d80f1fcb7678b4e822bc252280d15 100644 (file)
@@ -5,7 +5,6 @@ import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExistin
 import Search from '../search/Search';
 import CreateRoom from '../create-room/CreateRoom';
 import JoinAlias from '../join-alias/JoinAlias';
-import EmojiVerification from '../emoji-verification/EmojiVerification';
 
 import ReusableDialog from '../../molecules/dialog/ReusableDialog';
 
@@ -17,7 +16,6 @@ function Dialogs() {
       <JoinAlias />
       <SpaceAddExisting />
       <Search />
-      <EmojiVerification />
 
       <ReusableDialog />
     </>
index 3ee997695d566c6ecb67a7855cf89a149a509dbd..b3fd0b3b3b552046dda22ae0c2ea5302b36be1a4 100644 (file)
@@ -4,7 +4,6 @@ import cons from '../../../client/state/cons';
 import navigation from '../../../client/state/navigation';
 
 import InviteUser from '../invite-user/InviteUser';
-import Settings from '../settings/Settings';
 import SpaceSettings from '../space-settings/SpaceSettings';
 import RoomSettings from '../room/RoomSettings';
 
@@ -38,7 +37,6 @@ function Windows() {
         searchTerm={inviteUser.searchTerm}
         onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
       />
-      <Settings />
       <SpaceSettings />
       <RoomSettings />
     </>
diff --git a/src/app/organisms/settings/AuthRequest.jsx b/src/app/organisms/settings/AuthRequest.jsx
deleted file mode 100644 (file)
index f897f83..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-import './AuthRequest.scss';
-
-import { openReusableDialog } from '../../../client/action/navigation';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-import Input from '../../atoms/input/Input';
-import Spinner from '../../atoms/spinner/Spinner';
-
-import { useStore } from '../../hooks/useStore';
-import { getSecret } from '../../../client/state/auth';
-
-let lastUsedPassword;
-const getAuthId = (password) => ({
-  type: 'm.login.password',
-  password,
-  identifier: {
-    type: 'm.id.user',
-    user: getSecret().userId,
-  },
-});
-
-function AuthRequest({ onComplete, makeRequest }) {
-  const [status, setStatus] = useState(false);
-  const mountStore = useStore();
-
-  const handleForm = async (e) => {
-    mountStore.setItem(true);
-    e.preventDefault();
-    const password = e.target.password.value;
-    if (password.trim() === '') return;
-    try {
-      setStatus({ ongoing: true });
-      await makeRequest(getAuthId(password));
-      lastUsedPassword = password;
-      if (!mountStore.getItem()) return;
-      onComplete(true);
-    } catch (err) {
-      lastUsedPassword = undefined;
-      if (!mountStore.getItem()) return;
-      if (err.errcode === 'M_FORBIDDEN') {
-        setStatus({ error: 'Wrong password. Please enter correct password.' });
-        return;
-      }
-      setStatus({ error: 'Request failed!' });
-    }
-  };
-
-  const handleChange = () => {
-    setStatus(false);
-  };
-
-  return (
-    <div className="auth-request">
-      <form onSubmit={handleForm}>
-        <Input
-          name="password"
-          label="Account password"
-          type="password"
-          onChange={handleChange}
-          required
-        />
-        {status.ongoing && <Spinner size="small" />}
-        {status.error && <Text variant="b3">{status.error}</Text>}
-        {(status === false || status.error) && <Button variant="primary" type="submit" disabled={!!status.error}>Continue</Button>}
-      </form>
-    </div>
-  );
-}
-AuthRequest.propTypes = {
-  onComplete: PropTypes.func.isRequired,
-  makeRequest: PropTypes.func.isRequired,
-};
-
-/**
- * @param {string} title Title of dialog
- * @param {(auth) => void} makeRequest request to make
- * @returns {Promise<boolean>} whether the request succeed or not.
- */
-export const authRequest = async (title, makeRequest) => {
-  try {
-    const auth = lastUsedPassword ? getAuthId(lastUsedPassword) : undefined;
-    await makeRequest(auth);
-    return true;
-  } catch (e) {
-    lastUsedPassword = undefined;
-    if (e.httpStatus !== 401 || e.data?.flows === undefined) return false;
-
-    const { flows } = e.data;
-    const canUsePassword = flows.find((f) => f.stages.includes('m.login.password'));
-    if (!canUsePassword) return false;
-
-    return new Promise((resolve) => {
-      let isCompleted = false;
-      openReusableDialog(
-        <Text variant="s1" weight="medium">{title}</Text>,
-        (requestClose) => (
-          <AuthRequest
-            onComplete={(done) => {
-              isCompleted = true;
-              resolve(done);
-              requestClose();
-            }}
-            makeRequest={makeRequest}
-          />
-        ),
-        () => {
-          if (!isCompleted) resolve(false);
-        },
-      );
-    });
-  }
-};
-
-export default AuthRequest;
diff --git a/src/app/organisms/settings/AuthRequest.scss b/src/app/organisms/settings/AuthRequest.scss
deleted file mode 100644 (file)
index 35e95bf..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-.auth-request {
-  padding: var(--sp-normal);
-
-  & form > *:not(:first-child) {
-    margin-top: var(--sp-normal);
-  }
-
-  & .text-b3 {
-    color: var(--tc-danger-high);
-    margin-top: var(--sp-ultra-tight) !important;
-  }
-}
\ No newline at end of file
diff --git a/src/app/organisms/settings/CrossSigning.jsx b/src/app/organisms/settings/CrossSigning.jsx
deleted file mode 100644 (file)
index b993e32..0000000
+++ /dev/null
@@ -1,256 +0,0 @@
-/* eslint-disable react/jsx-one-expression-per-line */
-import React, { useState } from 'react';
-import './CrossSigning.scss';
-import FileSaver from 'file-saver';
-import { Formik } from 'formik';
-
-import { openReusableDialog } from '../../../client/action/navigation';
-import { copyToClipboard } from '../../../util/common';
-import { clearSecretStorageKeys } from '../../../client/state/secretStorageKeys';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-import Input from '../../atoms/input/Input';
-import Spinner from '../../atoms/spinner/Spinner';
-import SettingTile from '../../molecules/setting-tile/SettingTile';
-
-import { authRequest } from './AuthRequest';
-import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-const failedDialog = () => {
-  const renderFailure = (requestClose) => (
-    <div className="cross-signing__failure">
-      <Text variant="h1">❌</Text>
-      <Text weight="medium">Failed to setup cross signing. Please try again.</Text>
-      <Button onClick={requestClose}>Close</Button>
-    </div>
-  );
-
-  openReusableDialog(
-    <Text variant="s1" weight="medium">
-      Setup cross signing
-    </Text>,
-    renderFailure
-  );
-};
-
-const securityKeyDialog = (key) => {
-  const downloadKey = () => {
-    const blob = new Blob([key.encodedPrivateKey], {
-      type: 'text/plain;charset=us-ascii',
-    });
-    FileSaver.saveAs(blob, 'security-key.txt');
-  };
-  const copyKey = () => {
-    copyToClipboard(key.encodedPrivateKey);
-  };
-
-  const renderSecurityKey = () => (
-    <div className="cross-signing__key">
-      <Text weight="medium">Please save this security key somewhere safe.</Text>
-      <Text className="cross-signing__key-text">{key.encodedPrivateKey}</Text>
-      <div className="cross-signing__key-btn">
-        <Button variant="primary" onClick={() => copyKey(key)}>
-          Copy
-        </Button>
-        <Button onClick={() => downloadKey(key)}>Download</Button>
-      </div>
-    </div>
-  );
-
-  // Download automatically.
-  downloadKey();
-
-  openReusableDialog(
-    <Text variant="s1" weight="medium">
-      Security Key
-    </Text>,
-    () => renderSecurityKey()
-  );
-};
-
-function CrossSigningSetup() {
-  const initialValues = { phrase: '', confirmPhrase: '' };
-  const [genWithPhrase, setGenWithPhrase] = useState(undefined);
-  const mx = useMatrixClient();
-
-  const setup = async (securityPhrase = undefined) => {
-    setGenWithPhrase(typeof securityPhrase === 'string');
-    const recoveryKey = await mx.createRecoveryKeyFromPassphrase(securityPhrase);
-    clearSecretStorageKeys();
-
-    await mx.bootstrapSecretStorage({
-      createSecretStorageKey: async () => recoveryKey,
-      setupNewKeyBackup: true,
-      setupNewSecretStorage: true,
-    });
-
-    const authUploadDeviceSigningKeys = async (makeRequest) => {
-      const isDone = await authRequest('Setup cross signing', async (auth) => {
-        await makeRequest(auth);
-      });
-      setTimeout(() => {
-        if (isDone) securityKeyDialog(recoveryKey);
-        else failedDialog();
-      });
-    };
-
-    await mx.bootstrapCrossSigning({
-      authUploadDeviceSigningKeys,
-      setupNewCrossSigning: true,
-    });
-  };
-
-  const validator = (values) => {
-    const errors = {};
-    if (values.phrase === '12345678') {
-      errors.phrase = 'How about 87654321 ?';
-    }
-    if (values.phrase === '87654321') {
-      errors.phrase = 'Your are playing with 🔥';
-    }
-    const PHRASE_REGEX = /^([^\s]){8,127}$/;
-    if (values.phrase.length > 0 && !PHRASE_REGEX.test(values.phrase)) {
-      errors.phrase = 'Phrase must contain 8-127 characters with no space.';
-    }
-    if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) {
-      errors.confirmPhrase = "Phrase don't match.";
-    }
-    return errors;
-  };
-
-  return (
-    <div className="cross-signing__setup">
-      <div className="cross-signing__setup-entry">
-        <Text>
-          We will generate a <b>Security Key</b>, which you can use to manage messages backup and
-          session verification.
-        </Text>
-        {genWithPhrase !== false && (
-          <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>
-            Generate Key
-          </Button>
-        )}
-        {genWithPhrase === false && <Spinner size="small" />}
-      </div>
-      <Text className="cross-signing__setup-divider">OR</Text>
-      <Formik
-        initialValues={initialValues}
-        onSubmit={(values) => setup(values.phrase)}
-        validate={validator}
-      >
-        {({ values, errors, handleChange, handleSubmit }) => (
-          <form
-            className="cross-signing__setup-entry"
-            onSubmit={handleSubmit}
-            disabled={genWithPhrase !== undefined}
-          >
-            <Text>
-              Alternatively you can also set a <b>Security Phrase </b>
-              so you don't have to remember long Security Key, and optionally save the Key as
-              backup.
-            </Text>
-            <Input
-              name="phrase"
-              value={values.phrase}
-              onChange={handleChange}
-              label="Security Phrase"
-              type="password"
-              required
-              disabled={genWithPhrase !== undefined}
-            />
-            {errors.phrase && (
-              <Text variant="b3" className="cross-signing__error">
-                {errors.phrase}
-              </Text>
-            )}
-            <Input
-              name="confirmPhrase"
-              value={values.confirmPhrase}
-              onChange={handleChange}
-              label="Confirm Security Phrase"
-              type="password"
-              required
-              disabled={genWithPhrase !== undefined}
-            />
-            {errors.confirmPhrase && (
-              <Text variant="b3" className="cross-signing__error">
-                {errors.confirmPhrase}
-              </Text>
-            )}
-            {genWithPhrase !== true && (
-              <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>
-                Set Phrase & Generate Key
-              </Button>
-            )}
-            {genWithPhrase === true && <Spinner size="small" />}
-          </form>
-        )}
-      </Formik>
-    </div>
-  );
-}
-
-const setupDialog = () => {
-  openReusableDialog(
-    <Text variant="s1" weight="medium">
-      Setup cross signing
-    </Text>,
-    () => <CrossSigningSetup />
-  );
-};
-
-function CrossSigningReset() {
-  return (
-    <div className="cross-signing__reset">
-      <Text variant="h1">✋🧑‍🚒🤚</Text>
-      <Text weight="medium">Resetting cross-signing keys is permanent.</Text>
-      <Text>
-        Anyone you have verified with will see security alerts and your message backup will be lost.
-        You almost certainly do not want to do this, unless you have lost <b>Security Key</b> or{' '}
-        <b>Phrase</b> and every session you can cross-sign from.
-      </Text>
-      <Button variant="danger" onClick={setupDialog}>
-        Reset
-      </Button>
-    </div>
-  );
-}
-
-const resetDialog = () => {
-  openReusableDialog(
-    <Text variant="s1" weight="medium">
-      Reset cross signing
-    </Text>,
-    () => <CrossSigningReset />
-  );
-};
-
-function CrossSignin() {
-  const isCSEnabled = useCrossSigningStatus();
-  return (
-    <SettingTile
-      title="Cross signing"
-      content={
-        <Text variant="b3">
-          Setup to verify and keep track of all your sessions. Also required to backup encrypted
-          message.
-        </Text>
-      }
-      options={
-        isCSEnabled ? (
-          <Button variant="danger" onClick={resetDialog}>
-            Reset
-          </Button>
-        ) : (
-          <Button variant="primary" onClick={setupDialog}>
-            Setup
-          </Button>
-        )
-      }
-    />
-  );
-}
-
-export default CrossSignin;
diff --git a/src/app/organisms/settings/CrossSigning.scss b/src/app/organisms/settings/CrossSigning.scss
deleted file mode 100644 (file)
index b4b606d..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-.cross-signing {
-  &__setup {
-    padding: var(--sp-normal);
-  }
-  &__setup-entry {
-    & > *:not(:first-child) {
-      margin-top: var(--sp-normal);
-    }
-  }
-
-  &__error {
-    color: var(--tc-danger-high);
-    margin-top: var(--sp-ultra-tight) !important;
-  }
-
-  &__setup-divider {
-    margin: var(--sp-tight) 0;
-    display: flex;
-    align-items: center;
-  
-    &::before,
-    &::after {
-      flex: 1;
-      content: '';
-      margin: var(--sp-tight) 0;
-      border-bottom: 1px solid var(--bg-surface-border);
-    }
-  }
-}
-
-.cross-signing__key {
-  padding: var(--sp-normal);
-
-  &-text {
-    margin: var(--sp-normal) 0;
-    padding: var(--sp-extra-tight);
-    background-color: var(--bg-surface-low);
-    border-radius: var(--bo-radius);
-  }
-  &-btn {
-    display: flex;
-    & > button:last-child {
-      margin: 0 var(--sp-normal);
-    }
-  }
-}
-
-.cross-signing__failure,
-.cross-signing__reset {
-  padding: var(--sp-normal);
-  padding-top: var(--sp-extra-loose);
-  & > .text {
-    padding-bottom: var(--sp-normal);
-  }
-}
\ No newline at end of file
diff --git a/src/app/organisms/settings/DeviceManage.jsx b/src/app/organisms/settings/DeviceManage.jsx
deleted file mode 100644 (file)
index 9fa8273..0000000
+++ /dev/null
@@ -1,285 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import './DeviceManage.scss';
-import dateFormat from 'dateformat';
-
-import { isCrossVerified } from '../../../util/matrixUtil';
-import { openReusableDialog, openEmojiVerification } from '../../../client/action/navigation';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-import IconButton from '../../atoms/button/IconButton';
-import Input from '../../atoms/input/Input';
-import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
-import InfoCard from '../../atoms/card/InfoCard';
-import Spinner from '../../atoms/spinner/Spinner';
-import SettingTile from '../../molecules/setting-tile/SettingTile';
-
-import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
-import BinIC from '../../../../public/res/ic/outlined/bin.svg';
-import InfoIC from '../../../../public/res/ic/outlined/info.svg';
-
-import { authRequest } from './AuthRequest';
-import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
-
-import { useStore } from '../../hooks/useStore';
-import { useDeviceList } from '../../hooks/useDeviceList';
-import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
-import { accessSecretStorage } from './SecretStorageAccess';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-const promptDeviceName = async (deviceName) => new Promise((resolve) => {
-  let isCompleted = false;
-
-  const renderContent = (onComplete) => {
-    const handleSubmit = (e) => {
-      e.preventDefault();
-      const name = e.target.session.value;
-      if (typeof name !== 'string') onComplete(null);
-      onComplete(name);
-    };
-    return (
-      <form className="device-manage__rename" onSubmit={handleSubmit}>
-        <Input value={deviceName} label="Session name" name="session" />
-        <div className="device-manage__rename-btn">
-          <Button variant="primary" type="submit">Save</Button>
-          <Button onClick={() => onComplete(null)}>Cancel</Button>
-        </div>
-      </form>
-    );
-  };
-
-  openReusableDialog(
-    <Text variant="s1" weight="medium">Edit session name</Text>,
-    (requestClose) => renderContent((name) => {
-      isCompleted = true;
-      resolve(name);
-      requestClose();
-    }),
-    () => {
-      if (!isCompleted) resolve(null);
-    },
-  );
-});
-
-function DeviceManage() {
-  const TRUNCATED_COUNT = 4;
-  const mx = useMatrixClient();
-  const isCSEnabled = useCrossSigningStatus();
-  const deviceList = useDeviceList();
-  const [processing, setProcessing] = useState([]);
-  const [truncated, setTruncated] = useState(true);
-  const mountStore = useStore();
-  mountStore.setItem(true);
-  const isMeVerified = isCrossVerified(mx, mx.deviceId);
-
-  useEffect(() => {
-    setProcessing([]);
-  }, [deviceList]);
-
-  const addToProcessing = (device) => {
-    const old = [...processing];
-    old.push(device.device_id);
-    setProcessing(old);
-  };
-
-  const removeFromProcessing = () => {
-    setProcessing([]);
-  };
-
-  if (deviceList === null) {
-    return (
-      <div className="device-manage">
-        <div className="device-manage__loading">
-          <Spinner size="small" />
-          <Text>Loading sessions...</Text>
-        </div>
-      </div>
-    );
-  }
-
-  const handleRename = async (device) => {
-    const newName = await promptDeviceName(device.display_name);
-    if (newName === null || newName.trim() === '') return;
-    if (newName.trim() === device.display_name) return;
-    addToProcessing(device);
-    try {
-      await mx.setDeviceDetails(device.device_id, {
-        display_name: newName,
-      });
-    } catch {
-      if (!mountStore.getItem()) return;
-      removeFromProcessing(device);
-    }
-  };
-
-  const handleRemove = async (device) => {
-    const isConfirmed = await confirmDialog(
-      `Logout ${device.display_name}`,
-      `You are about to logout "${device.display_name}" session.`,
-      'Logout',
-      'danger',
-    );
-    if (!isConfirmed) return;
-    addToProcessing(device);
-    await authRequest(`Logout "${device.display_name}"`, async (auth) => {
-      await mx.deleteDevice(device.device_id, auth);
-    });
-
-    if (!mountStore.getItem()) return;
-    removeFromProcessing(device);
-  };
-
-  const verifyWithKey = async (device) => {
-    const keyData = await accessSecretStorage(mx, 'Session verification');
-    if (!keyData) return;
-    addToProcessing(device);
-    await mx.checkOwnCrossSigningTrust();
-  };
-
-  const verifyWithEmojis = async (deviceId) => {
-    const req = await mx.requestVerification(mx.getUserId(), [deviceId]);
-    openEmojiVerification(req, { userId: mx.getUserId(), deviceId });
-  };
-
-  const verify = (deviceId, isCurrentDevice) => {
-    if (isCurrentDevice) {
-      verifyWithKey(deviceId);
-      return;
-    }
-    verifyWithEmojis(deviceId);
-  };
-
-  const renderDevice = (device, isVerified) => {
-    const deviceId = device.device_id;
-    const displayName = device.display_name;
-    const lastIP = device.last_seen_ip;
-    const lastTS = device.last_seen_ts;
-    const isCurrentDevice = mx.deviceId === deviceId;
-    const canVerify = isVerified === false && (isMeVerified || isCurrentDevice);
-
-    return (
-      <SettingTile
-        key={deviceId}
-        title={(
-          <Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
-            {displayName}
-            <Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
-            {isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">Current</Text>}
-          </Text>
-        )}
-        options={
-          processing.includes(deviceId)
-            ? <Spinner size="small" />
-            : (
-              <>
-                {(isCSEnabled && canVerify) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">Verify</Button>}
-                <IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
-                <IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
-              </>
-            )
-        }
-        content={(
-          <>
-            {lastTS && (
-              <Text variant="b3">
-                Last activity
-                <span style={{ color: 'var(--tc-surface-normal)' }}>
-                  {dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
-                </span>
-                {lastIP ? ` at ${lastIP}` : ''}
-              </Text>
-            )}
-            {isCurrentDevice && (
-              <Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
-                {`Session Key: ${mx.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
-              </Text>
-            )}
-          </>
-        )}
-      />
-    );
-  };
-
-  const unverified = [];
-  const verified = [];
-  const noEncryption = [];
-  deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => {
-    const isVerified = isCrossVerified(mx, device.device_id);
-    if (isVerified === true) {
-      verified.push(device);
-    } else if (isVerified === false) {
-      unverified.push(device);
-    } else {
-      noEncryption.push(device);
-    }
-  });
-  return (
-    <div className="device-manage">
-      <div>
-        <MenuHeader>Unverified sessions</MenuHeader>
-        {!isMeVerified && isCSEnabled && (
-          <div style={{ padding: 'var(--sp-extra-tight) var(--sp-normal)' }}>
-            <InfoCard
-              rounded
-              variant="primary"
-              iconSrc={InfoIC}
-              title="Verify this session either with your Security Key/Phrase here or by initiating emoji verification from a verified session."
-            />
-          </div>
-        )}
-        {isMeVerified && unverified.length > 0 && (
-          <div style={{ padding: 'var(--sp-extra-tight) var(--sp-normal)' }}>
-            <InfoCard
-              rounded
-              variant="surface"
-              iconSrc={InfoIC}
-              title="Verify other sessions by emoji verification or remove unfamiliar ones."
-            />
-          </div>
-        )}
-        {!isCSEnabled && (
-          <div style={{ padding: 'var(--sp-extra-tight) var(--sp-normal)' }}>
-            <InfoCard
-              rounded
-              variant="caution"
-              iconSrc={InfoIC}
-              title="Setup cross signing in case you lose all your sessions."
-            />
-          </div>
-        )}
-        {
-          unverified.length > 0
-            ? unverified.map((device) => renderDevice(device, false))
-            : <Text className="device-manage__info">No unverified sessions</Text>
-        }
-      </div>
-      {noEncryption.length > 0 && (
-      <div>
-        <MenuHeader>Sessions without encryption support</MenuHeader>
-        {noEncryption.map((device) => renderDevice(device, null))}
-      </div>
-      )}
-      <div>
-        <MenuHeader>Verified sessions</MenuHeader>
-        {
-          verified.length > 0
-            ? verified.map((device, index) => {
-              if (truncated && index >= TRUNCATED_COUNT) return null;
-              return renderDevice(device, true);
-            })
-            : <Text className="device-manage__info">No verified sessions</Text>
-        }
-        { verified.length > TRUNCATED_COUNT && (
-          <Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
-            {truncated ? `View ${verified.length - 4} more` : 'View less'}
-          </Button>
-        )}
-        { deviceList.length > 0 && (
-          <Text className="device-manage__info" variant="b3">Session names are visible to everyone, so do not put any private info here.</Text>
-        )}
-      </div>
-    </div>
-  );
-}
-
-export default DeviceManage;
diff --git a/src/app/organisms/settings/DeviceManage.scss b/src/app/organisms/settings/DeviceManage.scss
deleted file mode 100644 (file)
index 5116cda..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-@use '../../partials/flex';
-
-.device-manage {
-  &__loading {
-    @extend .cp-fx__row--c-c;
-    padding: var(--sp-extra-loose) var(--sp-normal);
-
-    .text {
-      margin: 0 var(--sp-normal);
-    }
-  }
-  &__info {
-    margin: var(--sp-normal);
-  }
-  & .setting-tile:last-of-type {
-     border-bottom: none;
-  }
-  & .setting-tile__options {
-    display: flex;
-    align-items: center;
-    gap: var(--sp-ultra-tight);
-    & .btn-positive {
-      padding: 6px var(--sp-tight);
-      min-width: 0;
-    }
-  }
-
-  &__current-label {
-    margin: 0 var(--sp-extra-tight);
-    padding: 2px var(--sp-ultra-tight);
-    color: var(--bg-surface);
-    background-color: var(--tc-surface-low);
-    border-radius: 4px;
-  }
-
-  &__rename {
-    padding: var(--sp-normal);
-    & > *:not(:last-child) {
-      margin-bottom: var(--sp-normal);
-    }
-    &-btn {
-      display: flex;
-      gap: var(--sp-normal);
-    }
-  }
-}
\ No newline at end of file
diff --git a/src/app/organisms/settings/KeyBackup.jsx b/src/app/organisms/settings/KeyBackup.jsx
deleted file mode 100644 (file)
index 47be511..0000000
+++ /dev/null
@@ -1,303 +0,0 @@
-/* eslint-disable react/prop-types */
-import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
-import './KeyBackup.scss';
-
-import { openReusableDialog } from '../../../client/action/navigation';
-import { deletePrivateKey } from '../../../client/state/secretStorageKeys';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-import IconButton from '../../atoms/button/IconButton';
-import Spinner from '../../atoms/spinner/Spinner';
-import InfoCard from '../../atoms/card/InfoCard';
-import SettingTile from '../../molecules/setting-tile/SettingTile';
-
-import { accessSecretStorage } from './SecretStorageAccess';
-
-import InfoIC from '../../../../public/res/ic/outlined/info.svg';
-import BinIC from '../../../../public/res/ic/outlined/bin.svg';
-import DownloadIC from '../../../../public/res/ic/outlined/download.svg';
-
-import { useStore } from '../../hooks/useStore';
-import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-function CreateKeyBackupDialog({ keyData }) {
-  const [done, setDone] = useState(false);
-  const mx = useMatrixClient();
-  const mountStore = useStore();
-
-  const doBackup = async () => {
-    setDone(false);
-    let info;
-
-    try {
-      info = await mx.prepareKeyBackupVersion(null, { secureSecretStorage: true });
-      info = await mx.createKeyBackupVersion(info);
-      await mx.scheduleAllGroupSessionsForBackup();
-      if (!mountStore.getItem()) return;
-      setDone(true);
-    } catch (e) {
-      deletePrivateKey(keyData.keyId);
-      await mx.deleteKeyBackupVersion(info.version);
-      if (!mountStore.getItem()) return;
-      setDone(null);
-    }
-  };
-
-  useEffect(() => {
-    mountStore.setItem(true);
-    doBackup();
-  }, []);
-
-  return (
-    <div className="key-backup__create">
-      {done === false && (
-        <div>
-          <Spinner size="small" />
-          <Text>Creating backup...</Text>
-        </div>
-      )}
-      {done === true && (
-        <>
-          <Text variant="h1">✅</Text>
-          <Text>Successfully created backup</Text>
-        </>
-      )}
-      {done === null && (
-        <>
-          <Text>Failed to create backup</Text>
-          <Button onClick={doBackup}>Retry</Button>
-        </>
-      )}
-    </div>
-  );
-}
-CreateKeyBackupDialog.propTypes = {
-  keyData: PropTypes.shape({}).isRequired,
-};
-
-function RestoreKeyBackupDialog({ keyData }) {
-  const [status, setStatus] = useState(false);
-  const mx = useMatrixClient();
-  const mountStore = useStore();
-
-  const restoreBackup = async () => {
-    setStatus(false);
-
-    let meBreath = true;
-    const progressCallback = (progress) => {
-      if (!progress.successes) return;
-      if (meBreath === false) return;
-      meBreath = false;
-      setTimeout(() => {
-        meBreath = true;
-      }, 200);
-
-      setStatus({ message: `Restoring backup keys... (${progress.successes}/${progress.total})` });
-    };
-
-    try {
-      const backupInfo = await mx.getKeyBackupVersion();
-      const info = await mx.restoreKeyBackupWithSecretStorage(backupInfo, undefined, undefined, {
-        progressCallback,
-      });
-      if (!mountStore.getItem()) return;
-      setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
-    } catch (e) {
-      if (!mountStore.getItem()) return;
-      if (e.errcode === 'RESTORE_BACKUP_ERROR_BAD_KEY') {
-        deletePrivateKey(keyData.keyId);
-        setStatus({ error: 'Failed to restore backup. Key is invalid!', errorCode: 'BAD_KEY' });
-      } else {
-        setStatus({ error: 'Failed to restore backup.', errCode: 'UNKNOWN' });
-      }
-    }
-  };
-
-  useEffect(() => {
-    mountStore.setItem(true);
-    restoreBackup();
-  }, []);
-
-  return (
-    <div className="key-backup__restore">
-      {(status === false || status.message) && (
-        <div>
-          <Spinner size="small" />
-          <Text>{status.message ?? 'Restoring backup keys...'}</Text>
-        </div>
-      )}
-      {status.done && (
-        <>
-          <Text variant="h1">✅</Text>
-          <Text>{status.done}</Text>
-        </>
-      )}
-      {status.error && (
-        <>
-          <Text>{status.error}</Text>
-          <Button onClick={restoreBackup}>Retry</Button>
-        </>
-      )}
-    </div>
-  );
-}
-RestoreKeyBackupDialog.propTypes = {
-  keyData: PropTypes.shape({}).isRequired,
-};
-
-function DeleteKeyBackupDialog({ requestClose }) {
-  const [isDeleting, setIsDeleting] = useState(false);
-  const mx = useMatrixClient();
-  const mountStore = useStore();
-
-  const deleteBackup = async () => {
-    mountStore.setItem(true);
-    setIsDeleting(true);
-    try {
-      const backupInfo = await mx.getKeyBackupVersion();
-      if (backupInfo) await mx.deleteKeyBackupVersion(backupInfo.version);
-      if (!mountStore.getItem()) return;
-      requestClose(true);
-    } catch {
-      if (!mountStore.getItem()) return;
-      setIsDeleting(false);
-    }
-  };
-
-  return (
-    <div className="key-backup__delete">
-      <Text variant="h1">🗑</Text>
-      <Text weight="medium">Deleting key backup is permanent.</Text>
-      <Text>All encrypted messages keys stored on server will be deleted.</Text>
-      {isDeleting ? (
-        <Spinner size="small" />
-      ) : (
-        <Button variant="danger" onClick={deleteBackup}>
-          Delete
-        </Button>
-      )}
-    </div>
-  );
-}
-DeleteKeyBackupDialog.propTypes = {
-  requestClose: PropTypes.func.isRequired,
-};
-
-function KeyBackup() {
-  const mx = useMatrixClient();
-  const isCSEnabled = useCrossSigningStatus();
-  const [keyBackup, setKeyBackup] = useState(undefined);
-  const mountStore = useStore();
-
-  const fetchKeyBackupVersion = async () => {
-    const info = await mx.getKeyBackupVersion();
-    if (!mountStore.getItem()) return;
-    setKeyBackup(info);
-  };
-
-  useEffect(() => {
-    mountStore.setItem(true);
-    fetchKeyBackupVersion();
-
-    const handleAccountData = (event) => {
-      if (event.getType() === 'm.megolm_backup.v1') {
-        fetchKeyBackupVersion();
-      }
-    };
-
-    mx.on('accountData', handleAccountData);
-    return () => {
-      mx.removeListener('accountData', handleAccountData);
-    };
-  }, [isCSEnabled]);
-
-  const openCreateKeyBackup = async () => {
-    const keyData = await accessSecretStorage(mx, 'Create Key Backup');
-    if (keyData === null) return;
-
-    openReusableDialog(
-      <Text variant="s1" weight="medium">
-        Create Key Backup
-      </Text>,
-      () => <CreateKeyBackupDialog keyData={keyData} />,
-      () => fetchKeyBackupVersion()
-    );
-  };
-
-  const openRestoreKeyBackup = async () => {
-    const keyData = await accessSecretStorage(mx, 'Restore Key Backup');
-    if (keyData === null) return;
-
-    openReusableDialog(
-      <Text variant="s1" weight="medium">
-        Restore Key Backup
-      </Text>,
-      () => <RestoreKeyBackupDialog keyData={keyData} />
-    );
-  };
-
-  const openDeleteKeyBackup = () =>
-    openReusableDialog(
-      <Text variant="s1" weight="medium">
-        Delete Key Backup
-      </Text>,
-      (requestClose) => (
-        <DeleteKeyBackupDialog
-          requestClose={(isDone) => {
-            if (isDone) setKeyBackup(null);
-            requestClose();
-          }}
-        />
-      )
-    );
-
-  const renderOptions = () => {
-    if (keyBackup === undefined) return <Spinner size="small" />;
-    if (keyBackup === null)
-      return (
-        <Button variant="primary" onClick={openCreateKeyBackup}>
-          Create Backup
-        </Button>
-      );
-    return (
-      <>
-        <IconButton
-          src={DownloadIC}
-          variant="positive"
-          onClick={openRestoreKeyBackup}
-          tooltip="Restore backup"
-        />
-        <IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
-      </>
-    );
-  };
-
-  return (
-    <SettingTile
-      title="Encrypted messages backup"
-      content={
-        <>
-          <Text variant="b3">
-            Online backup your encrypted messages keys with your account data in case you lose
-            access to your sessions. Your keys will be secured with a unique Security Key.
-          </Text>
-          {!isCSEnabled && (
-            <InfoCard
-              style={{ marginTop: 'var(--sp-ultra-tight)' }}
-              rounded
-              variant="caution"
-              iconSrc={InfoIC}
-              title="Setup cross signing to backup your encrypted messages."
-            />
-          )}
-        </>
-      }
-      options={isCSEnabled ? renderOptions() : null}
-    />
-  );
-}
-
-export default KeyBackup;
diff --git a/src/app/organisms/settings/KeyBackup.scss b/src/app/organisms/settings/KeyBackup.scss
deleted file mode 100644 (file)
index 1f2b9b6..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-.key-backup__create,
-.key-backup__restore {
-  padding: var(--sp-normal);
-
-  & > div {
-    padding: var(--sp-normal) 0;
-    display: flex;
-    align-items: center;
-  
-    & > .text {
-      margin: 0 var(--sp-normal);
-    }
-  }
-
-  & > .text {
-    margin-bottom: var(--sp-normal);
-  }
-}
-
-.key-backup__delete {
-  padding: var(--sp-normal);
-  padding-top: var(--sp-extra-loose);
-
-  & > .text {
-    padding-bottom: var(--sp-normal);
-  }
-}
\ No newline at end of file
diff --git a/src/app/organisms/settings/SecretStorageAccess.jsx b/src/app/organisms/settings/SecretStorageAccess.jsx
deleted file mode 100644 (file)
index 02882a4..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-import './SecretStorageAccess.scss';
-import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
-
-import { openReusableDialog } from '../../../client/action/navigation';
-import { getDefaultSSKey, getSSKeyInfo } from '../../../util/matrixUtil';
-import { storePrivateKey, hasPrivateKey, getPrivateKey } from '../../../client/state/secretStorageKeys';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-import Input from '../../atoms/input/Input';
-import Spinner from '../../atoms/spinner/Spinner';
-
-import { useStore } from '../../hooks/useStore';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-function SecretStorageAccess({ onComplete }) {
-  const mx = useMatrixClient();
-  const sSKeyId = getDefaultSSKey(mx);
-  const sSKeyInfo = getSSKeyInfo(mx, sSKeyId);
-  const isPassphrase = !!sSKeyInfo.passphrase;
-  const [withPhrase, setWithPhrase] = useState(isPassphrase);
-  const [process, setProcess] = useState(false);
-  const [error, setError] = useState(null);
-  const mountStore = useStore();
-
-  const toggleWithPhrase = () => setWithPhrase(!withPhrase);
-
-  const processInput = async ({ key, phrase }) => {
-    mountStore.setItem(true);
-    setProcess(true);
-    try {
-      const { salt, iterations } = sSKeyInfo.passphrase || {};
-      const privateKey = key
-        ? mx.keyBackupKeyFromRecoveryKey(key)
-        : await deriveKey(phrase, salt, iterations);
-      const isCorrect = await mx.checkSecretStorageKey(privateKey, sSKeyInfo);
-
-      if (!mountStore.getItem()) return;
-      if (!isCorrect) {
-        setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
-        setProcess(false);
-        return;
-      }
-      onComplete({
-        keyId: sSKeyId,
-        key,
-        phrase,
-        privateKey,
-      });
-    } catch (e) {
-      if (!mountStore.getItem()) return;
-      setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
-      setProcess(false);
-    }
-  };
-
-  const handleForm = async (e) => {
-    e.preventDefault();
-    const password = e.target.password.value;
-    if (password.trim() === '') return;
-    const data = {};
-    if (withPhrase) data.phrase = password;
-    else data.key = password;
-    processInput(data);
-  };
-
-  const handleChange = () => {
-    setError(null);
-    setProcess(false);
-  };
-
-  return (
-    <div className="secret-storage-access">
-      <form onSubmit={handleForm}>
-        <Input
-          name="password"
-          label={`Security ${withPhrase ? 'Phrase' : 'Key'}`}
-          type="password"
-          onChange={handleChange}
-          required
-        />
-        {error && <Text variant="b3">{error}</Text>}
-        {!process && (
-          <div className="secret-storage-access__btn">
-            <Button variant="primary" type="submit">Continue</Button>
-            {isPassphrase && <Button onClick={toggleWithPhrase}>{`Use Security ${withPhrase ? 'Key' : 'Phrase'}`}</Button>}
-          </div>
-        )}
-      </form>
-      {process && <Spinner size="small" />}
-    </div>
-  );
-}
-SecretStorageAccess.propTypes = {
-  onComplete: PropTypes.func.isRequired,
-};
-
-/**
- * @param {MatrixClient} mx Matrix client
- * @param {string} title Title of secret storage access dialog
- * @returns {Promise<keyData | null>} resolve to keyData or null
- */
-export const accessSecretStorage = (mx, title) => new Promise((resolve) => {
-  let isCompleted = false;
-  const defaultSSKey = getDefaultSSKey(mx);
-  if (hasPrivateKey(defaultSSKey)) {
-    resolve({ keyId: defaultSSKey, privateKey: getPrivateKey(defaultSSKey) });
-    return;
-  }
-  const handleComplete = (keyData) => {
-    isCompleted = true;
-    storePrivateKey(keyData.keyId, keyData.privateKey);
-    resolve(keyData);
-  };
-
-  openReusableDialog(
-    <Text variant="s1" weight="medium">{title}</Text>,
-    (requestClose) => (
-      <SecretStorageAccess
-        onComplete={(keyData) => {
-          handleComplete(keyData);
-          requestClose(requestClose);
-        }}
-      />
-    ),
-    () => {
-      if (!isCompleted) resolve(null);
-    },
-  );
-});
-
-export default SecretStorageAccess;
diff --git a/src/app/organisms/settings/SecretStorageAccess.scss b/src/app/organisms/settings/SecretStorageAccess.scss
deleted file mode 100644 (file)
index a7c0a9f..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-.secret-storage-access {
-  padding: var(--sp-normal);
-
-  & form > *:not(:first-child) {
-    margin-top: var(--sp-normal);
-  }
-
-  & .text-b3 {
-    color: var(--tc-danger-high);
-    margin-top: var(--sp-ultra-tight) !important;
-  }
-
-  &__btn {
-    display: flex;
-    justify-content: space-between;
-  }
-  & .donut-spinner {
-    margin-top: var(--sp-normal);
-  }
-}
\ No newline at end of file
diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx
deleted file mode 100644 (file)
index 6329a57..0000000
+++ /dev/null
@@ -1,660 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Input, toRem } from 'folds';
-import { isKeyHotkey } from 'is-hotkey';
-import './Settings.scss';
-
-import { clearCacheAndReload, logoutClient } from '../../../client/initMatrix';
-import cons from '../../../client/state/cons';
-import settings from '../../../client/state/settings';
-import navigation from '../../../client/state/navigation';
-import { toggleSystemTheme } from '../../../client/action/settings';
-import { usePermissionState } from '../../hooks/usePermission';
-
-import Text from '../../atoms/text/Text';
-import IconButton from '../../atoms/button/IconButton';
-import Button from '../../atoms/button/Button';
-import Toggle from '../../atoms/button/Toggle';
-import Tabs from '../../atoms/tabs/Tabs';
-import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
-import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
-
-import PopupWindow from '../../molecules/popup-window/PopupWindow';
-import SettingTile from '../../molecules/setting-tile/SettingTile';
-import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
-import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
-import { ImagePackUser, ImagePackGlobal } from '../../molecules/image-pack/ImagePack';
-import GlobalNotification from '../../molecules/global-notification/GlobalNotification';
-import KeywordNotification from '../../molecules/global-notification/KeywordNotification';
-import IgnoreUserList from '../../molecules/global-notification/IgnoreUserList';
-
-import ProfileEditor from '../profile-editor/ProfileEditor';
-import CrossSigning from './CrossSigning';
-import KeyBackup from './KeyBackup';
-import DeviceManage from './DeviceManage';
-
-import SunIC from '../../../../public/res/ic/outlined/sun.svg';
-import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
-import LockIC from '../../../../public/res/ic/outlined/lock.svg';
-import BellIC from '../../../../public/res/ic/outlined/bell.svg';
-import InfoIC from '../../../../public/res/ic/outlined/info.svg';
-import PowerIC from '../../../../public/res/ic/outlined/power.svg';
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-
-import CinnySVG from '../../../../public/res/svg/cinny.svg';
-import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
-import { useSetting } from '../../state/hooks/settings';
-import { settingsAtom } from '../../state/settings';
-import { isMacOS } from '../../utils/user-agent';
-import { KeySymbol } from '../../utils/key-symbol';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-function AppearanceSection() {
-  const [, updateState] = useState({});
-
-  const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
-  const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout');
-  const [messageSpacing, setMessageSpacing] = useSetting(settingsAtom, 'messageSpacing');
-  const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
-  const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom');
-  const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
-  const [hideMembershipEvents, setHideMembershipEvents] = useSetting(
-    settingsAtom,
-    'hideMembershipEvents'
-  );
-  const [hideNickAvatarEvents, setHideNickAvatarEvents] = useSetting(
-    settingsAtom,
-    'hideNickAvatarEvents'
-  );
-  const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
-  const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview');
-  const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
-  const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
-  const spacings = ['0', '100', '200', '300', '400', '500'];
-
-  const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`);
-
-  const handleZoomChange = (evt) => {
-    setCurrentZoom(evt.target.value);
-  };
-
-  const handleZoomEnter = (evt) => {
-    if (isKeyHotkey('escape', evt)) {
-      evt.stopPropagation();
-      setCurrentZoom(pageZoom);
-    }
-    if (isKeyHotkey('enter', evt)) {
-      const newZoom = parseInt(evt.target.value, 10);
-      if (Number.isNaN(newZoom)) return;
-      const safeZoom = Math.max(Math.min(newZoom, 150), 75);
-      setPageZoom(safeZoom);
-      setCurrentZoom(safeZoom);
-    }
-  };
-
-  return (
-    <div className="settings-appearance">
-      <div className="settings-appearance__card">
-        <MenuHeader>Theme</MenuHeader>
-        <SettingTile
-          title="Follow system theme"
-          options={
-            <Toggle
-              isActive={settings.useSystemTheme}
-              onToggle={() => {
-                toggleSystemTheme();
-                updateState({});
-              }}
-            />
-          }
-          content={<Text variant="b3">Use light or dark mode based on the system settings.</Text>}
-        />
-        <SettingTile
-          title="Theme"
-          content={
-            <SegmentedControls
-              selected={settings.useSystemTheme ? -1 : settings.getThemeIndex()}
-              segments={[
-                { text: 'Light' },
-                { text: 'Silver' },
-                { text: 'Dark' },
-                { text: 'Butter' },
-              ]}
-              onSelect={(index) => {
-                if (settings.useSystemTheme) toggleSystemTheme();
-                settings.setTheme(index);
-                updateState({});
-              }}
-            />
-          }
-        />
-        <SettingTile
-          title="Use Twitter Emoji"
-          options={
-            <Toggle isActive={twitterEmoji} onToggle={() => setTwitterEmoji(!twitterEmoji)} />
-          }
-          content={<Text variant="b3">Use Twitter emoji instead of system emoji.</Text>}
-        />
-        <SettingTile
-          title="Page Zoom"
-          options={
-            <Input
-              style={{ width: toRem(150) }}
-              variant={pageZoom === parseInt(currentZoom, 10) ? 'Background' : 'Primary'}
-              size="400"
-              type="number"
-              min="75"
-              max="150"
-              value={currentZoom}
-              onChange={handleZoomChange}
-              onKeyDown={handleZoomEnter}
-              outlined
-              after={<Text variant="b2">%</Text>}
-            />
-          }
-          content={
-            <Text variant="b3">
-              Change page zoom to scale user interface between 75% to 150%. Default: 100%
-            </Text>
-          }
-        />
-      </div>
-      <div className="settings-appearance__card">
-        <MenuHeader>Room messages</MenuHeader>
-        <SettingTile
-          title="Message Layout"
-          content={
-            <SegmentedControls
-              selected={messageLayout}
-              segments={[{ text: 'Modern' }, { text: 'Compact' }, { text: 'Bubble' }]}
-              onSelect={(index) => setMessageLayout(index)}
-            />
-          }
-        />
-        <SettingTile
-          title="Message Spacing"
-          content={
-            <SegmentedControls
-              selected={spacings.findIndex((s) => s === messageSpacing)}
-              segments={[
-                { text: 'No' },
-                { text: 'XXS' },
-                { text: 'XS' },
-                { text: 'S' },
-                { text: 'M' },
-                { text: 'L' },
-              ]}
-              onSelect={(index) => {
-                setMessageSpacing(spacings[index]);
-              }}
-            />
-          }
-        />
-        <SettingTile
-          title="Use ENTER for Newline"
-          options={
-            <Toggle
-              isActive={enterForNewline}
-              onToggle={() => setEnterForNewline(!enterForNewline)}
-            />
-          }
-          content={
-            <Text variant="b3">{`Use ${
-              isMacOS() ? KeySymbol.Command : 'Ctrl'
-            } + ENTER to send message and ENTER for newline.`}</Text>
-          }
-        />
-        <SettingTile
-          title="Markdown formatting"
-          options={<Toggle isActive={isMarkdown} onToggle={() => setIsMarkdown(!isMarkdown)} />}
-          content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
-        />
-        <SettingTile
-          title="Hide membership events"
-          options={
-            <Toggle
-              isActive={hideMembershipEvents}
-              onToggle={() => setHideMembershipEvents(!hideMembershipEvents)}
-            />
-          }
-          content={
-            <Text variant="b3">
-              Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and
-              Ban)
-            </Text>
-          }
-        />
-        <SettingTile
-          title="Hide nick/avatar events"
-          options={
-            <Toggle
-              isActive={hideNickAvatarEvents}
-              onToggle={() => setHideNickAvatarEvents(!hideNickAvatarEvents)}
-            />
-          }
-          content={
-            <Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>
-          }
-        />
-        <SettingTile
-          title="Disable media auto load"
-          options={
-            <Toggle isActive={!mediaAutoLoad} onToggle={() => setMediaAutoLoad(!mediaAutoLoad)} />
-          }
-          content={
-            <Text variant="b3">Prevent images and videos from auto loading to save bandwidth.</Text>
-          }
-        />
-        <SettingTile
-          title="Url Preview"
-          options={<Toggle isActive={urlPreview} onToggle={() => setUrlPreview(!urlPreview)} />}
-          content={<Text variant="b3">Show url preview for link in messages.</Text>}
-        />
-        <SettingTile
-          title="Url Preview in Encrypted Room"
-          options={
-            <Toggle isActive={encUrlPreview} onToggle={() => setEncUrlPreview(!encUrlPreview)} />
-          }
-          content={<Text variant="b3">Show url preview for link in encrypted messages.</Text>}
-        />
-        <SettingTile
-          title="Show hidden events"
-          options={
-            <Toggle
-              isActive={showHiddenEvents}
-              onToggle={() => setShowHiddenEvents(!showHiddenEvents)}
-            />
-          }
-          content={<Text variant="b3">Show hidden state and message events.</Text>}
-        />
-      </div>
-    </div>
-  );
-}
-
-function NotificationsSection() {
-  const notifPermission = usePermissionState(
-    'notifications',
-    window.Notification?.permission ?? 'denied'
-  );
-  const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications');
-  const [isNotificationSounds, setIsNotificationSounds] = useSetting(
-    settingsAtom,
-    'isNotificationSounds'
-  );
-
-  const renderOptions = () => {
-    if (window.Notification === undefined) {
-      return (
-        <Text className="settings-notifications__not-supported">
-          Not supported in this browser.
-        </Text>
-      );
-    }
-
-    if (notifPermission === 'denied') {
-      return <Text>Permission Denied</Text>;
-    }
-
-    if (notifPermission === 'granted') {
-      return (
-        <Toggle
-          isActive={showNotifications}
-          onToggle={() => {
-            setShowNotifications(!showNotifications);
-          }}
-        />
-      );
-    }
-
-    return (
-      <Button
-        variant="primary"
-        onClick={() =>
-          window.Notification.requestPermission().then(() => {
-            setShowNotifications(window.Notification?.permission === 'granted');
-          })
-        }
-      >
-        Request permission
-      </Button>
-    );
-  };
-
-  return (
-    <>
-      <div className="settings-notifications">
-        <MenuHeader>Notification & Sound</MenuHeader>
-        <SettingTile
-          title="Desktop notification"
-          options={renderOptions()}
-          content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
-        />
-        <SettingTile
-          title="Notification Sound"
-          options={
-            <Toggle
-              isActive={isNotificationSounds}
-              onToggle={() => setIsNotificationSounds(!isNotificationSounds)}
-            />
-          }
-          content={<Text variant="b3">Play sound when new messages arrive.</Text>}
-        />
-      </div>
-      <GlobalNotification />
-      <KeywordNotification />
-      <IgnoreUserList />
-    </>
-  );
-}
-
-function EmojiSection() {
-  return (
-    <>
-      <div className="settings-emoji__card">
-        <ImagePackUser />
-      </div>
-      <div className="settings-emoji__card">
-        <ImagePackGlobal />
-      </div>
-    </>
-  );
-}
-
-function SecuritySection() {
-  return (
-    <div className="settings-security">
-      <div className="settings-security__card">
-        <MenuHeader>Cross signing and backup</MenuHeader>
-        <CrossSigning />
-        <KeyBackup />
-      </div>
-      <DeviceManage />
-      <div className="settings-security__card">
-        <MenuHeader>Export/Import encryption keys</MenuHeader>
-        <SettingTile
-          title="Export E2E room keys"
-          content={
-            <>
-              <Text variant="b3">
-                Export end-to-end encryption room keys to decrypt old messages in other session. In
-                order to encrypt keys you need to set a password, which will be used while
-                importing.
-              </Text>
-              <ExportE2ERoomKeys />
-            </>
-          }
-        />
-        <SettingTile
-          title="Import E2E room keys"
-          content={
-            <>
-              <Text variant="b3">
-                {
-                  "To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you'll have to enter the password you set in order to decrypt it."
-                }
-              </Text>
-              <ImportE2ERoomKeys />
-            </>
-          }
-        />
-      </div>
-    </div>
-  );
-}
-
-function AboutSection() {
-  const mx = useMatrixClient();
-
-  return (
-    <div className="settings-about">
-      <div className="settings-about__card">
-        <MenuHeader>Application</MenuHeader>
-        <div className="settings-about__branding">
-          <img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
-          <div>
-            <Text variant="h2" weight="medium">
-              Cinny
-              <span
-                className="text text-b3"
-                style={{ margin: '0 var(--sp-extra-tight)' }}
-              >{`v${cons.version}`}</span>
-            </Text>
-            <Text>Yet another matrix client</Text>
-
-            <div className="settings-about__btns">
-              <Button onClick={() => window.open('https://github.com/ajbura/cinny')}>
-                Source code
-              </Button>
-              <Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
-              <Button onClick={() => clearCacheAndReload(mx)} variant="danger">
-                Clear cache & reload
-              </Button>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div className="settings-about__card">
-        <MenuHeader>Credits</MenuHeader>
-        <div className="settings-about__credits">
-          <ul>
-            <li>
-              {/* eslint-disable-next-line react/jsx-one-expression-per-line */}
-              <Text>
-                The{' '}
-                <a
-                  href="https://github.com/matrix-org/matrix-js-sdk"
-                  rel="noreferrer noopener"
-                  target="_blank"
-                >
-                  matrix-js-sdk
-                </a>{' '}
-                is ©{' '}
-                <a href="https://matrix.org/foundation" rel="noreferrer noopener" target="_blank">
-                  The Matrix.org Foundation C.I.C
-                </a>{' '}
-                used under the terms of{' '}
-                <a
-                  href="http://www.apache.org/licenses/LICENSE-2.0"
-                  rel="noreferrer noopener"
-                  target="_blank"
-                >
-                  Apache 2.0
-                </a>
-                .
-              </Text>
-            </li>
-            <li>
-              {/* eslint-disable-next-line react/jsx-one-expression-per-line */}
-              <Text>
-                The{' '}
-                <a
-                  href="https://github.com/mozilla/twemoji-colr"
-                  target="_blank"
-                  rel="noreferrer noopener"
-                >
-                  twemoji-colr
-                </a>{' '}
-                font is ©{' '}
-                <a href="https://mozilla.org/" target="_blank" rel="noreferrer noopener">
-                  Mozilla Foundation
-                </a>{' '}
-                used under the terms of{' '}
-                <a
-                  href="http://www.apache.org/licenses/LICENSE-2.0"
-                  target="_blank"
-                  rel="noreferrer noopener"
-                >
-                  Apache 2.0
-                </a>
-                .
-              </Text>
-            </li>
-            <li>
-              {/* eslint-disable-next-line react/jsx-one-expression-per-line */}
-              <Text>
-                The{' '}
-                <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">
-                  Twemoji
-                </a>{' '}
-                emoji art is ©{' '}
-                <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">
-                  Twitter, Inc and other contributors
-                </a>{' '}
-                used under the terms of{' '}
-                <a
-                  href="https://creativecommons.org/licenses/by/4.0/"
-                  target="_blank"
-                  rel="noreferrer noopener"
-                >
-                  CC-BY 4.0
-                </a>
-                .
-              </Text>
-            </li>
-            <li>
-              {/* eslint-disable-next-line react/jsx-one-expression-per-line */}
-              <Text>
-                The{' '}
-                <a
-                  href="https://material.io/design/sound/sound-resources.html"
-                  target="_blank"
-                  rel="noreferrer noopener"
-                >
-                  Material sound resources
-                </a>{' '}
-                are ©{' '}
-                <a href="https://google.com" target="_blank" rel="noreferrer noopener">
-                  Google
-                </a>{' '}
-                used under the terms of{' '}
-                <a
-                  href="https://creativecommons.org/licenses/by/4.0/"
-                  target="_blank"
-                  rel="noreferrer noopener"
-                >
-                  CC-BY 4.0
-                </a>
-                .
-              </Text>
-            </li>
-          </ul>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-export const tabText = {
-  APPEARANCE: 'Appearance',
-  NOTIFICATIONS: 'Notifications',
-  EMOJI: 'Emoji',
-  SECURITY: 'Security',
-  ABOUT: 'About',
-};
-const tabItems = [
-  {
-    text: tabText.APPEARANCE,
-    iconSrc: SunIC,
-    disabled: false,
-    render: () => <AppearanceSection />,
-  },
-  {
-    text: tabText.NOTIFICATIONS,
-    iconSrc: BellIC,
-    disabled: false,
-    render: () => <NotificationsSection />,
-  },
-  {
-    text: tabText.EMOJI,
-    iconSrc: EmojiIC,
-    disabled: false,
-    render: () => <EmojiSection />,
-  },
-  {
-    text: tabText.SECURITY,
-    iconSrc: LockIC,
-    disabled: false,
-    render: () => <SecuritySection />,
-  },
-  {
-    text: tabText.ABOUT,
-    iconSrc: InfoIC,
-    disabled: false,
-    render: () => <AboutSection />,
-  },
-];
-
-function useWindowToggle(setSelectedTab) {
-  const [isOpen, setIsOpen] = useState(false);
-
-  useEffect(() => {
-    const openSettings = (tab) => {
-      const tabItem = tabItems.find((item) => item.text === tab);
-      if (tabItem) setSelectedTab(tabItem);
-      setIsOpen(true);
-    };
-    navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
-    return () => {
-      navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
-    };
-  }, []);
-
-  const requestClose = () => setIsOpen(false);
-
-  return [isOpen, requestClose];
-}
-
-function Settings() {
-  const [selectedTab, setSelectedTab] = useState(tabItems[0]);
-  const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
-  const mx = useMatrixClient();
-
-  const handleTabChange = (tabItem) => setSelectedTab(tabItem);
-  const handleLogout = async () => {
-    if (
-      await confirmDialog(
-        'Logout',
-        'Are you sure that you want to logout your session?',
-        'Logout',
-        'danger'
-      )
-    ) {
-      logoutClient(mx);
-    }
-  };
-
-  return (
-    <PopupWindow
-      isOpen={isOpen}
-      className="settings-window"
-      title={
-        <Text variant="s1" weight="medium" primary>
-          Settings
-        </Text>
-      }
-      contentOptions={
-        <>
-          <Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
-            Logout
-          </Button>
-          <IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />
-        </>
-      }
-      onRequestClose={requestClose}
-    >
-      {isOpen && (
-        <div className="settings-window__content">
-          <ProfileEditor userId={mx.getUserId()} />
-          <Tabs
-            items={tabItems}
-            defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
-            onSelect={handleTabChange}
-          />
-          <div className="settings-window__cards-wrapper">{selectedTab.render()}</div>
-        </div>
-      )}
-    </PopupWindow>
-  );
-}
-
-export default Settings;
diff --git a/src/app/organisms/settings/Settings.scss b/src/app/organisms/settings/Settings.scss
deleted file mode 100644 (file)
index a9ddd47..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-@use '../../partials/flex';
-@use '../../partials/dir';
-@use '../../partials/screen';
-
-.settings-window {
-  & .pw {
-    background-color: var(--bg-surface-low);
-  }
-  
-  .header .btn-danger {
-    margin: 0 var(--sp-tight);
-    box-shadow: none;
-  }
-
-  & .profile-editor {
-    padding: var(--sp-loose) var(--sp-extra-loose);
-  } 
-
-  & .tabs__content {
-    padding: 0 var(--sp-normal);
-  }
-
-  &__cards-wrapper {
-    padding: 0 var(--sp-normal);
-    @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
-  }
-}
-.settings-window__card {
-  margin: var(--sp-normal) 0;
-  background-color: var(--bg-surface);
-  border-radius: var(--bo-radius);
-  box-shadow: var(--bs-surface-border);
-  overflow: hidden;
-
-  & > .context-menu__header:first-child {
-    margin-top: 2px;
-  }
-}
-.settings-appearance__card,
-.settings-notifications,
-.global-notification,
-.keyword-notification,
-.ignore-user-list,
-.settings-security__card,
-.settings-security .device-manage,
-.settings-about__card,
-.settings-emoji__card {
-  @extend .settings-window__card;
-}
-
-.settings-window__cards-wrapper{
-  & .setting-tile {
-    margin: 0 var(--sp-normal);
-    margin-top: var(--sp-normal);
-    padding-bottom: 16px;
-    border-bottom: 1px solid var(--bg-surface-border);
-    &:last-child {
-      border-bottom: none;
-    }
-  }
-}
-
-.settings-notifications {
-  &__not-supported {
-    padding: 0 var(--sp-ultra-tight);
-  }
-}
-
-.settings-about {
-  &__branding {
-    padding: var(--sp-normal);
-    display: flex;
-    
-    & > div {
-      margin: 0 var(--sp-loose);
-    }
-    
-  }
-  &__btns {
-    & button {
-      margin-top: var(--sp-tight);
-      @include dir.side(margin, 0, var(--sp-tight));
-    }
-  }
-  
-  &__credits {
-    padding: 0 var(--sp-normal);
-    & ul {
-      color: var(--tc-surface-low);
-      padding: var(--sp-normal);
-      margin: var(--sp-extra-tight) 0;
-    }
-  }
-}
index 88fa9932599bb86be08d789c067b90b2c963b976..657422e402e6bb02b9f99c4e7db1f78a15d1ba68 100644 (file)
@@ -55,6 +55,9 @@ import { ScreenSize } from '../hooks/useScreenSize';
 import { MobileFriendlyPageNav, MobileFriendlyClientNav } from './MobileFriendly';
 import { ClientInitStorageAtom } from './client/ClientInitStorageAtom';
 import { ClientNonUIFeatures } from './client/ClientNonUIFeatures';
+import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager';
+import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification';
+import { AutoRestoreBackupOnVerification } from '../components/BackupRestore';
 
 export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
   const { hashRouter } = clientConfig;
@@ -79,7 +82,12 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
 
           return null;
         }}
-        element={<AuthLayout />}
+        element={
+          <>
+            <AuthLayout />
+            <UnAuthRouteThemeManager />
+          </>
+        }
       >
         <Route path={LOGIN_PATH} element={<Login />} />
         <Route path={REGISTER_PATH} element={<Register />} />
@@ -99,23 +107,28 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
           return null;
         }}
         element={
-          <ClientRoot>
-            <ClientInitStorageAtom>
-              <ClientBindAtoms>
-                <ClientNonUIFeatures>
-                  <ClientLayout
-                    nav={
-                      <MobileFriendlyClientNav>
-                        <SidebarNav />
-                      </MobileFriendlyClientNav>
-                    }
-                  >
-                    <Outlet />
-                  </ClientLayout>
-                </ClientNonUIFeatures>
-              </ClientBindAtoms>
-            </ClientInitStorageAtom>
-          </ClientRoot>
+          <>
+            <ClientRoot>
+              <ClientInitStorageAtom>
+                <ClientBindAtoms>
+                  <ClientNonUIFeatures>
+                    <ClientLayout
+                      nav={
+                        <MobileFriendlyClientNav>
+                          <SidebarNav />
+                        </MobileFriendlyClientNav>
+                      }
+                    >
+                      <Outlet />
+                    </ClientLayout>
+                    <ReceiveSelfDeviceVerification />
+                    <AutoRestoreBackupOnVerification />
+                  </ClientNonUIFeatures>
+                </ClientBindAtoms>
+              </ClientInitStorageAtom>
+            </ClientRoot>
+            <AuthRouteThemeManager />
+          </>
         }
       >
         <Route
diff --git a/src/app/pages/ThemeManager.tsx b/src/app/pages/ThemeManager.tsx
new file mode 100644 (file)
index 0000000..cabd8c6
--- /dev/null
@@ -0,0 +1,58 @@
+import { useEffect } from 'react';
+import { configClass, varsClass } from 'folds';
+import { DarkTheme, LightTheme, ThemeKind, useSystemThemeKind, useThemes } from '../hooks/useTheme';
+import { useSetting } from '../state/hooks/settings';
+import { settingsAtom } from '../state/settings';
+
+export function UnAuthRouteThemeManager() {
+  const systemThemeKind = useSystemThemeKind();
+
+  useEffect(() => {
+    document.body.className = '';
+    document.body.classList.add(configClass, varsClass);
+    if (systemThemeKind === ThemeKind.Dark) {
+      document.body.classList.add(...DarkTheme.classNames);
+    }
+    if (systemThemeKind === ThemeKind.Light) {
+      document.body.classList.add(...LightTheme.classNames);
+    }
+  }, [systemThemeKind]);
+
+  return null;
+}
+
+export function AuthRouteThemeManager() {
+  const systemThemeKind = useSystemThemeKind();
+  const themes = useThemes();
+  const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme');
+  const [themeId] = useSetting(settingsAtom, 'themeId');
+  const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
+  const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
+
+  // apply normal theme if system theme is disabled
+  useEffect(() => {
+    if (!systemTheme) {
+      document.body.className = '';
+      document.body.classList.add(configClass, varsClass);
+      const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
+
+      document.body.classList.add(...selectedTheme.classNames);
+    }
+  }, [systemTheme, themes, themeId]);
+
+  // apply preferred system theme if system theme is enabled
+  useEffect(() => {
+    if (systemTheme) {
+      document.body.className = '';
+      document.body.classList.add(configClass, varsClass);
+      const selectedTheme =
+        systemThemeKind === ThemeKind.Dark
+          ? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
+          : themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
+
+      document.body.classList.add(...selectedTheme.classNames);
+    }
+  }, [systemTheme, systemThemeKind, themes, lightThemeId, darkThemeId]);
+
+  return null;
+}
index 087d384d5f3f5afdaa47e706545466358298db77..90c305d00195e4e27987726f2adb6795c35f9155 100644 (file)
@@ -33,7 +33,7 @@ import {
   login,
   useLoginComplete,
 } from './loginUtil';
-import { PasswordInput } from '../../../components/password-input/PasswordInput';
+import { PasswordInput } from '../../../components/password-input';
 import { FieldError } from '../FiledError';
 import { getResetPasswordPath } from '../../pathUtils';
 import { stopPropagation } from '../../../utils/keyboard';
index f4439dd6cd4244086cf68f196208d690ee82d823..9f17342295f9d16b30a9453a49b20ee1844f48aa 100644 (file)
@@ -20,7 +20,7 @@ import {
   UIAFlow,
   createClient,
 } from 'matrix-js-sdk';
-import { PasswordInput } from '../../../components/password-input/PasswordInput';
+import { PasswordInput } from '../../../components/password-input';
 import {
   getLoginTermUrl,
   getUIAFlowForStages,
index 7c71de021d0348cf664c8ebcf6b266ba80b704e3..392f45c076a1c756b5996fd6316308cea8543954 100644 (file)
@@ -19,7 +19,7 @@ import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
 import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
 import { useAuthServer } from '../../../hooks/useAuthServer';
 import { usePasswordEmail } from '../../../hooks/usePasswordEmail';
-import { PasswordInput } from '../../../components/password-input/PasswordInput';
+import { PasswordInput } from '../../../components/password-input';
 import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
 import { FieldError } from '../FiledError';
 import { UIAFlowOverlay } from '../../../components/UIAFlowOverlay';
index f30bf63a59bc1be5ccf2e13d135c9d4bd65dfa2c..846d8ff3f36beb5227e64842dc742ff431142a8a 100644 (file)
@@ -18,6 +18,7 @@ import FocusTrap from 'focus-trap-react';
 import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useState } from 'react';
 import {
   clearCacheAndReload,
+  clearLoginData,
   initClient,
   logoutClient,
   startClient,
@@ -48,7 +49,7 @@ function ClientRootLoading() {
   );
 }
 
-function ClientRootOptions({ mx }: { mx: MatrixClient }) {
+function ClientRootOptions({ mx }: { mx?: MatrixClient }) {
   const [menuAnchor, setMenuAnchor] = useState<RectCords>();
 
   const handleToggle: MouseEventHandler<HTMLButtonElement> = (evt) => {
@@ -90,13 +91,21 @@ function ClientRootOptions({ mx }: { mx: MatrixClient }) {
           >
             <Menu>
               <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
-                <MenuItem onClick={() => clearCacheAndReload(mx)} size="300" radii="300">
-                  <Text as="span" size="T300" truncate>
-                    Clear Cache and Reload
-                  </Text>
-                </MenuItem>
+                {mx && (
+                  <MenuItem onClick={() => clearCacheAndReload(mx)} size="300" radii="300">
+                    <Text as="span" size="T300" truncate>
+                      Clear Cache and Reload
+                    </Text>
+                  </MenuItem>
+                )}
                 <MenuItem
-                  onClick={() => logoutClient(mx)}
+                  onClick={() => {
+                    if (mx) {
+                      logoutClient(mx);
+                      return;
+                    }
+                    clearLoginData();
+                  }}
                   size="300"
                   radii="300"
                   variant="Critical"
@@ -172,7 +181,7 @@ export function ClientRoot({ children }: ClientRootProps) {
   return (
     <SpecVersions baseUrl={baseUrl!}>
       {mx && <SyncStatus mx={mx} />}
-      {loading && mx && <ClientRootOptions mx={mx} />}
+      {loading && <ClientRootOptions mx={mx} />}
       {(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && (
         <SplashScreen>
           <Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="400">
@@ -182,7 +191,7 @@ export function ClientRoot({ children }: ClientRootProps) {
                   <Text>{`Failed to load. ${loadState.error.message}`}</Text>
                 )}
                 {startState.status === AsyncStatus.Error && (
-                  <Text>{`Failed to load. ${startState.error.message}`}</Text>
+                  <Text>{`Failed to start. ${startState.error.message}`}</Text>
                 )}
                 <Button variant="Critical" onClick={mx ? () => startMatrix(mx) : loadMatrix}>
                   <Text as="span" size="B400">
index 110e46940a6d4a271f7d02223318864f6bec51cd..27759a2de33ae01956498742b59a960a6ae4d70c 100644 (file)
@@ -16,7 +16,7 @@ import {
   SpaceTabs,
   InboxTab,
   ExploreTab,
-  UserTab,
+  SettingsTab,
   UnverifiedTab,
 } from './sidebar';
 import { openCreateRoom, openSearch } from '../../../client/action/navigation';
@@ -76,7 +76,7 @@ export function SidebarNav() {
               <UnverifiedTab />
 
               <InboxTab />
-              <UserTab />
+              <SettingsTab />
             </SidebarStack>
           </>
         }
diff --git a/src/app/pages/client/sidebar/SettingsTab.tsx b/src/app/pages/client/sidebar/SettingsTab.tsx
new file mode 100644 (file)
index 0000000..83cd118
--- /dev/null
@@ -0,0 +1,49 @@
+import React, { useState } from 'react';
+import { Text } from 'folds';
+import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
+import { UserAvatar } from '../../../components/user-avatar';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
+import { nameInitials } from '../../../utils/common';
+import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { Settings } from '../../../features/settings';
+import { useUserProfile } from '../../../hooks/useUserProfile';
+import { Modal500 } from '../../../components/Modal500';
+
+export function SettingsTab() {
+  const mx = useMatrixClient();
+  const useAuthentication = useMediaAuthentication();
+  const userId = mx.getUserId()!;
+  const profile = useUserProfile(userId);
+
+  const [settings, setSettings] = useState(false);
+
+  const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
+  const avatarUrl = profile.avatarUrl
+    ? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
+    : undefined;
+
+  const openSettings = () => setSettings(true);
+  const closeSettings = () => setSettings(false);
+
+  return (
+    <SidebarItem active={settings}>
+      <SidebarItemTooltip tooltip={displayName}>
+        {(triggerRef) => (
+          <SidebarAvatar as="button" ref={triggerRef} onClick={openSettings}>
+            <UserAvatar
+              userId={userId}
+              src={avatarUrl}
+              renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
+            />
+          </SidebarAvatar>
+        )}
+      </SidebarItemTooltip>
+      {settings && (
+        <Modal500 requestClose={closeSettings}>
+          <Settings requestClose={closeSettings} />
+        </Modal500>
+      )}
+    </SidebarItem>
+  );
+}
index e8fe8c51b2881f21ac75f6a843761f8c48770d22..cce17ccb9270aecf7c65df7e2ae86a79579f3afb 100644 (file)
@@ -22,3 +22,9 @@ export const UnverifiedAvatar = style({
   color: color.Critical.OnContainer,
   borderColor: color.Critical.ContainerLine,
 });
+
+export const UnverifiedOtherAvatar = style({
+  backgroundColor: color.Warning.Container,
+  color: color.Warning.OnContainer,
+  borderColor: color.Warning.ContainerLine,
+});
index 919c4a708a25b254512b538aaf85ed917e6590bc..0a2f117b131b8dd7f67947b167ebf60af5a3ea3a 100644 (file)
@@ -1,49 +1,94 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { Badge, color, Icon, Icons, Text } from 'folds';
-import { openSettings } from '../../../../client/action/navigation';
-import { isCrossVerified } from '../../../../util/matrixUtil';
 import {
   SidebarAvatar,
   SidebarItem,
   SidebarItemBadge,
   SidebarItemTooltip,
 } from '../../../components/sidebar';
-import { useDeviceList } from '../../../hooks/useDeviceList';
-import { tabText } from '../../../organisms/settings/Settings';
+import { useDeviceIds, useDeviceList, useSplitCurrentDevice } from '../../../hooks/useDeviceList';
 import { useMatrixClient } from '../../../hooks/useMatrixClient';
 import * as css from './UnverifiedTab.css';
+import {
+  useDeviceVerificationStatus,
+  useUnverifiedDeviceCount,
+  VerificationStatus,
+} from '../../../hooks/useDeviceVerificationStatus';
+import { useCrossSigningActive } from '../../../hooks/useCrossSigning';
+import { Modal500 } from '../../../components/Modal500';
+import { Settings, SettingsPages } from '../../../features/settings';
 
-export function UnverifiedTab() {
+function UnverifiedIndicator() {
   const mx = useMatrixClient();
-  const deviceList = useDeviceList();
-  const unverified = deviceList?.filter(
-    (device) => isCrossVerified(mx, device.device_id) === false
+
+  const crypto = mx.getCrypto();
+  const [devices] = useDeviceList();
+
+  const [currentDevice, otherDevices] = useSplitCurrentDevice(devices);
+
+  const verificationStatus = useDeviceVerificationStatus(
+    crypto,
+    mx.getSafeUserId(),
+    currentDevice?.device_id
+  );
+  const unverified = verificationStatus === VerificationStatus.Unverified;
+
+  const otherDevicesId = useDeviceIds(otherDevices);
+  const unverifiedDeviceCount = useUnverifiedDeviceCount(
+    crypto,
+    mx.getSafeUserId(),
+    otherDevicesId
   );
 
-  if (!unverified?.length) return null;
+  const [settings, setSettings] = useState(false);
+  const closeSettings = () => setSettings(false);
 
+  const hasUnverified =
+    unverified || (unverifiedDeviceCount !== undefined && unverifiedDeviceCount > 0);
   return (
-    <SidebarItem className={css.UnverifiedTab}>
-      <SidebarItemTooltip tooltip="Unverified Sessions">
-        {(triggerRef) => (
-          <SidebarAvatar
-            className={css.UnverifiedAvatar}
-            as="button"
-            ref={triggerRef}
-            outlined
-            onClick={() => openSettings(tabText.SECURITY)}
-          >
-            <Icon style={{ color: color.Critical.Main }} src={Icons.ShieldUser} />
-          </SidebarAvatar>
-        )}
-      </SidebarItemTooltip>
-      <SidebarItemBadge hasCount>
-        <Badge variant="Critical" size="400" fill="Solid" radii="Pill" outlined={false}>
-          <Text as="span" size="L400">
-            {unverified.length}
-          </Text>
-        </Badge>
-      </SidebarItemBadge>
-    </SidebarItem>
+    <>
+      {hasUnverified && (
+        <SidebarItem active={settings} className={css.UnverifiedTab}>
+          <SidebarItemTooltip tooltip={unverified ? 'Unverified Device' : 'Unverified Devices'}>
+            {(triggerRef) => (
+              <SidebarAvatar
+                className={unverified ? css.UnverifiedAvatar : css.UnverifiedOtherAvatar}
+                as="button"
+                ref={triggerRef}
+                outlined
+                onClick={() => setSettings(true)}
+              >
+                <Icon
+                  style={{ color: unverified ? color.Critical.Main : color.Warning.Main }}
+                  src={Icons.ShieldUser}
+                />
+              </SidebarAvatar>
+            )}
+          </SidebarItemTooltip>
+          {!unverified && unverifiedDeviceCount && unverifiedDeviceCount > 0 && (
+            <SidebarItemBadge hasCount>
+              <Badge variant="Warning" size="400" fill="Solid" radii="Pill" outlined={false}>
+                <Text as="span" size="L400">
+                  {unverifiedDeviceCount}
+                </Text>
+              </Badge>
+            </SidebarItemBadge>
+          )}
+        </SidebarItem>
+      )}
+      {settings && (
+        <Modal500 requestClose={closeSettings}>
+          <Settings initialPage={SettingsPages.DevicesPage} requestClose={closeSettings} />
+        </Modal500>
+      )}
+    </>
   );
 }
+
+export function UnverifiedTab() {
+  const crossSigningActive = useCrossSigningActive();
+
+  if (!crossSigningActive) return null;
+
+  return <UnverifiedIndicator />;
+}
diff --git a/src/app/pages/client/sidebar/UserTab.tsx b/src/app/pages/client/sidebar/UserTab.tsx
deleted file mode 100644 (file)
index 5e6c82a..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Text } from 'folds';
-import { UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
-import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
-import { openSettings } from '../../../../client/action/navigation';
-import { UserAvatar } from '../../../components/user-avatar';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
-import { nameInitials } from '../../../utils/common';
-import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
-
-type UserProfile = {
-  avatar_url?: string;
-  displayname?: string;
-};
-export function UserTab() {
-  const mx = useMatrixClient();
-  const useAuthentication = useMediaAuthentication();
-  const userId = mx.getUserId()!;
-
-  const [profile, setProfile] = useState<UserProfile>({});
-  const displayName = profile.displayname ?? getMxIdLocalPart(userId) ?? userId;
-  const avatarUrl = profile.avatar_url
-    ? mxcUrlToHttp(mx, profile.avatar_url, useAuthentication, 96, 96, 'crop') ?? undefined
-    : undefined;
-
-  useEffect(() => {
-    const user = mx.getUser(userId);
-    const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (event, myUser) => {
-      setProfile((cp) => ({
-        ...cp,
-        avatar_url: myUser.avatarUrl,
-      }));
-    };
-    const onDisplayNameChange: UserEventHandlerMap[UserEvent.DisplayName] = (event, myUser) => {
-      setProfile((cp) => ({
-        ...cp,
-        avatar_url: myUser.displayName,
-      }));
-    };
-    mx.getProfileInfo(userId).then((info) => setProfile(() => ({ ...info })));
-    user?.on(UserEvent.AvatarUrl, onAvatarChange);
-    user?.on(UserEvent.DisplayName, onDisplayNameChange);
-    return () => {
-      user?.removeListener(UserEvent.AvatarUrl, onAvatarChange);
-      user?.removeListener(UserEvent.DisplayName, onDisplayNameChange);
-    };
-  }, [mx, userId]);
-
-  return (
-    <SidebarItem>
-      <SidebarItemTooltip tooltip="User Settings">
-        {(triggerRef) => (
-          <SidebarAvatar as="button" ref={triggerRef} onClick={() => openSettings()}>
-            <UserAvatar
-              userId={userId}
-              src={avatarUrl}
-              renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
-            />
-          </SidebarAvatar>
-        )}
-      </SidebarItemTooltip>
-    </SidebarItem>
-  );
-}
index 5da8780b12efcc07e77deca93648afaaa363e09c..6460d1f27dc45113d0bdca106ea8c8b46934cb5b 100644 (file)
@@ -3,5 +3,5 @@ export * from './DirectTab';
 export * from './SpaceTabs';
 export * from './InboxTab';
 export * from './ExploreTab';
-export * from './UserTab';
+export * from './SettingsTab';
 export * from './UnverifiedTab';
index d3dc0be7f0eb2c510cd672850ae0fa37dfc4b24f..c8e2b78356a14df815d7db4f22a18dbb0d3cbf27 100644 (file)
@@ -23,7 +23,8 @@ import {
   toRem,
 } from 'folds';
 import { useVirtualizer } from '@tanstack/react-virtual';
-import { IJoinRuleEventContent, JoinRule, Room } from 'matrix-js-sdk';
+import { JoinRule, Room } from 'matrix-js-sdk';
+import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
 import FocusTrap from 'focus-trap-react';
 import { useMatrixClient } from '../../../hooks/useMatrixClient';
 import { mDirectAtom } from '../../../state/mDirectList';
@@ -201,7 +202,7 @@ function SpaceHeader() {
   const joinRules = useStateEvent(
     space,
     StateEvent.RoomJoinRules
-  )?.getContent<IJoinRuleEventContent>();
+  )?.getContent<RoomJoinRulesEventContent>();
 
   const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
     const cords = evt.currentTarget.getBoundingClientRect();
index 57750383a515fabd0e297bf57123b413d5b6f9b7..54da892200031de035759425e2189d15b351f0d3 100644 (file)
@@ -83,8 +83,6 @@ export type InboxNotificationsPathSearchParams = {
 export const INBOX_NOTIFICATIONS_PATH = `/inbox/${_NOTIFICATIONS_PATH}`;
 export const INBOX_INVITES_PATH = `/inbox/${_INVITES_PATH}`;
 
-export const USER_SETTINGS_PATH = '/user-settings/';
-
 export const SPACE_SETTINGS_PATH = '/space-settings/';
 
 export const ROOM_SETTINGS_PATH = '/room-settings/';
diff --git a/src/app/plugins/custom-emoji.ts b/src/app/plugins/custom-emoji.ts
deleted file mode 100644 (file)
index ffb3794..0000000
+++ /dev/null
@@ -1,303 +0,0 @@
-import { IImageInfo, MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
-import { AccountDataEvent } from '../../types/matrix/accountData';
-import { getAccountData, getStateEvents } from '../utils/room';
-import { StateEvent } from '../../types/matrix/room';
-
-// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
-
-export type PackEventIdToUnknown = Record<string, unknown>;
-export type EmoteRoomIdToPackEvents = Record<string, PackEventIdToUnknown>;
-export type EmoteRoomsContent = {
-  rooms?: EmoteRoomIdToPackEvents;
-};
-
-export enum PackUsage {
-  Emoticon = 'emoticon',
-  Sticker = 'sticker',
-}
-
-export type PackImage = {
-  url: string;
-  body?: string;
-  usage?: PackUsage[];
-  info?: IImageInfo;
-};
-
-export type PackImages = Record<string, PackImage>;
-
-export type PackMeta = {
-  display_name?: string;
-  avatar_url?: string;
-  attribution?: string;
-  usage?: PackUsage[];
-};
-
-export type ExtendedPackImage = PackImage & {
-  shortcode: string;
-};
-
-export type PackContent = {
-  pack?: PackMeta;
-  images?: PackImages;
-};
-
-export class ImagePack {
-  public id: string;
-
-  public content: PackContent;
-
-  public displayName?: string;
-
-  public avatarUrl?: string;
-
-  public usage?: PackUsage[];
-
-  public attribution?: string;
-
-  public images: Map<string, ExtendedPackImage>;
-
-  public emoticons: ExtendedPackImage[];
-
-  public stickers: ExtendedPackImage[];
-
-  static parsePack(eventId: string, packContent: PackContent) {
-    if (!eventId || typeof packContent?.images !== 'object') {
-      return undefined;
-    }
-
-    return new ImagePack(eventId, packContent);
-  }
-
-  constructor(eventId: string, content: PackContent) {
-    this.id = eventId;
-    this.content = JSON.parse(JSON.stringify(content));
-
-    this.images = new Map();
-    this.emoticons = [];
-    this.stickers = [];
-
-    this.applyPackMeta(content);
-    this.applyImages(content);
-  }
-
-  applyPackMeta(content: PackContent) {
-    const pack = content.pack ?? {};
-
-    this.displayName = pack.display_name;
-    this.avatarUrl = pack.avatar_url;
-    this.usage = pack.usage ?? [PackUsage.Emoticon, PackUsage.Sticker];
-    this.attribution = pack.attribution;
-  }
-
-  applyImages(content: PackContent) {
-    this.images = new Map();
-    this.emoticons = [];
-    this.stickers = [];
-    if (!content.images) return;
-
-    Object.entries(content.images).forEach(([shortcode, data]) => {
-      const { url } = data;
-      const body = data.body ?? shortcode;
-      const usage = data.usage ?? this.usage;
-      const { info } = data;
-
-      if (!url) return;
-      const image: ExtendedPackImage = {
-        shortcode,
-        url,
-        body,
-        usage,
-        info,
-      };
-
-      this.images.set(shortcode, image);
-      if (usage && usage.includes(PackUsage.Emoticon)) {
-        this.emoticons.push(image);
-      }
-      if (usage && usage.includes(PackUsage.Sticker)) {
-        this.stickers.push(image);
-      }
-    });
-  }
-
-  getImages() {
-    return this.images;
-  }
-
-  getEmojis() {
-    return this.emoticons;
-  }
-
-  getStickers() {
-    return this.stickers;
-  }
-
-  getImagesFor(usage: PackUsage) {
-    if (usage === PackUsage.Emoticon) return this.getEmojis();
-    if (usage === PackUsage.Sticker) return this.getStickers();
-    return this.getEmojis();
-  }
-
-  getContent() {
-    return this.content;
-  }
-
-  getPackAvatarUrl(usage: PackUsage): string | undefined {
-    return this.avatarUrl || this.getImagesFor(usage)[0].url;
-  }
-
-  private updatePackProperty<K extends keyof PackMeta>(property: K, value: PackMeta[K]) {
-    if (this.content.pack === undefined) {
-      this.content.pack = {};
-    }
-    this.content.pack[property] = value;
-    this.applyPackMeta(this.content);
-  }
-
-  setAvatarUrl(avatarUrl?: string) {
-    this.updatePackProperty('avatar_url', avatarUrl);
-  }
-
-  setDisplayName(displayName?: string) {
-    this.updatePackProperty('display_name', displayName);
-  }
-
-  setAttribution(attribution?: string) {
-    this.updatePackProperty('attribution', attribution);
-  }
-
-  setUsage(usage?: PackUsage[]) {
-    this.updatePackProperty('usage', usage);
-  }
-
-  addImage(key: string, imgContent: PackImage) {
-    this.content.images = {
-      [key]: imgContent,
-      ...this.content.images,
-    };
-    this.applyImages(this.content);
-  }
-
-  removeImage(key: string) {
-    if (!this.content.images) return;
-    if (this.content.images[key] === undefined) return;
-    delete this.content.images[key];
-    this.applyImages(this.content);
-  }
-
-  updateImageKey(key: string, newKey: string) {
-    const { images } = this.content;
-    if (!images) return;
-    if (images[key] === undefined) return;
-    const copyImages: PackImages = {};
-    Object.keys(images).forEach((imgKey) => {
-      copyImages[imgKey === key ? newKey : imgKey] = images[imgKey];
-    });
-    this.content.images = copyImages;
-    this.applyImages(this.content);
-  }
-
-  private updateImageProperty<K extends keyof PackImage>(
-    key: string,
-    property: K,
-    value: PackImage[K]
-  ) {
-    if (!this.content.images) return;
-    if (this.content.images[key] === undefined) return;
-    this.content.images[key][property] = value;
-    this.applyImages(this.content);
-  }
-
-  setImageUrl(key: string, url: string) {
-    this.updateImageProperty(key, 'url', url);
-  }
-
-  setImageBody(key: string, body?: string) {
-    this.updateImageProperty(key, 'body', body);
-  }
-
-  setImageInfo(key: string, info?: IImageInfo) {
-    this.updateImageProperty(key, 'info', info);
-  }
-
-  setImageUsage(key: string, usage?: PackUsage[]) {
-    this.updateImageProperty(key, 'usage', usage);
-  }
-}
-
-export function packEventsToImagePacks(packEvents: MatrixEvent[]): ImagePack[] {
-  return packEvents.reduce<ImagePack[]>((imagePacks, packEvent) => {
-    const packId = packEvent?.getId();
-    const content = packEvent?.getContent() as PackContent | undefined;
-    if (!packId || !content) return imagePacks;
-    const pack = ImagePack.parsePack(packId, content);
-    if (pack) {
-      imagePacks.push(pack);
-    }
-    return imagePacks;
-  }, []);
-}
-
-export function getRoomImagePacks(room: Room): ImagePack[] {
-  const dataEvents = getStateEvents(room, StateEvent.PoniesRoomEmotes);
-  return packEventsToImagePacks(dataEvents);
-}
-
-export function getGlobalImagePacks(mx: MatrixClient): ImagePack[] {
-  const emoteRoomsContent = getAccountData(mx, AccountDataEvent.PoniesEmoteRooms)?.getContent() as
-    | EmoteRoomsContent
-    | undefined;
-  if (typeof emoteRoomsContent !== 'object') return [];
-
-  const { rooms } = emoteRoomsContent;
-  if (typeof rooms !== 'object') return [];
-
-  const roomIds = Object.keys(rooms);
-
-  const packs = roomIds.flatMap((roomId) => {
-    if (typeof rooms[roomId] !== 'object') return [];
-    const room = mx.getRoom(roomId);
-    if (!room) return [];
-    const packEventIdToUnknown = rooms[roomId];
-    const roomPacks = getStateEvents(room, StateEvent.PoniesRoomEmotes);
-    const globalPacks = roomPacks.filter((mE) => {
-      const packKey = mE.getStateKey();
-      if (typeof packKey === 'string') return !!packEventIdToUnknown[packKey];
-      return false;
-    });
-    return packEventsToImagePacks(globalPacks);
-  });
-
-  return packs;
-}
-
-export function getUserImagePack(mx: MatrixClient): ImagePack | undefined {
-  const userPackContent = getAccountData(mx, AccountDataEvent.PoniesUserEmotes)?.getContent() as
-    | PackContent
-    | undefined;
-  const userId = mx.getUserId();
-  if (!userPackContent || !userId) {
-    return undefined;
-  }
-
-  const userImagePack = ImagePack.parsePack(userId, userPackContent);
-  return userImagePack;
-}
-
-/**
- * @param {MatrixClient} mx Provide if you want to include user personal/global pack
- * @param {Room[]} rooms Provide rooms if you want to include rooms pack
- * @returns {ImagePack[]} packs
- */
-export function getRelevantPacks(mx?: MatrixClient, rooms?: Room[]): ImagePack[] {
-  const userPack = mx && getUserImagePack(mx);
-  const userPacks = userPack ? [userPack] : [];
-  const globalPacks = mx ? getGlobalImagePacks(mx) : [];
-  const globalPackIds = new Set(globalPacks.map((pack) => pack.id));
-  const roomsPack = rooms?.flatMap(getRoomImagePacks) ?? [];
-
-  return userPacks.concat(
-    globalPacks,
-    roomsPack.filter((pack) => !globalPackIds.has(pack.id))
-  );
-}
diff --git a/src/app/plugins/custom-emoji/ImagePack.ts b/src/app/plugins/custom-emoji/ImagePack.ts
new file mode 100644 (file)
index 0000000..1685362
--- /dev/null
@@ -0,0 +1,78 @@
+import { MatrixEvent } from 'matrix-js-sdk';
+import { PackAddress } from './PackAddress';
+import { PackImageReader } from './PackImageReader';
+import { PackImagesReader } from './PackImagesReader';
+import { PackMetaReader } from './PackMetaReader';
+import { ImageUsage, PackContent } from './types';
+
+export class ImagePack {
+  public readonly id: string;
+
+  public readonly deleted: boolean;
+
+  public readonly address: PackAddress | undefined;
+
+  public readonly meta: PackMetaReader;
+
+  public readonly images: PackImagesReader;
+
+  private emoticonMemo: PackImageReader[] | undefined;
+
+  private stickerMemo: PackImageReader[] | undefined;
+
+  constructor(id: string, content: PackContent, address: PackAddress | undefined) {
+    this.id = id;
+
+    this.address = address;
+
+    this.deleted = content.pack === undefined && content.images === undefined;
+
+    this.meta = new PackMetaReader(content.pack ?? {});
+    this.images = new PackImagesReader(content.images ?? {});
+  }
+
+  static fromMatrixEvent(id: string, matrixEvent: MatrixEvent) {
+    const roomId = matrixEvent.getRoomId();
+    const stateKey = matrixEvent.getStateKey();
+
+    const address =
+      roomId && typeof stateKey === 'string' ? new PackAddress(roomId, stateKey) : undefined;
+
+    const content = matrixEvent.getContent<PackContent>();
+
+    const imagePack: ImagePack = new ImagePack(id, content, address);
+
+    return imagePack;
+  }
+
+  public getImages(usage: ImageUsage): PackImageReader[] {
+    if (usage === ImageUsage.Emoticon && this.emoticonMemo) {
+      return this.emoticonMemo;
+    }
+    if (usage === ImageUsage.Sticker && this.stickerMemo) {
+      return this.stickerMemo;
+    }
+
+    const images = Array.from(this.images.collection.values()).filter((image) => {
+      const usg = image.usage ?? this.meta.usage;
+      return usg.includes(usage);
+    });
+
+    if (usage === ImageUsage.Emoticon) {
+      this.emoticonMemo = images;
+    }
+    if (usage === ImageUsage.Sticker) {
+      this.stickerMemo = images;
+    }
+
+    return images;
+  }
+
+  public getAvatarUrl(usage: ImageUsage): string | undefined {
+    if (this.meta.avatar) return this.meta.avatar;
+    const images = this.getImages(usage);
+    const firstImage = images[0];
+    if (firstImage) return firstImage.url;
+    return undefined;
+  }
+}
diff --git a/src/app/plugins/custom-emoji/PackAddress.ts b/src/app/plugins/custom-emoji/PackAddress.ts
new file mode 100644 (file)
index 0000000..53595c2
--- /dev/null
@@ -0,0 +1,10 @@
+export class PackAddress {
+  public readonly roomId: string;
+
+  public readonly stateKey: string;
+
+  constructor(roomId: string, stateKey: string) {
+    this.roomId = roomId;
+    this.stateKey = stateKey;
+  }
+}
diff --git a/src/app/plugins/custom-emoji/PackImageReader.ts b/src/app/plugins/custom-emoji/PackImageReader.ts
new file mode 100644 (file)
index 0000000..4bac3aa
--- /dev/null
@@ -0,0 +1,52 @@
+import { IImageInfo } from '../../../types/matrix/common';
+import { ImageUsage, PackImage } from './types';
+
+export class PackImageReader {
+  public readonly shortcode: string;
+
+  public readonly url: string;
+
+  private readonly image: Omit<PackImage, 'url'>;
+
+  constructor(shortcode: string, url: string, image: Omit<PackImage, 'url'>) {
+    this.shortcode = shortcode;
+    this.url = url;
+
+    this.image = image;
+  }
+
+  static fromPackImage(shortcode: string, image: PackImage): PackImageReader | undefined {
+    const { url } = image;
+
+    if (typeof url !== 'string') return undefined;
+
+    return new PackImageReader(shortcode, url, image);
+  }
+
+  get body(): string | undefined {
+    const { body } = this.image;
+    return typeof body === 'string' ? body : undefined;
+  }
+
+  get info(): IImageInfo | undefined {
+    return this.image.info;
+  }
+
+  get usage(): ImageUsage[] | undefined {
+    const usg = this.image.usage;
+    if (!Array.isArray(usg)) return undefined;
+
+    const knownUsage = usg.filter((u) => u === ImageUsage.Emoticon || u === ImageUsage.Sticker);
+
+    return knownUsage.length > 0 ? knownUsage : undefined;
+  }
+
+  get content(): PackImage {
+    return {
+      url: this.url,
+      body: this.image.body,
+      usage: this.image.usage,
+      info: this.image.info,
+    };
+  }
+}
diff --git a/src/app/plugins/custom-emoji/PackImagesReader.ts b/src/app/plugins/custom-emoji/PackImagesReader.ts
new file mode 100644 (file)
index 0000000..1531218
--- /dev/null
@@ -0,0 +1,28 @@
+import { PackImageReader } from './PackImageReader';
+import { PackImages } from './types';
+
+export class PackImagesReader {
+  private readonly rawImages: PackImages;
+
+  private shortcodeToImages: Map<string, PackImageReader> | undefined;
+
+  constructor(images: PackImages) {
+    this.rawImages = images;
+  }
+
+  get collection(): Map<string, PackImageReader> {
+    if (this.shortcodeToImages) return this.shortcodeToImages;
+
+    const shortcodeToImages: Map<string, PackImageReader> = new Map();
+
+    Object.entries(this.rawImages).forEach(([shortcode, image]) => {
+      const imageReader = PackImageReader.fromPackImage(shortcode, image);
+      if (imageReader) {
+        shortcodeToImages.set(shortcode, imageReader);
+      }
+    });
+
+    this.shortcodeToImages = shortcodeToImages;
+    return this.shortcodeToImages;
+  }
+}
diff --git a/src/app/plugins/custom-emoji/PackMetaReader.ts b/src/app/plugins/custom-emoji/PackMetaReader.ts
new file mode 100644 (file)
index 0000000..537addf
--- /dev/null
@@ -0,0 +1,45 @@
+import { PackMeta, ImageUsage } from './types';
+
+export class PackMetaReader {
+  private readonly meta: PackMeta;
+
+  public readonly fallbackUsage: ImageUsage[] = [ImageUsage.Emoticon, ImageUsage.Sticker];
+
+  constructor(meta: PackMeta) {
+    this.meta = meta;
+  }
+
+  get name(): string | undefined {
+    const displayName = this.meta.display_name;
+    if (typeof displayName === 'string') return displayName;
+    return undefined;
+  }
+
+  get avatar(): string | undefined {
+    const avatarURL = this.meta.avatar_url;
+    if (typeof avatarURL === 'string') return avatarURL;
+    return undefined;
+  }
+
+  get attribution(): string | undefined {
+    const { attribution } = this.meta;
+    if (typeof this.meta.attribution === 'string') return attribution;
+    return undefined;
+  }
+
+  get usage(): ImageUsage[] {
+    if (!Array.isArray(this.meta.usage)) return this.fallbackUsage;
+
+    const knownUsage = this.meta.usage.filter(
+      (u) => u === ImageUsage.Emoticon || u === ImageUsage.Sticker
+    );
+
+    if (knownUsage.length === 0) return this.fallbackUsage;
+
+    return knownUsage;
+  }
+
+  get content(): PackMeta {
+    return this.meta;
+  }
+}
diff --git a/src/app/plugins/custom-emoji/index.ts b/src/app/plugins/custom-emoji/index.ts
new file mode 100644 (file)
index 0000000..f833fa5
--- /dev/null
@@ -0,0 +1,7 @@
+export * from './PackAddress';
+export * from './PackMetaReader';
+export * from './PackImageReader';
+export * from './PackImagesReader';
+export * from './ImagePack';
+export * from './types';
+export * from './utils';
diff --git a/src/app/plugins/custom-emoji/types.ts b/src/app/plugins/custom-emoji/types.ts
new file mode 100644 (file)
index 0000000..4596707
--- /dev/null
@@ -0,0 +1,41 @@
+import { IImageInfo } from '../../../types/matrix/common';
+
+// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
+
+/**
+ * im.ponies.emote_rooms content
+ */
+export type PackStateKeyToObject = Record<string, object>;
+export type RoomIdToStateKey = Record<string, PackStateKeyToObject>;
+export type EmoteRoomsContent = {
+  rooms?: RoomIdToStateKey;
+};
+
+/**
+ * Pack
+ */
+export enum ImageUsage {
+  Emoticon = 'emoticon',
+  Sticker = 'sticker',
+}
+
+export type PackImage = {
+  url: string;
+  body?: string;
+  usage?: ImageUsage[];
+  info?: IImageInfo;
+};
+
+export type PackImages = Record<string, PackImage>;
+
+export type PackMeta = {
+  display_name?: string;
+  avatar_url?: string;
+  attribution?: string;
+  usage?: ImageUsage[];
+};
+
+export type PackContent = {
+  pack?: PackMeta;
+  images?: PackImages;
+};
diff --git a/src/app/plugins/custom-emoji/utils.ts b/src/app/plugins/custom-emoji/utils.ts
new file mode 100644 (file)
index 0000000..cb80bb1
--- /dev/null
@@ -0,0 +1,88 @@
+import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
+import { ImagePack } from './ImagePack';
+import { EmoteRoomsContent, ImageUsage } from './types';
+import { StateEvent } from '../../../types/matrix/room';
+import { getAccountData, getStateEvent, getStateEvents } from '../../utils/room';
+import { AccountDataEvent } from '../../../types/matrix/accountData';
+import { PackMetaReader } from './PackMetaReader';
+import { PackAddress } from './PackAddress';
+
+export function packAddressEqual(a1?: PackAddress, a2?: PackAddress): boolean {
+  if (!a1 && !a2) return true;
+  if (!a1 || !a2) return false;
+  return a1.roomId === a2.roomId && a1.stateKey === a2.stateKey;
+}
+
+export function imageUsageEqual(u1: ImageUsage[], u2: ImageUsage[]) {
+  return u1.length === u2.length && u1.every((u) => u2.includes(u));
+}
+
+export function packMetaEqual(a: PackMetaReader, b: PackMetaReader): boolean {
+  return (
+    a.name === b.name &&
+    a.avatar === b.avatar &&
+    a.attribution === b.attribution &&
+    imageUsageEqual(a.usage, b.usage)
+  );
+}
+
+export function makeImagePacks(packEvents: MatrixEvent[]): ImagePack[] {
+  return packEvents.reduce<ImagePack[]>((imagePacks, packEvent) => {
+    const packId = packEvent.getId();
+    if (!packId) return imagePacks;
+    imagePacks.push(ImagePack.fromMatrixEvent(packId, packEvent));
+    return imagePacks;
+  }, []);
+}
+
+export function getRoomImagePack(room: Room, stateKey: string): ImagePack | undefined {
+  const packEvent = getStateEvent(room, StateEvent.PoniesRoomEmotes, stateKey);
+  if (!packEvent) return undefined;
+  const packId = packEvent.getId();
+  if (!packId) return undefined;
+  return ImagePack.fromMatrixEvent(packId, packEvent);
+}
+
+export function getRoomImagePacks(room: Room): ImagePack[] {
+  const packEvents = getStateEvents(room, StateEvent.PoniesRoomEmotes);
+  return makeImagePacks(packEvents);
+}
+
+export function getGlobalImagePacks(mx: MatrixClient): ImagePack[] {
+  const emoteRoomsContent = getAccountData(mx, AccountDataEvent.PoniesEmoteRooms)?.getContent() as
+    | EmoteRoomsContent
+    | undefined;
+  if (typeof emoteRoomsContent !== 'object') return [];
+
+  const { rooms: roomIdToPackInfo } = emoteRoomsContent;
+  if (typeof roomIdToPackInfo !== 'object') return [];
+
+  const roomIds = Object.keys(roomIdToPackInfo);
+
+  const packs = roomIds.flatMap((roomId) => {
+    if (typeof roomIdToPackInfo[roomId] !== 'object') return [];
+    const room = mx.getRoom(roomId);
+    if (!room) return [];
+    const packStateKeyToUnknown = roomIdToPackInfo[roomId];
+    const packEvents = getStateEvents(room, StateEvent.PoniesRoomEmotes);
+    const globalPackEvents = packEvents.filter((mE) => {
+      const stateKey = mE.getStateKey();
+      if (typeof stateKey === 'string') return !!packStateKeyToUnknown[stateKey];
+      return false;
+    });
+    return makeImagePacks(globalPackEvents);
+  });
+
+  return packs;
+}
+
+export function getUserImagePack(mx: MatrixClient): ImagePack | undefined {
+  const packEvent = getAccountData(mx, AccountDataEvent.PoniesUserEmotes);
+  const userId = mx.getUserId();
+  if (!packEvent || !userId) {
+    return undefined;
+  }
+
+  const userImagePack = ImagePack.fromMatrixEvent(userId, packEvent);
+  return userImagePack;
+}
index 1aa79548308c8724c6458decd030ac56ee7e4c4f..f93c6ef10b6584b9abe19af29e50502c044be84e 100644 (file)
@@ -16,7 +16,6 @@ import 'prismjs/components/prism-java';
 import 'prismjs/components/prism-python';
 
 import './ReactPrism.css';
-// we apply theme in client/state/settings.js
 // using classNames .prism-dark .prism-light from ReactPrism.css
 
 export default function ReactPrism({
diff --git a/src/app/plugins/text-area/Cursor.ts b/src/app/plugins/text-area/Cursor.ts
new file mode 100644 (file)
index 0000000..8a2098e
--- /dev/null
@@ -0,0 +1,27 @@
+export type CursorDirection = 'forward' | 'backward' | 'none';
+
+export class Cursor {
+  public readonly start: number;
+
+  public readonly end: number;
+
+  public readonly direction: CursorDirection;
+
+  constructor(start: number, end: number, direction: CursorDirection) {
+    this.start = start;
+    this.end = end;
+    this.direction = direction;
+  }
+
+  static fromTextAreaElement(element: HTMLTextAreaElement) {
+    return new Cursor(element.selectionStart, element.selectionEnd, element.selectionDirection);
+  }
+
+  public get selection() {
+    return this.start !== this.end;
+  }
+
+  public get length() {
+    return this.end - this.start;
+  }
+}
diff --git a/src/app/plugins/text-area/Operations.ts b/src/app/plugins/text-area/Operations.ts
new file mode 100644 (file)
index 0000000..a4c813a
--- /dev/null
@@ -0,0 +1,7 @@
+import { Cursor } from './Cursor';
+
+export interface Operations {
+  select(cursor: Cursor): void;
+  deselect(cursor: Cursor): void;
+  insert(cursor: Cursor, text: string): Cursor;
+}
diff --git a/src/app/plugins/text-area/TextArea.ts b/src/app/plugins/text-area/TextArea.ts
new file mode 100644 (file)
index 0000000..2e8a085
--- /dev/null
@@ -0,0 +1,58 @@
+import { Cursor } from './Cursor';
+import { GetTarget } from './type';
+
+export class TextArea {
+  private readonly getTarget: GetTarget;
+
+  constructor(getTarget: GetTarget) {
+    this.getTarget = getTarget;
+  }
+
+  get target() {
+    return this.getTarget();
+  }
+
+  public selection(cursor: Cursor): string {
+    return this.target.value.substring(cursor.start, cursor.end);
+  }
+
+  public lineBeginIndex(cursor: Cursor): number {
+    const beforeValue = this.target.value.substring(0, cursor.start);
+    const lineEndIndex = beforeValue.lastIndexOf('\n');
+    return lineEndIndex + 1;
+  }
+
+  public lineEndIndex(cursor: Cursor): number {
+    const afterValue = this.target.value.substring(cursor.end);
+    const lineEndIndex = afterValue.indexOf('\n');
+    return cursor.end + (lineEndIndex === -1 ? afterValue.length : lineEndIndex);
+  }
+
+  public cursorLines(cursor: Cursor): Cursor {
+    const lineBeginIndex = this.lineBeginIndex(cursor);
+    const lineEndIndex = this.lineEndIndex(cursor);
+
+    const linesCursor = new Cursor(lineBeginIndex, lineEndIndex, 'none');
+    return linesCursor;
+  }
+
+  public prevLine(cursor: Cursor): Cursor | undefined {
+    const currentLineIndex = this.lineBeginIndex(cursor);
+    const prevIndex = currentLineIndex - 1;
+
+    if (prevIndex < 0) return undefined;
+
+    const lineCursor = this.cursorLines(new Cursor(prevIndex, prevIndex, 'none'));
+    return lineCursor;
+  }
+
+  public nextLine(cursor: Cursor): Cursor | undefined {
+    const currentLineIndex = this.lineEndIndex(cursor);
+    const nextIndex = currentLineIndex + 1;
+
+    if (nextIndex > this.target.value.length) return undefined;
+
+    const lineCursor = this.cursorLines(new Cursor(nextIndex, nextIndex, 'none'));
+    return lineCursor;
+  }
+}
diff --git a/src/app/plugins/text-area/TextAreaOperations.ts b/src/app/plugins/text-area/TextAreaOperations.ts
new file mode 100644 (file)
index 0000000..3bc1b4f
--- /dev/null
@@ -0,0 +1,34 @@
+import { Cursor } from './Cursor';
+import { Operations } from './Operations';
+import { GetTarget } from './type';
+
+export class TextAreaOperations implements Operations {
+  private readonly getTarget: GetTarget;
+
+  constructor(getTarget: GetTarget) {
+    this.getTarget = getTarget;
+  }
+
+  get target() {
+    return this.getTarget();
+  }
+
+  public select(cursor: Cursor) {
+    this.target.setSelectionRange(cursor.start, cursor.end, cursor.direction);
+  }
+
+  public deselect(cursor: Cursor) {
+    if (cursor.direction === 'backward') {
+      this.target.setSelectionRange(cursor.start, cursor.start, 'none');
+      return;
+    }
+    this.target.setSelectionRange(cursor.end, cursor.end, 'none');
+  }
+
+  public insert(cursor: Cursor, text: string): Cursor {
+    const { value } = this.target;
+    this.target.value = `${value.substring(0, cursor.start)}${text}${value.substring(cursor.end)}`;
+
+    return new Cursor(cursor.start, cursor.start + text.length, cursor.direction);
+  }
+}
diff --git a/src/app/plugins/text-area/TextUtils.ts b/src/app/plugins/text-area/TextUtils.ts
new file mode 100644 (file)
index 0000000..43d0583
--- /dev/null
@@ -0,0 +1,5 @@
+export class TextUtils {
+  static multiline(str: string) {
+    return str.indexOf('\n') !== -1;
+  }
+}
diff --git a/src/app/plugins/text-area/index.ts b/src/app/plugins/text-area/index.ts
new file mode 100644 (file)
index 0000000..b99ebf5
--- /dev/null
@@ -0,0 +1,6 @@
+export * from './Cursor';
+export * from './mods';
+export * from './Operations';
+export * from './TextArea';
+export * from './TextAreaOperations';
+export * from './TextUtils';
diff --git a/src/app/plugins/text-area/mods/Intent.ts b/src/app/plugins/text-area/mods/Intent.ts
new file mode 100644 (file)
index 0000000..35212b9
--- /dev/null
@@ -0,0 +1,102 @@
+import { Cursor } from '../Cursor';
+import { Operations } from '../Operations';
+import { TextArea } from '../TextArea';
+
+export class Intent {
+  public readonly textArea: TextArea;
+
+  public readonly operations: Operations;
+
+  public readonly size: number;
+
+  public readonly str: string;
+
+  private intentReg: RegExp;
+
+  constructor(size: number, textArea: TextArea, operations: Operations) {
+    this.textArea = textArea;
+    this.operations = operations;
+    this.size = size;
+    this.intentReg = /^\s*/;
+
+    this.str = '';
+    for (let i = 0; i < size; i += 1) this.str += ' ';
+  }
+
+  private lineIntent(cursor: Cursor): string {
+    const lines = this.textArea.cursorLines(cursor);
+    const selection = this.textArea.selection(lines);
+    const match = selection.match(this.intentReg);
+    if (!match) return '';
+    return match[0];
+  }
+
+  public moveForward(cursor: Cursor): Cursor {
+    const linesCursor = this.textArea.cursorLines(cursor);
+
+    const selection = this.textArea.selection(linesCursor);
+    const lines = selection.split('\n');
+
+    const intentLines = lines.map((line) => `${this.str}${line}`);
+    this.operations.insert(linesCursor, intentLines.join('\n'));
+
+    const addedIntentLength = lines.length * this.str.length;
+    return new Cursor(
+      cursor.start === linesCursor.start ? cursor.start : cursor.start + this.str.length,
+      cursor.end + addedIntentLength,
+      cursor.direction
+    );
+  }
+
+  public moveBackward(cursor: Cursor): Cursor {
+    const linesCursor = this.textArea.cursorLines(cursor);
+
+    const selection = this.textArea.selection(linesCursor);
+    const lines = selection.split('\n');
+
+    const intentLines = lines.map((line) => {
+      if (line.startsWith(this.str)) return line.substring(this.str.length);
+      return line.replace(this.intentReg, '');
+    });
+    const intentCursor = this.operations.insert(linesCursor, intentLines.join('\n'));
+
+    const firstLineTrimLength = lines[0].length - intentLines[0].length;
+    const lastLine = this.textArea.cursorLines(
+      new Cursor(intentCursor.end, intentCursor.end, 'none')
+    );
+
+    const start = Math.max(cursor.start - firstLineTrimLength, linesCursor.start);
+    const trimmedContentLength = linesCursor.length - intentCursor.length;
+    const end = Math.max(lastLine.start, cursor.end - trimmedContentLength);
+    return new Cursor(start, end, cursor.direction);
+  }
+
+  public addNewLine(cursor: Cursor): Cursor {
+    const lineIntent = this.lineIntent(cursor);
+    const line = `\n${lineIntent}`;
+
+    const insertCursor = this.operations.insert(cursor, line);
+    return new Cursor(insertCursor.end, insertCursor.end, 'none');
+  }
+
+  public addNextLine(cursor: Cursor): Cursor {
+    const lineIntent = this.lineIntent(cursor);
+    const line = `\n${lineIntent}`;
+
+    const currentLine = this.textArea.cursorLines(cursor);
+    const lineCursor = new Cursor(currentLine.end, currentLine.end, 'none');
+    const insertCursor = this.operations.insert(lineCursor, line);
+    return new Cursor(insertCursor.end, insertCursor.end, 'none');
+  }
+
+  public addPreviousLine(cursor: Cursor): Cursor {
+    const lineIntent = this.lineIntent(cursor);
+    const line = `\n${lineIntent}`;
+
+    const prevLine = this.textArea.prevLine(cursor);
+    const insertIndex = prevLine?.end ?? 0;
+    const lineCursor = new Cursor(insertIndex, insertIndex, 'none');
+    const insertCursor = this.operations.insert(lineCursor, line);
+    return new Cursor(insertCursor.end, insertCursor.end, 'none');
+  }
+}
diff --git a/src/app/plugins/text-area/mods/index.ts b/src/app/plugins/text-area/mods/index.ts
new file mode 100644 (file)
index 0000000..7e1f4aa
--- /dev/null
@@ -0,0 +1 @@
+export * from './Intent';
diff --git a/src/app/plugins/text-area/type.ts b/src/app/plugins/text-area/type.ts
new file mode 100644 (file)
index 0000000..1ce38ae
--- /dev/null
@@ -0,0 +1 @@
+export type GetTarget = () => HTMLTextAreaElement;
diff --git a/src/app/state/backupRestore.ts b/src/app/state/backupRestore.ts
new file mode 100644 (file)
index 0000000..2f86b4d
--- /dev/null
@@ -0,0 +1,76 @@
+import { atom } from 'jotai';
+import { ImportRoomKeyProgressData } from 'matrix-js-sdk/lib/crypto-api';
+
+export enum BackupProgressStatus {
+  Idle,
+  Fetching,
+  Loading,
+  Done,
+}
+export type ProgressData = {
+  downloaded: number;
+  successes: number;
+  failures: number;
+  total: number;
+};
+export type IBackupProgress =
+  | {
+      status: BackupProgressStatus.Idle;
+    }
+  | {
+      status: BackupProgressStatus.Fetching;
+    }
+  | {
+      status: BackupProgressStatus.Loading;
+      data: ProgressData;
+    }
+  | {
+      status: BackupProgressStatus.Done;
+    };
+
+const baseBackupRestoreProgressAtom = atom<IBackupProgress>({
+  status: BackupProgressStatus.Idle,
+});
+
+export const backupRestoreProgressAtom = atom<
+  IBackupProgress,
+  [ImportRoomKeyProgressData],
+  undefined
+>(
+  (get) => get(baseBackupRestoreProgressAtom),
+  (get, set, progress) => {
+    if (progress.stage === 'fetch') {
+      set(baseBackupRestoreProgressAtom, {
+        status: BackupProgressStatus.Fetching,
+      });
+      return;
+    }
+
+    if (progress.stage === 'load_keys') {
+      const { total, successes, failures } = progress;
+      if (total === undefined || successes === undefined || failures === undefined) {
+        // Setting to idle as https://github.com/matrix-org/matrix-js-sdk/issues/4703
+        set(baseBackupRestoreProgressAtom, {
+          status: BackupProgressStatus.Idle,
+        });
+        return;
+      }
+      const downloaded = successes + failures;
+      if (downloaded === total) {
+        set(baseBackupRestoreProgressAtom, {
+          status: BackupProgressStatus.Done,
+        });
+        return;
+      }
+      set(baseBackupRestoreProgressAtom, {
+        status: BackupProgressStatus.Loading,
+        data: {
+          downloaded,
+          successes,
+          failures,
+          total,
+        },
+      });
+    }
+  }
+);
index e438024958cebc910ee9c4900432ac884dc534e3..ac47e78b218d6184775908d8b5bdb8ed60a027e7 100644 (file)
@@ -2,11 +2,17 @@ import { atom } from 'jotai';
 
 const STORAGE_KEY = 'settings';
 export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
-export type MessageLayout = 0 | 1 | 2;
+export enum MessageLayout {
+  Modern = 0,
+  Compact = 1,
+  Bubble = 2,
+}
 
 export interface Settings {
-  themeIndex: number;
+  themeId?: string;
   useSystemTheme: boolean;
+  lightThemeId?: string;
+  darkThemeId?: string;
   isMarkdown: boolean;
   editorToolbar: boolean;
   twitterEmoji: boolean;
@@ -26,11 +32,15 @@ export interface Settings {
 
   showNotifications: boolean;
   isNotificationSounds: boolean;
+
+  developerTools: boolean;
 }
 
 const defaultSettings: Settings = {
-  themeIndex: 0,
+  themeId: undefined,
   useSystemTheme: true,
+  lightThemeId: undefined,
+  darkThemeId: undefined,
   isMarkdown: true,
   editorToolbar: false,
   twitterEmoji: false,
@@ -50,6 +60,8 @@ const defaultSettings: Settings = {
 
   showNotifications: true,
   isNotificationSounds: true,
+
+  developerTools: false,
 };
 
 export const getSettings = () => {
index 13869afb25ed096ebb35b45e1f03fa5a52012a02..810c0d2e73826d232fb485032ace87c308621cf5 100644 (file)
@@ -99,11 +99,11 @@ export type TUploadAtom = ReturnType<typeof createUploadAtom>;
 
 export const useBindUploadAtom = (
   mx: MatrixClient,
-  file: TUploadContent,
   uploadAtom: TUploadAtom,
   hideFilename?: boolean
 ) => {
   const [upload, setUpload] = useAtom(uploadAtom);
+  const { file } = upload;
 
   const handleProgress = useThrottle(
     useCallback((progress: UploadProgress) => setUpload({ progress }), [setUpload]),
diff --git a/src/app/styles/Modal.css.ts b/src/app/styles/Modal.css.ts
new file mode 100644 (file)
index 0000000..83e84c2
--- /dev/null
@@ -0,0 +1,6 @@
+import { style } from '@vanilla-extract/css';
+
+export const ModalWide = style({
+  minWidth: '85vw',
+  minHeight: '90vh',
+});
diff --git a/src/app/styles/Text.css.ts b/src/app/styles/Text.css.ts
new file mode 100644 (file)
index 0000000..4e2bd4b
--- /dev/null
@@ -0,0 +1,19 @@
+import { style } from '@vanilla-extract/css';
+
+export const BreakWord = style({
+  wordBreak: 'break-word',
+});
+
+export const LineClamp2 = style({
+  display: '-webkit-box',
+  WebkitLineClamp: 2,
+  WebkitBoxOrient: 'vertical',
+  overflow: 'hidden',
+});
+
+export const LineClamp3 = style({
+  display: '-webkit-box',
+  WebkitLineClamp: 3,
+  WebkitBoxOrient: 'vertical',
+  overflow: 'hidden',
+});
index 6d7b69c13afdec63a5aa083c6c7b5c66ace208a1..d230c6bbf7674aafa0771b06fb9112ce7003ac1c 100644 (file)
@@ -112,3 +112,16 @@ export const randomStr = (len = 12): string => {
   }
   return str;
 };
+
+export const suffixRename = (name: string, validator: (newName: string) => boolean): string => {
+  let suffix = 1;
+  let newName = name;
+  do {
+    newName = name + suffix;
+    suffix += 1;
+  } while (validator(newName));
+
+  return newName;
+};
+
+export const replaceSpaceWithDash = (str: string): string => str.replace(/ /g, '-');
index ab4b8e65c4654e6838bffeecb9185ba726b6a7b2..f931ac45369c503cc606ae4b61a5746b1c22e774 100644 (file)
@@ -6,10 +6,10 @@ export const targetFromEvent = (evt: Event, selector: string): Element | undefin
 export const editableActiveElement = (): boolean =>
   !!document.activeElement &&
   (document.activeElement.nodeName.toLowerCase() === 'input' ||
-    document.activeElement.nodeName.toLowerCase() === 'textbox' ||
+    document.activeElement.nodeName.toLowerCase() === 'textarea' ||
     document.activeElement.getAttribute('contenteditable') === 'true' ||
     document.activeElement.getAttribute('role') === 'input' ||
-    document.activeElement.getAttribute('role') === 'textbox');
+    document.activeElement.getAttribute('role') === 'textarea');
 
 export const isIntersectingScrollView = (
   scrollElement: HTMLElement,
@@ -75,6 +75,9 @@ export const getDataTransferFiles = (dataTransfer: DataTransfer): File[] | undef
   return files;
 };
 
+export const renameFile = (file: File, name: string): File =>
+  new File([file], name, { type: file.type });
+
 export const getImageUrlBlob = async (url: string) => {
   const res = await fetch(url);
   const blob = await res.blob();
@@ -204,3 +207,13 @@ export const tryDecodeURIComponent = (encodedURIComponent: string): string => {
     return encodedURIComponent;
   }
 };
+
+export const syntaxErrorPosition = (error: SyntaxError): number | undefined => {
+  const match = error.message.match(/position\s(\d+)\s/);
+  if (!match) return undefined;
+
+  const posStr = match[1];
+  const position = parseInt(posStr, 10);
+  if (Number.isNaN(position)) return undefined;
+  return position;
+};
index 46a951ffc02cabbed7a632db2b7aaf75cd83b782..ad0463dd10659c1c7eb7342522b265e6bf0abc8d 100644 (file)
@@ -34,6 +34,15 @@ export const onEnterOrSpace =
   };
 
 export const stopPropagation = (evt: KeyboardEvent): boolean => {
+  const ae = document.activeElement;
+  const editableActiveElement = ae
+    ? ae.nodeName.toLowerCase() === 'input' ||
+      ae.nodeName.toLowerCase() === 'textarea' ||
+      ae.getAttribute('contenteditable') === 'true'
+    : false;
+
+  if (editableActiveElement) return false;
+
   evt.stopPropagation();
   return true;
 };
diff --git a/src/app/utils/matrix-crypto.ts b/src/app/utils/matrix-crypto.ts
new file mode 100644 (file)
index 0000000..dda402f
--- /dev/null
@@ -0,0 +1,14 @@
+import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api';
+
+export const verifiedDevice = async (
+  api: CryptoApi,
+  userId: string,
+  deviceId: string
+): Promise<boolean | null> => {
+  const status = await api.getDeviceVerificationStatus(userId, deviceId);
+
+  if (!status) return null;
+
+  const verified = status.crossSigningVerified;
+  return verified;
+};
index 66975e7b9676eaaeab7a4786d5fb263cff39f81f..09f7e8f1c8d23cf2bf392bae96140a7e7e23750b 100644 (file)
@@ -4,6 +4,7 @@ import {
   encryptAttachment,
 } from 'browser-encrypt-attachment';
 import {
+  EventTimeline,
   MatrixClient,
   MatrixError,
   MatrixEvent,
@@ -173,7 +174,7 @@ export const eventWithShortcode = (ev: MatrixEvent) =>
 export const getDMRoomFor = (mx: MatrixClient, userId: string): Room | undefined => {
   const dmLikeRooms = mx
     .getRooms()
-    .filter((room) => mx.isRoomEncrypted(room.roomId) && room.getMembers().length <= 2);
+    .filter((room) => room.hasEncryptionStateEvent() && room.getMembers().length <= 2);
 
   return dmLikeRooms.find((room) => room.getMember(userId));
 };
@@ -205,7 +206,9 @@ export const guessDmRoomUserId = (room: Room, myUserId: string): string => {
   if (member) return member.userId;
 
   // if there are no joined members other than us, use the oldest member
-  const member1 = getOldestMember(room.currentState.getMembers());
+  const member1 = getOldestMember(
+    room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getMembers() ?? []
+  );
   return member1?.userId ?? myUserId;
 };
 
index ad91f18a6aaf38e1e41df301b564f2ca86d4a735..98c5aee975eee62e277f229c0b9c0486cbbb1ac3 100644 (file)
@@ -134,3 +134,8 @@ export const getFileNameExt = (fileName: string): string => {
   const extStart = fileName.lastIndexOf('.') + 1;
   return fileName.slice(extStart);
 };
+export const getFileNameWithoutExt = (fileName: string): string => {
+  const extStart = fileName.lastIndexOf('.');
+  if (extStart === 0 || extStart === -1) return fileName;
+  return fileName.slice(0, extStart);
+};
index 5f4615a6eddcaca18849af23bee4104698b348b1..36de449397eafed2b5ef81f873a5f8b2e16c345b 100644 (file)
@@ -30,10 +30,12 @@ export const getStateEvent = (
   room: Room,
   eventType: StateEvent,
   stateKey = ''
-): MatrixEvent | undefined => room.currentState.getStateEvents(eventType, stateKey) ?? undefined;
+): MatrixEvent | undefined =>
+  room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(eventType, stateKey) ??
+  undefined;
 
 export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[] =>
-  room.currentState.getStateEvents(eventType);
+  room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents(eventType) ?? [];
 
 export const getAccountData = (
   mx: MatrixClient,
index 1967a463bc21f2ea1de92a100e9da1726896f8b2..3e6339d3cd5ec226b025befad4bf210ccdca3a2d 100644 (file)
@@ -57,13 +57,6 @@ export function openProfileViewer(userId, roomId) {
   });
 }
 
-export function openSettings(tabText) {
-  appDispatcher.dispatch({
-    type: cons.actions.navigation.OPEN_SETTINGS,
-    tabText,
-  });
-}
-
 export function openSearch(term) {
   appDispatcher.dispatch({
     type: cons.actions.navigation.OPEN_SEARCH,
@@ -90,10 +83,3 @@ export function openReusableDialog(title, render, afterClose) {
   });
 }
 
-export function openEmojiVerification(request, targetDevice) {
-  appDispatcher.dispatch({
-    type: cons.actions.navigation.OPEN_EMOJI_VERIFICATION,
-    request,
-    targetDevice,
-  });
-}
index 48ae7c47649676e2ded40a3e27927b383e20ea32..90b748104de7f5d8616ecf65d226e02ccad5ed3d 100644 (file)
@@ -1,3 +1,4 @@
+import { EventTimeline } from 'matrix-js-sdk';
 import { getIdServer } from '../../util/matrixUtil';
 
 /**
@@ -63,7 +64,7 @@ function guessDMRoomTargetId(room, myUserId) {
   if (oldestMember) return oldestMember.userId;
 
   // if there are no joined members other than us, use the oldest member
-  room.currentState.getMembers().forEach((member) => {
+  room.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getMembers().forEach((member) => {
     if (member.userId === myUserId) return;
 
     if (typeof oldestMemberTs === 'undefined' || (member.events.member && member.events.member.getTs() < oldestMemberTs)) {
@@ -250,7 +251,7 @@ async function setPowerLevel(mx, roomId, userId, powerLevel) {
 
 async function setMyRoomNick(mx, roomId, nick) {
   const room = mx.getRoom(roomId);
-  const mEvent = room.currentState.getStateEvents('m.room.member', mx.getUserId());
+  const mEvent = room.getLiveTimeline().getState(EventTimeline.FORWARDS).getStateEvents('m.room.member', mx.getUserId());
   const content = mEvent?.getContent();
   if (!content) return;
   await mx.sendStateEvent(roomId, 'm.room.member', {
@@ -261,7 +262,7 @@ async function setMyRoomNick(mx, roomId, nick) {
 
 async function setMyRoomAvatar(mx, roomId, mxc) {
   const room = mx.getRoom(roomId);
-  const mEvent = room.currentState.getStateEvents('m.room.member', mx.getUserId());
+  const mEvent = room.getLiveTimeline().getState(EventTimeline.FORWARDS).getStateEvents('m.room.member', mx.getUserId());
   const content = mEvent?.getContent();
   if (!content) return;
   await mx.sendStateEvent(roomId, 'm.room.member', {
diff --git a/src/client/action/settings.js b/src/client/action/settings.js
deleted file mode 100644 (file)
index e849702..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-import appDispatcher from '../dispatcher';
-import cons from '../state/cons';
-
-export function toggleSystemTheme() {
-  appDispatcher.dispatch({
-    type: cons.actions.settings.TOGGLE_SYSTEM_THEME,
-  });
-}
-
-export function toggleMarkdown() {
-  appDispatcher.dispatch({
-    type: cons.actions.settings.TOGGLE_MARKDOWN,
-  });
-}
-
-export function togglePeopleDrawer() {
-  appDispatcher.dispatch({
-    type: cons.actions.settings.TOGGLE_PEOPLE_DRAWER,
-  });
-}
-
-export function toggleMembershipEvents() {
-  appDispatcher.dispatch({
-    type: cons.actions.settings.TOGGLE_MEMBERSHIP_EVENT,
-  });
-}
-
-export function toggleNickAvatarEvents() {
-  appDispatcher.dispatch({
-    type: cons.actions.settings.TOGGLE_NICKAVATAR_EVENT,
-  });
-}
index 52e8317e40f71cd493b2ad679a6325b40ae406b2..7c774cf19afbbfe9aec3d8e6b054a7482c2ea416 100644 (file)
@@ -1,11 +1,8 @@
 import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
-import Olm from '@matrix-org/olm';
 import { logger } from 'matrix-js-sdk/lib/logger';
 
 import { cryptoCallbacks } from './state/secretStorageKeys';
 
-global.Olm = Olm;
-
 if (import.meta.env.PROD) {
   logger.disableAll();
 }
@@ -24,20 +21,22 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
     dbName: 'web-sync-store',
   });
 
+  const legacyCryptoStore = new IndexedDBCryptoStore(global.indexedDB, 'crypto-store');
+
   const mx = createClient({
     baseUrl: session.baseUrl,
     accessToken: session.accessToken,
     userId: session.userId,
     store: indexedDBStore,
-    cryptoStore: new IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
+    cryptoStore: legacyCryptoStore,
     deviceId: session.deviceId,
     timelineSupport: true,
     cryptoCallbacks: cryptoCallbacks as any,
     verificationMethods: ['m.sas.v1'],
   });
 
-  await mx.initCrypto();
   await indexedDBStore.startup();
+  await mx.initRustCrypto();
 
   mx.setGlobalErrorOnUnknownDevices(false);
   mx.setMaxListeners(50);
@@ -68,3 +67,17 @@ export const logoutClient = async (mx: MatrixClient) => {
   window.localStorage.clear();
   window.location.reload();
 };
+
+export const clearLoginData = async () => {
+  const dbs = await window.indexedDB.databases();
+
+  dbs.forEach((idbInfo) => {
+    const { name } = idbInfo;
+    if (name) {
+      window.indexedDB.deleteDatabase(name);
+    }
+  });
+
+  window.localStorage.clear();
+  window.location.reload();
+};
index 0b225edeffc8b248f80c18280b0131ce16360c9e..1de3d54b1afcdcc8502f79257411e2fb02f1509c 100644 (file)
@@ -37,18 +37,9 @@ const cons = {
       OPEN_JOIN_ALIAS: 'OPEN_JOIN_ALIAS',
       OPEN_INVITE_USER: 'OPEN_INVITE_USER',
       OPEN_PROFILE_VIEWER: 'OPEN_PROFILE_VIEWER',
-      OPEN_SETTINGS: 'OPEN_SETTINGS',
       OPEN_SEARCH: 'OPEN_SEARCH',
       OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
       OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
-      OPEN_EMOJI_VERIFICATION: 'OPEN_EMOJI_VERIFICATION',
-    },
-    settings: {
-      TOGGLE_SYSTEM_THEME: 'TOGGLE_SYSTEM_THEME',
-      TOGGLE_MARKDOWN: 'TOGGLE_MARKDOWN',
-      TOGGLE_PEOPLE_DRAWER: 'TOGGLE_PEOPLE_DRAWER',
-      TOGGLE_MEMBERSHIP_EVENT: 'TOGGLE_MEMBERSHIP_EVENT',
-      TOGGLE_NICKAVATAR_EVENT: 'TOGGLE_NICKAVATAR_EVENT',
     },
   },
   events: {
@@ -59,23 +50,9 @@ const cons = {
       CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED',
       JOIN_ALIAS_OPENED: 'JOIN_ALIAS_OPENED',
       INVITE_USER_OPENED: 'INVITE_USER_OPENED',
-      SETTINGS_OPENED: 'SETTINGS_OPENED',
       SEARCH_OPENED: 'SEARCH_OPENED',
       REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
       REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
-      EMOJI_VERIFICATION_OPENED: 'EMOJI_VERIFICATION_OPENED',
-    },
-    notifications: {
-      NOTI_CHANGED: 'NOTI_CHANGED',
-      FULL_READ: 'FULL_READ',
-      MUTE_TOGGLED: 'MUTE_TOGGLED',
-    },
-    settings: {
-      SYSTEM_THEME_TOGGLED: 'SYSTEM_THEME_TOGGLED',
-      MARKDOWN_TOGGLED: 'MARKDOWN_TOGGLED',
-      PEOPLE_DRAWER_TOGGLED: 'PEOPLE_DRAWER_TOGGLED',
-      MEMBERSHIP_EVENTS_TOGGLED: 'MEMBERSHIP_EVENTS_TOGGLED',
-      NICKAVATAR_EVENTS_TOGGLED: 'NICKAVATAR_EVENTS_TOGGLED',
     },
   },
 };
index 5f28f232f15567964352735c90a7b80216f72a00..1228d9c88540cddcece99f485775cec156938a6f 100644 (file)
@@ -51,9 +51,6 @@ class Navigation extends EventEmitter {
       [cons.actions.navigation.OPEN_PROFILE_VIEWER]: () => {
         this.emit(cons.events.navigation.PROFILE_VIEWER_OPENED, action.userId, action.roomId);
       },
-      [cons.actions.navigation.OPEN_SETTINGS]: () => {
-        this.emit(cons.events.navigation.SETTINGS_OPENED, action.tabText);
-      },
       [cons.actions.navigation.OPEN_SEARCH]: () => {
         this.emit(
           cons.events.navigation.SEARCH_OPENED,
@@ -77,13 +74,6 @@ class Navigation extends EventEmitter {
           action.afterClose,
         );
       },
-      [cons.actions.navigation.OPEN_EMOJI_VERIFICATION]: () => {
-        this.emit(
-          cons.events.navigation.EMOJI_VERIFICATION_OPENED,
-          action.request,
-          action.targetDevice,
-        );
-      },
     };
     actions[action.type]?.();
   }
diff --git a/src/client/state/settings.js b/src/client/state/settings.js
deleted file mode 100644 (file)
index bf9562c..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-import { lightTheme } from 'folds';
-import EventEmitter from 'events';
-import appDispatcher from '../dispatcher';
-
-import cons from './cons';
-import { darkTheme, butterTheme, silverTheme } from '../../colors.css';
-import { onLightFontWeight, onDarkFontWeight } from '../../config.css';
-
-function getSettings() {
-  const settings = localStorage.getItem('settings');
-  if (settings === null) return null;
-  return JSON.parse(settings);
-}
-
-function setSettings(key, value) {
-  let settings = getSettings();
-  if (settings === null) settings = {};
-  settings[key] = value;
-  localStorage.setItem('settings', JSON.stringify(settings));
-}
-
-class Settings extends EventEmitter {
-  constructor() {
-    super();
-
-    this.themeClasses = [lightTheme, silverTheme, darkTheme, butterTheme];
-    this.fontWeightClasses = [onLightFontWeight, onLightFontWeight, onDarkFontWeight, onDarkFontWeight]
-    this.themes = ['', 'silver-theme', 'dark-theme', 'butter-theme'];
-    this.themeIndex = this.getThemeIndex();
-
-    this.useSystemTheme = this.getUseSystemTheme();
-    this.isMarkdown = this.getIsMarkdown();
-    this.isPeopleDrawer = this.getIsPeopleDrawer();
-    this.hideMembershipEvents = this.getHideMembershipEvents();
-    this.hideNickAvatarEvents = this.getHideNickAvatarEvents();
-
-    this.darkModeQueryList = window.matchMedia('(prefers-color-scheme: dark)');
-
-    this.darkModeQueryList.addEventListener('change', () => this.applyTheme())
-
-    this.isTouchScreenDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
-  }
-
-  getThemeIndex() {
-    if (typeof this.themeIndex === 'number') return this.themeIndex;
-
-    const settings = getSettings();
-    if (settings === null) return 0;
-    if (typeof settings.themeIndex === 'undefined') return 0;
-    // eslint-disable-next-line radix
-    return parseInt(settings.themeIndex);
-  }
-
-  getThemeName() {
-    return this.themes[this.themeIndex];
-  }
-
-  _clearTheme() {
-    this.themes.forEach((themeName, index) => {
-      if (themeName !== '') document.body.classList.remove(themeName);
-      document.body.classList.remove(this.themeClasses[index]);
-      document.body.classList.remove(this.fontWeightClasses[index]);
-      document.body.classList.remove('prism-light')
-      document.body.classList.remove('prism-dark')
-    });
-  }
-
-  applyTheme() {
-    this._clearTheme();
-    const autoThemeIndex = this.darkModeQueryList.matches ? 2 : 0;
-    const themeIndex = this.useSystemTheme ? autoThemeIndex : this.themeIndex;
-    if (this.themes[themeIndex] === undefined) return
-    if (this.themes[themeIndex]) document.body.classList.add(this.themes[themeIndex]);
-    document.body.classList.add(this.themeClasses[themeIndex]);
-    document.body.classList.add(this.fontWeightClasses[themeIndex]);
-    document.body.classList.add(themeIndex < 2 ? 'prism-light' : 'prism-dark');
-  }
-
-  setTheme(themeIndex) {
-    this.themeIndex = themeIndex;
-    setSettings('themeIndex', this.themeIndex);
-    this.applyTheme();
-  }
-
-  toggleUseSystemTheme() {
-    this.useSystemTheme = !this.useSystemTheme;
-    setSettings('useSystemTheme', this.useSystemTheme);
-    this.applyTheme();
-
-    this.emit(cons.events.settings.SYSTEM_THEME_TOGGLED, this.useSystemTheme);
-  }
-
-  getUseSystemTheme() {
-    if (typeof this.useSystemTheme === 'boolean') return this.useSystemTheme;
-
-    const settings = getSettings();
-    if (settings === null) return true;
-    if (typeof settings.useSystemTheme === 'undefined') return true;
-    return settings.useSystemTheme;
-  }
-
-  getIsMarkdown() {
-    if (typeof this.isMarkdown === 'boolean') return this.isMarkdown;
-
-    const settings = getSettings();
-    if (settings === null) return true;
-    if (typeof settings.isMarkdown === 'undefined') return true;
-    return settings.isMarkdown;
-  }
-
-  getHideMembershipEvents() {
-    if (typeof this.hideMembershipEvents === 'boolean') return this.hideMembershipEvents;
-
-    const settings = getSettings();
-    if (settings === null) return false;
-    if (typeof settings.hideMembershipEvents === 'undefined') return false;
-    return settings.hideMembershipEvents;
-  }
-
-  getHideNickAvatarEvents() {
-    if (typeof this.hideNickAvatarEvents === 'boolean') return this.hideNickAvatarEvents;
-
-    const settings = getSettings();
-    if (settings === null) return true;
-    if (typeof settings.hideNickAvatarEvents === 'undefined') return true;
-    return settings.hideNickAvatarEvents;
-  }
-
-  getIsPeopleDrawer() {
-    if (typeof this.isPeopleDrawer === 'boolean') return this.isPeopleDrawer;
-
-    const settings = getSettings();
-    if (settings === null) return true;
-    if (typeof settings.isPeopleDrawer === 'undefined') return true;
-    return settings.isPeopleDrawer;
-  }
-
-  setter(action) {
-    const actions = {
-      [cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => {
-        this.toggleUseSystemTheme();
-      },
-      [cons.actions.settings.TOGGLE_MARKDOWN]: () => {
-        this.isMarkdown = !this.isMarkdown;
-        setSettings('isMarkdown', this.isMarkdown);
-        this.emit(cons.events.settings.MARKDOWN_TOGGLED, this.isMarkdown);
-      },
-      [cons.actions.settings.TOGGLE_PEOPLE_DRAWER]: () => {
-        this.isPeopleDrawer = !this.isPeopleDrawer;
-        setSettings('isPeopleDrawer', this.isPeopleDrawer);
-        this.emit(cons.events.settings.PEOPLE_DRAWER_TOGGLED, this.isPeopleDrawer);
-      },
-      [cons.actions.settings.TOGGLE_MEMBERSHIP_EVENT]: () => {
-        this.hideMembershipEvents = !this.hideMembershipEvents;
-        setSettings('hideMembershipEvents', this.hideMembershipEvents);
-        this.emit(cons.events.settings.MEMBERSHIP_EVENTS_TOGGLED, this.hideMembershipEvents);
-      },
-      [cons.actions.settings.TOGGLE_NICKAVATAR_EVENT]: () => {
-        this.hideNickAvatarEvents = !this.hideNickAvatarEvents;
-        setSettings('hideNickAvatarEvents', this.hideNickAvatarEvents);
-        this.emit(cons.events.settings.NICKAVATAR_EVENTS_TOGGLED, this.hideNickAvatarEvents);
-      },
-    };
-
-    actions[action.type]?.();
-  }
-}
-
-const settings = new Settings();
-appDispatcher.register(settings.setter.bind(settings));
-
-export default settings;
index c4f122d2a34244598466c4987203b0afdb479a9f..268cbf788a3a208f4b72177b42d9a75dba3c851c 100644 (file)
@@ -191,7 +191,7 @@ const darkThemeData = {
   Other: {
     FocusRing: 'rgba(255, 255, 255, 0.5)',
     Shadow: 'rgba(0, 0, 0, 1)',
-    Overlay: 'rgba(0, 0, 0, 0.6)',
+    Overlay: 'rgba(0, 0, 0, 0.8)',
   },
 };
 
index 805847f1972ef35a83448a3ccfcd90e63f97f91c..402a4c1b7c60b1be41bc61331d09d7ea4f6bfc14 100644 (file)
@@ -10,8 +10,6 @@ enableMapSet();
 
 import './index.scss';
 
-import settings from './client/state/settings';
-
 import { trimTrailingSlash } from './app/utils/common';
 import App from './app/pages/App';
 
@@ -19,7 +17,6 @@ import App from './app/pages/App';
 import './app/i18n';
 
 document.body.classList.add(configClass, varsClass);
-settings.applyTheme();
 
 // Register Service Worker
 if ('serviceWorker' in navigator) {
index 1078cb3562729390fe1e3a370816c2e9289e690e..20ce9419118e9d0bcc6078e50e94f4af2f57aaef 100644 (file)
@@ -9,4 +9,40 @@ export enum AccountDataEvent {
 
   PoniesUserEmotes = 'im.ponies.user_emotes',
   PoniesEmoteRooms = 'im.ponies.emote_rooms',
+
+  SecretStorageDefaultKey = 'm.secret_storage.default_key',
+
+  CrossSigningMaster = 'm.cross_signing.master',
+  CrossSigningSelf = 'm.cross_signing.self',
+  CrossSigningUser = 'm.cross_signing.user',
+  MegolmBackupV1 = 'm.megolm_backup.v1',
 }
+
+export type SecretStorageDefaultKeyContent = {
+  key: string;
+};
+
+export type SecretStoragePassphraseContent = {
+  algorithm: string;
+  salt: string;
+  iterations: number;
+  bits?: number;
+};
+
+export type SecretStorageKeyContent = {
+  name?: string;
+  algorithm: string;
+  iv?: string;
+  mac?: string;
+  passphrase?: SecretStoragePassphraseContent;
+};
+
+export type SecretContent = {
+  iv: string;
+  ciphertext: string;
+  mac: string;
+};
+
+export type SecretAccountData = {
+  encrypted: Record<string, SecretContent>;
+};
index 7664c3a4000d6f980a26fd93886076d4488f5790..0389177eb312ec7e569b3ba2314b500d142035d8 100644 (file)
@@ -100,44 +100,11 @@ export function getIdServer(userId) {
   return idParts[1];
 }
 
-export function isCrossVerified(mx, deviceId) {
-  try {
-    const crossSignInfo = mx.getStoredCrossSigningForUser(mx.getUserId());
-    const deviceInfo = mx.getStoredDevice(mx.getUserId(), deviceId);
-    const deviceTrust = crossSignInfo.checkDeviceTrust(crossSignInfo, deviceInfo, false, true);
-    return deviceTrust.isCrossSigningVerified();
-  } catch (e) {
-    // device does not support encryption
-    return null;
-  }
-}
-
-export function hasCrossSigningAccountData(mx) {
-  const masterKeyData = mx.getAccountData('m.cross_signing.master');
-  return !!masterKeyData;
-}
-
-export function getDefaultSSKey(mx) {
-  try {
-    return mx.getAccountData('m.secret_storage.default_key').getContent().key;
-  } catch {
-    return undefined;
-  }
-}
-
-export function getSSKeyInfo(mx, key) {
-  try {
-    return mx.getAccountData(`m.secret_storage.key.${key}`).getContent();
-  } catch {
-    return undefined;
-  }
-}
-
 export async function hasDevices(mx, userId) {
   try {
-    const usersDeviceMap = await mx.downloadKeys([userId, mx.getUserId()]);
+    const usersDeviceMap = await mx.getUserDeviceInfo([userId, mx.getUserId()]);
     return Object.values(usersDeviceMap)
-      .every((userDevices) => (Object.keys(userDevices).length > 0));
+      .every((deviceIdToDevices) => deviceIdToDevices.size > 0);
   } catch (e) {
     console.error("Error determining if it's possible to encrypt to all users: ", e);
     return false;