diff --git a/.eslintrc.json b/.eslintrc.json
index 26c9f5aaafb0af24447ebd42fb63ec50fc8d8071..6568f6110a7b555c79d0a0aaa608367860c5af84 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -9,7 +9,7 @@
     },
     "extends": [
         "eslint:recommended",
-        "plugin:vue/essential",
+        "plugin:vue/vue3-essential",
         "@vue/eslint-config-typescript"
     ],
     "globals": {
diff --git a/app/views/oer/mymaterial/edit.php b/app/views/oer/mymaterial/edit.php
index d95b17bc4531538dae5f24e5e9e29a9f33ce33b9..71e010175e03206d1554a150ffcacf6a9ab9f37c 100644
--- a/app/views/oer/mymaterial/edit.php
+++ b/app/views/oer/mymaterial/edit.php
@@ -36,7 +36,9 @@
                             </h1>
                         </header>
                         <div class="image"
-                             :style="'background-image: url(' + logo_url + ');' + (!customlogo ? ' background-size: 60% auto;': '')"></div>
+                             :style="{
+                             backgroundImage: logo_url ? `url(${logo_url})` : null,
+                             backgroundSize: customlogo ? null : '60% auto'}"></div>
                     </article>
                 </label>
 
@@ -198,6 +200,7 @@
                                      searchtype="<?= htmlReady($tagsearch) ?>"
                                      v-model="tags[index]"
                                      :autocomplete="true"
+                                     :keep-value="true"
                         ></quicksearch>
                         <a href="#"
                            @click.prevent="removeTag(index)"
diff --git a/lib/classes/StudipController.php b/lib/classes/StudipController.php
index f749bd0d602f1d37ccac7f7bac72c731c5c7e795..d103621b3e5a2bde805ed8c164a1a51944caeecf 100644
--- a/lib/classes/StudipController.php
+++ b/lib/classes/StudipController.php
@@ -586,6 +586,9 @@ abstract class StudipController extends Trails\Controller
     public function render_form(\Studip\Forms\Form $form)
     {
         \NotificationCenter::postNotification('FormWillRender', $form);
+        if (\Request::isDialog()) {
+            $this->response->add_header('X-No-Buttons', 1);
+        }
         $this->render_template($form->getTemplate(), $this->layout);
     }
 
diff --git a/lib/classes/forms/Form.php b/lib/classes/forms/Form.php
index 58fe47a2a3baa0c535960c02526c3c3a60c19898..576ea43702a5214ca7d1014ca3f5451c5dc9dad0 100644
--- a/lib/classes/forms/Form.php
+++ b/lib/classes/forms/Form.php
@@ -502,6 +502,9 @@ class Form extends Part
     public function render(string|Template $layout = null)
     {
         \NotificationCenter::postNotification('FormWillRender', $this);
+        if (\Request::isDialog()) {
+            header('X-No-Buttons: 1');
+        }
         return $this->getTemplate()->render([], $layout);
     }
 
diff --git a/package-lock.json b/package-lock.json
index 3739a8aea10bff0298e7e1b1b3486a080f294b24..f9151062361bdb42eddeddc2a2f7e7ff18823741 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,14 +8,17 @@
             "name": "@studip/core",
             "version": "6.0.0",
             "license": "GPL-2.0",
+            "dependencies": {
+                "@vojtechlanka/vue-tags-input": "^3.1.1"
+            },
             "devDependencies": {
                 "@axe-core/playwright": "^4.6.1",
-                "@babel/core": "^7.17.9",
-                "@babel/eslint-parser": "^7.17.0",
+                "@babel/core": "^7.26.0",
+                "@babel/eslint-parser": "^7.25.9",
                 "@babel/plugin-syntax-dynamic-import": "^7.8.3",
-                "@babel/plugin-transform-runtime": "^7.12.1",
-                "@babel/preset-env": "^7.12.1",
-                "@babel/register": "^7.12.1",
+                "@babel/plugin-transform-runtime": "^7.25.9",
+                "@babel/preset-env": "^7.26.0",
+                "@babel/register": "^7.25.9",
                 "@ckeditor/ckeditor5-alignment": "^36.x",
                 "@ckeditor/ckeditor5-autoformat": "^36.x",
                 "@ckeditor/ckeditor5-basic-styles": "^36.x",
@@ -45,8 +48,7 @@
                 "@ckeditor/ckeditor5-theme-lark": "^36.x",
                 "@ckeditor/ckeditor5-typing": "^36.x",
                 "@ckeditor/ckeditor5-upload": "^36.x",
-                "@ckeditor/ckeditor5-vue2": "^3.0.1",
-                "@elan-ev/reststate-vuex": "~1.0.5",
+                "@ckeditor/ckeditor5-vue": "^5.1.0",
                 "@fullcalendar/core": "^4.3.1",
                 "@fullcalendar/daygrid": "^4.3.0",
                 "@fullcalendar/interaction": "^4.3.0",
@@ -54,17 +56,17 @@
                 "@fullcalendar/resource-timegrid": "^4.3.0",
                 "@fullcalendar/resource-timeline": "^4.3.0",
                 "@fullcalendar/timegrid": "^4.3.0",
-                "@johmun/vue-tags-input": "^2.1.0",
                 "@playwright/test": "^1.33.0",
                 "@popperjs/core": "^2.11.2",
                 "@types/jquery": "^3.5.16",
                 "@types/jqueryui": "^1.12.16",
                 "@types/lodash": "^4.14.191",
-                "@vue/eslint-config-typescript": "^12.0.0",
+                "@vue/compiler-sfc": "^3.5.13",
+                "@vue/eslint-config-typescript": "^13.0.0",
                 "altcha": "^0.3.2",
-                "autoprefixer": "^10.2.5",
+                "autoprefixer": "^10.4.20",
                 "axios": "^0.21.0",
-                "babel-loader": "^8.2.1",
+                "babel-loader": "^9.2.1",
                 "blueimp-file-upload": "10.31.0",
                 "buffer": "^6.0.3",
                 "chart.js": "^2.9.4",
@@ -72,18 +74,19 @@
                 "ckeditor5-math": "^36.x",
                 "colorpare": "^2.2.0",
                 "cropperjs": "1.5.9",
-                "css-loader": "^5.0.1",
-                "css-minimizer-webpack-plugin": "^1.1.5",
+                "css-loader": "^7.1.2",
+                "css-minimizer-webpack-plugin": "^7.0.0",
                 "dotenv": "^16.0.3",
                 "easygettext": "^2.17.0",
                 "es6-promise": "4.2.8",
-                "eslint": "^7.32.0",
-                "eslint-plugin-vue": "^9.10.0",
-                "eslint-webpack-plugin": "^3.1.1",
-                "expose-loader": "1.0.1",
+                "eslint": "^8.57.1",
+                "eslint-plugin-vue": "^9.28.0",
+                "eslint-webpack-plugin": "^4.2.0",
+                "expose-loader": "^5.0.0",
                 "favico.js": "0.3.10",
                 "file-saver": "^2.0.5",
-                "focus-trap-vue": "^1.1.1",
+                "focus-trap": "^7.6.2",
+                "focus-trap-vue": "^4.0.3",
                 "highlight.js": "10.5.0",
                 "jest": "^29.5.0",
                 "jest-environment-jsdom": "^29.5.0",
@@ -99,47 +102,45 @@
                 "jszip": "^3.8.0",
                 "lodash": "^4.17.20",
                 "md5": "^2.3.0",
-                "mini-css-extract-plugin": "1.3.1",
-                "mitt": "2.1.0",
+                "mini-css-extract-plugin": "^2.9.2",
+                "mitt": "^3.0.1",
                 "mp3tag.js": "3.7.1",
                 "multiselect": "0.9.12",
                 "pdfjs-dist": "^2.6.347",
-                "portal-vue": "^2.1.7",
-                "postcss": "^8.1.8",
-                "postcss-loader": "4.1.0",
+                "pinia": "^2.2.8",
+                "portal-vue": "^3.0.0",
+                "postcss": "^8.4.49",
+                "postcss-loader": "^8.1.1",
                 "postcss-scss": "^4.0.4",
                 "raw-loader": "^4.0.2",
                 "sanitize-html": "^2.7.0",
                 "sass": "^1.29.0",
-                "sass-loader": "^10.1.0",
+                "sass-loader": "^16.0.4",
                 "select2": "4.0.13",
                 "sprintf-js": "^1.0.3",
                 "stream-browserify": "^3.0.0",
-                "style-loader": "^2.0.0",
+                "style-loader": "^4.0.0",
                 "svgo": "3.3.2",
                 "tablesorter": "2.31.3",
-                "ts-loader": "^9.4.2",
-                "typescript": "^5.0.2",
-                "vrp-vue-resizable": "1.2.7",
-                "vue": "^2.7.14",
-                "vue-dragscroll": "^3.0.1",
-                "vue-gettext": "^2.1.12",
-                "vue-loader": "^15.9.8",
-                "vue-router": "^3.5.1",
-                "vue-select": "^3.11.2",
-                "vue-template-babel-compiler": "^1.2.0",
-                "vue-template-compiler": "^2.6.12",
-                "vue-twentytwenty": "^0.10.1",
+                "ts-loader": "^9.5.1",
+                "typescript": "^5.7.2",
+                "vue": "^3.5.13",
+                "vue-dragscroll": "^4.0.6",
+                "vue-loader": "^17.4.2",
+                "vue-resizable": "^2.1.7",
+                "vue-router": "^4.5.0",
+                "vue-select": "^4.0.0-beta.6",
                 "vue-typer": "^1.2.0",
-                "vuedraggable": "^2.24.3",
-                "vuex": "^3.6.2",
-                "webpack": "^5.70.0",
-                "webpack-cli": "^4.10.0",
-                "webpack-merge": "5.4.0",
+                "vue3-gettext": "^3.0.0-beta.5",
+                "vuedraggable": "^4.1.0",
+                "vuex": "^4.1.0",
+                "webpack": "^5.97.0",
+                "webpack-cli": "^5.1.4",
+                "webpack-merge": "^6.0.1",
                 "webpack-notifier": "^1.15.0"
             },
             "engines": {
-                "node": ">=18"
+                "node": ">=18 <23"
             }
         },
         "node_modules/@aashutoshrathi/word-wrap": {
@@ -177,43 +178,47 @@
             }
         },
         "node_modules/@babel/code-frame": {
-            "version": "7.22.13",
-            "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
-            "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+            "version": "7.26.2",
+            "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+            "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/highlight": "^7.22.13",
-                "chalk": "^2.4.2"
+                "@babel/helper-validator-identifier": "^7.25.9",
+                "js-tokens": "^4.0.0",
+                "picocolors": "^1.0.0"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/compat-data": {
-            "version": "7.22.20",
-            "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz",
-            "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==",
+            "version": "7.26.2",
+            "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz",
+            "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/core": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz",
-            "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==",
+            "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==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@ampproject/remapping": "^2.2.0",
-                "@babel/code-frame": "^7.22.13",
-                "@babel/generator": "^7.23.0",
-                "@babel/helper-compilation-targets": "^7.22.15",
-                "@babel/helper-module-transforms": "^7.23.0",
-                "@babel/helpers": "^7.23.0",
-                "@babel/parser": "^7.23.0",
-                "@babel/template": "^7.22.15",
-                "@babel/traverse": "^7.23.0",
-                "@babel/types": "^7.23.0",
+                "@babel/code-frame": "^7.26.0",
+                "@babel/generator": "^7.26.0",
+                "@babel/helper-compilation-targets": "^7.25.9",
+                "@babel/helper-module-transforms": "^7.26.0",
+                "@babel/helpers": "^7.26.0",
+                "@babel/parser": "^7.26.0",
+                "@babel/template": "^7.25.9",
+                "@babel/traverse": "^7.25.9",
+                "@babel/types": "^7.26.0",
                 "convert-source-map": "^2.0.0",
                 "debug": "^4.1.0",
                 "gensync": "^1.0.0-beta.2",
@@ -244,10 +249,11 @@
             }
         },
         "node_modules/@babel/eslint-parser": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.15.tgz",
-            "integrity": "sha512-yc8OOBIQk1EcRrpizuARSQS0TWAcOMpEJ1aafhNznaeYkeL+OhqnDObGFylB8ka8VFF/sZc+S4RzHyO+3LjQxg==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.9.tgz",
+            "integrity": "sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
                 "eslint-visitor-keys": "^2.1.0",
@@ -258,7 +264,7 @@
             },
             "peerDependencies": {
                 "@babel/core": "^7.11.0",
-                "eslint": "^7.5.0 || ^8.0.0"
+                "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0"
             }
         },
         "node_modules/@babel/eslint-parser/node_modules/semver": {
@@ -271,53 +277,59 @@
             }
         },
         "node_modules/@babel/generator": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
-            "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
+            "version": "7.26.2",
+            "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz",
+            "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/types": "^7.23.0",
-                "@jridgewell/gen-mapping": "^0.3.2",
-                "@jridgewell/trace-mapping": "^0.3.17",
-                "jsesc": "^2.5.1"
+                "@babel/parser": "^7.26.2",
+                "@babel/types": "^7.26.0",
+                "@jridgewell/gen-mapping": "^0.3.5",
+                "@jridgewell/trace-mapping": "^0.3.25",
+                "jsesc": "^3.0.2"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-annotate-as-pure": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
-            "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
+            "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/types": "^7.22.5"
+                "@babel/types": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz",
-            "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz",
+            "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/types": "^7.22.5"
+                "@babel/traverse": "^7.25.9",
+                "@babel/types": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-compilation-targets": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
-            "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
+            "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/compat-data": "^7.22.9",
-                "@babel/helper-validator-option": "^7.22.15",
-                "browserslist": "^4.21.9",
+                "@babel/compat-data": "^7.25.9",
+                "@babel/helper-validator-option": "^7.25.9",
+                "browserslist": "^4.24.0",
                 "lru-cache": "^5.1.1",
                 "semver": "^6.3.1"
             },
@@ -330,28 +342,31 @@
             "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
             "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
             "dev": true,
+            "license": "ISC",
             "bin": {
                 "semver": "bin/semver.js"
             }
         },
         "node_modules/@babel/helper-create-class-features-plugin": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz",
-            "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==",
-            "dev": true,
-            "dependencies": {
-                "@babel/helper-annotate-as-pure": "^7.22.5",
-                "@babel/helper-environment-visitor": "^7.22.5",
-                "@babel/helper-function-name": "^7.22.5",
-                "@babel/helper-member-expression-to-functions": "^7.22.15",
-                "@babel/helper-optimise-call-expression": "^7.22.5",
-                "@babel/helper-replace-supers": "^7.22.9",
-                "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
-                "@babel/helper-split-export-declaration": "^7.22.6",
+            "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,
+            "license": "MIT",
+            "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": {
                 "node": ">=6.9.0"
+            },
+            "peerDependencies": {
+                "@babel/core": "^7.0.0"
             }
         },
         "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
@@ -359,19 +374,21 @@
             "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
             "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
             "dev": true,
+            "license": "ISC",
             "bin": {
                 "semver": "bin/semver.js"
             }
         },
         "node_modules/@babel/helper-create-regexp-features-plugin": {
-            "version": "7.22.6",
-            "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.6.tgz",
-            "integrity": "sha512-nBookhLKxAWo/TUCmhnaEJyLz2dekjQvv5SRpE9epWQBcpedWLKt8aZdsuT9XV5ovzR3fENLjRXVT0GsSlGGhA==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz",
+            "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-annotate-as-pure": "^7.22.5",
-                "@nicolo-ribaudo/semver-v6": "^6.3.3",
-                "regexpu-core": "^5.3.1"
+                "@babel/helper-annotate-as-pure": "^7.25.9",
+                "regexpu-core": "^6.1.1",
+                "semver": "^6.3.1"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -380,11 +397,22 @@
                 "@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,
+            "license": "ISC",
+            "bin": {
+                "semver": "bin/semver.js"
+            }
+        },
         "node_modules/@babel/helper-define-polyfill-provider": {
-            "version": "0.4.2",
-            "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz",
-            "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==",
+            "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",
@@ -396,75 +424,44 @@
                 "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
             }
         },
-        "node_modules/@babel/helper-environment-visitor": {
-            "version": "7.22.20",
-            "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
-            "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
-            "dev": true,
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-function-name": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
-            "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
-            "dev": true,
-            "dependencies": {
-                "@babel/template": "^7.22.15",
-                "@babel/types": "^7.23.0"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-hoist-variables": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
-            "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
-            "dev": true,
-            "dependencies": {
-                "@babel/types": "^7.22.5"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
         "node_modules/@babel/helper-member-expression-to-functions": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz",
-            "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==",
+            "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/types": "^7.23.0"
+                "@babel/traverse": "^7.25.9",
+                "@babel/types": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-module-imports": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
-            "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+            "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/types": "^7.22.15"
+                "@babel/traverse": "^7.25.9",
+                "@babel/types": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-module-transforms": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
-            "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
+            "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==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-environment-visitor": "^7.22.20",
-                "@babel/helper-module-imports": "^7.22.15",
-                "@babel/helper-simple-access": "^7.22.5",
-                "@babel/helper-split-export-declaration": "^7.22.6",
-                "@babel/helper-validator-identifier": "^7.22.20"
+                "@babel/helper-module-imports": "^7.25.9",
+                "@babel/helper-validator-identifier": "^7.25.9",
+                "@babel/traverse": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -474,35 +471,38 @@
             }
         },
         "node_modules/@babel/helper-optimise-call-expression": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
-            "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
+            "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.22.5"
+                "@babel/types": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-plugin-utils": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
-            "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz",
+            "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-remap-async-to-generator": {
-            "version": "7.22.20",
-            "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz",
-            "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==",
+            "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.22.5",
-                "@babel/helper-environment-visitor": "^7.22.20",
-                "@babel/helper-wrap-function": "^7.22.20"
+                "@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"
@@ -512,14 +512,15 @@
             }
         },
         "node_modules/@babel/helper-replace-supers": {
-            "version": "7.22.20",
-            "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz",
-            "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz",
+            "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-environment-visitor": "^7.22.20",
-                "@babel/helper-member-expression-to-functions": "^7.22.15",
-                "@babel/helper-optimise-call-expression": "^7.22.5"
+                "@babel/helper-member-expression-to-functions": "^7.25.9",
+                "@babel/helper-optimise-call-expression": "^7.25.9",
+                "@babel/traverse": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -529,115 +530,98 @@
             }
         },
         "node_modules/@babel/helper-simple-access": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
-            "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz",
+            "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/types": "^7.22.5"
+                "@babel/traverse": "^7.25.9",
+                "@babel/types": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
-            "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
-            "dev": true,
-            "dependencies": {
-                "@babel/types": "^7.22.5"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-split-export-declaration": {
-            "version": "7.22.6",
-            "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
-            "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+            "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/types": "^7.22.5"
+                "@babel/traverse": "^7.25.9",
+                "@babel/types": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-string-parser": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
-            "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
-            "dev": true,
+            "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==",
+            "license": "MIT",
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-validator-identifier": {
-            "version": "7.22.20",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
-            "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
-            "dev": true,
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+            "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+            "license": "MIT",
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-validator-option": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
-            "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
+            "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/helper-wrap-function": {
-            "version": "7.22.20",
-            "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz",
-            "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==",
+            "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/helper-function-name": "^7.22.5",
-                "@babel/template": "^7.22.15",
-                "@babel/types": "^7.22.19"
+                "@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.23.1",
-            "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz",
-            "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==",
+            "version": "7.26.0",
+            "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
+            "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/template": "^7.22.15",
-                "@babel/traverse": "^7.23.0",
-                "@babel/types": "^7.23.0"
+                "@babel/template": "^7.25.9",
+                "@babel/types": "^7.26.0"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
-        "node_modules/@babel/highlight": {
-            "version": "7.22.20",
-            "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
-            "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
-            "dev": true,
+        "node_modules/@babel/parser": {
+            "version": "7.26.2",
+            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
+            "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-validator-identifier": "^7.22.20",
-                "chalk": "^2.4.2",
-                "js-tokens": "^4.0.0"
+                "@babel/types": "^7.26.0"
             },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/parser": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
-            "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
-            "dev": true,
             "bin": {
                 "parser": "bin/babel-parser.js"
             },
@@ -645,13 +629,15 @@
                 "node": ">=6.0.0"
             }
         },
-        "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
-            "version": "7.22.15",
-            "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.22.15.tgz",
-            "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==",
+        "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9",
+                "@babel/traverse": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -660,73 +646,71 @@
                 "@babel/core": "^7.0.0"
             }
         },
-        "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz",
-            "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==",
+        "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
+            "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.22.5",
-                "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
-                "@babel/plugin-transform-optional-chaining": "^7.22.15"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             },
             "peerDependencies": {
-                "@babel/core": "^7.13.0"
+                "@babel/core": "^7.0.0"
             }
         },
-        "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": {
-            "version": "7.18.6",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz",
-            "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==",
+        "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+            "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.18.6",
-                "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             },
             "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "@babel/core": "^7.0.0"
             }
         },
-        "node_modules/@babel/plugin-proposal-object-rest-spread": {
-            "version": "7.20.7",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz",
-            "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==",
+        "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+            "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/compat-data": "^7.20.5",
-                "@babel/helper-compilation-targets": "^7.20.7",
-                "@babel/helper-plugin-utils": "^7.20.2",
-                "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
-                "@babel/plugin-transform-parameters": "^7.20.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"
             },
             "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "@babel/core": "^7.13.0"
             }
         },
-        "node_modules/@babel/plugin-proposal-optional-chaining": {
-            "version": "7.21.0",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
-            "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==",
+        "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+            "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.20.2",
-                "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
-                "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+                "@babel/helper-plugin-utils": "^7.25.9",
+                "@babel/traverse": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
             },
             "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
+                "@babel/core": "^7.0.0"
             }
         },
         "node_modules/@babel/plugin-proposal-private-property-in-object": {
@@ -777,21 +761,6 @@
                 "@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,
-            "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-dynamic-import": {
             "version": "7.8.3",
             "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
@@ -804,25 +773,14 @@
                 "@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,
-            "dependencies": {
-                "@babel/helper-plugin-utils": "^7.8.3"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
         "node_modules/@babel/plugin-syntax-import-assertions": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz",
-            "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -832,12 +790,13 @@
             }
         },
         "node_modules/@babel/plugin-syntax-import-attributes": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz",
-            "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -957,21 +916,6 @@
                 "@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,
-            "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",
@@ -1019,12 +963,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-arrow-functions": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz",
-            "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1034,15 +979,15 @@
             }
         },
         "node_modules/@babel/plugin-transform-async-generator-functions": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz",
-            "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==",
+            "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-environment-visitor": "^7.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@babel/helper-remap-async-to-generator": "^7.22.9",
-                "@babel/plugin-syntax-async-generators": "^7.8.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"
@@ -1052,14 +997,15 @@
             }
         },
         "node_modules/@babel/plugin-transform-async-to-generator": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz",
-            "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@babel/helper-remap-async-to-generator": "^7.22.5"
+                "@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"
@@ -1069,12 +1015,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-block-scoped-functions": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz",
-            "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz",
+            "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1084,12 +1031,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-block-scoping": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz",
-            "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1099,13 +1047,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-class-properties": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz",
-            "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-create-class-features-plugin": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1115,14 +1064,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-class-static-block": {
-            "version": "7.22.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz",
-            "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==",
+            "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.22.11",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@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"
@@ -1132,19 +1081,17 @@
             }
         },
         "node_modules/@babel/plugin-transform-classes": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz",
-            "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==",
+            "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.22.5",
-                "@babel/helper-compilation-targets": "^7.22.15",
-                "@babel/helper-environment-visitor": "^7.22.5",
-                "@babel/helper-function-name": "^7.22.5",
-                "@babel/helper-optimise-call-expression": "^7.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@babel/helper-replace-supers": "^7.22.9",
-                "@babel/helper-split-export-declaration": "^7.22.6",
+                "@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": {
@@ -1155,13 +1102,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-computed-properties": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz",
-            "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==",
+            "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.22.5",
-                "@babel/template": "^7.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9",
+                "@babel/template": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1171,12 +1119,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-destructuring": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz",
-            "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1186,13 +1135,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-dotall-regex": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz",
-            "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1202,12 +1152,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-duplicate-keys": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz",
-            "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1216,14 +1167,31 @@
                 "@babel/core": "^7.0.0-0"
             }
         },
+        "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
+            "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.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
+            },
+            "engines": {
+                "node": ">=6.9.0"
+            },
+            "peerDependencies": {
+                "@babel/core": "^7.0.0"
+            }
+        },
         "node_modules/@babel/plugin-transform-dynamic-import": {
-            "version": "7.22.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz",
-            "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==",
+            "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.22.5",
-                "@babel/plugin-syntax-dynamic-import": "^7.8.3"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1233,13 +1201,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-exponentiation-operator": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz",
-            "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz",
+            "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1249,13 +1218,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-export-namespace-from": {
-            "version": "7.22.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz",
-            "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==",
+            "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.22.5",
-                "@babel/plugin-syntax-export-namespace-from": "^7.8.3"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1265,12 +1234,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-for-of": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz",
-            "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9",
+                "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1280,14 +1251,15 @@
             }
         },
         "node_modules/@babel/plugin-transform-function-name": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz",
-            "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==",
+            "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.22.5",
-                "@babel/helper-function-name": "^7.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-compilation-targets": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9",
+                "@babel/traverse": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1297,13 +1269,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-json-strings": {
-            "version": "7.22.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz",
-            "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==",
+            "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.22.5",
-                "@babel/plugin-syntax-json-strings": "^7.8.3"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1313,12 +1285,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-literals": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz",
-            "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1328,13 +1301,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-logical-assignment-operators": {
-            "version": "7.22.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz",
-            "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==",
+            "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.22.5",
-                "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1344,12 +1317,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-member-expression-literals": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz",
-            "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1359,13 +1333,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-modules-amd": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz",
-            "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-module-transforms": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1375,14 +1350,15 @@
             }
         },
         "node_modules/@babel/plugin-transform-modules-commonjs": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz",
-            "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz",
+            "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-module-transforms": "^7.23.0",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@babel/helper-simple-access": "^7.22.5"
+                "@babel/helper-module-transforms": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9",
+                "@babel/helper-simple-access": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1392,15 +1368,16 @@
             }
         },
         "node_modules/@babel/plugin-transform-modules-systemjs": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz",
-            "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==",
+            "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-hoist-variables": "^7.22.5",
-                "@babel/helper-module-transforms": "^7.23.0",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@babel/helper-validator-identifier": "^7.22.20"
+                "@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"
@@ -1410,13 +1387,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-modules-umd": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz",
-            "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-module-transforms": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1426,13 +1404,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz",
-            "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1442,12 +1421,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-new-target": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz",
-            "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1457,13 +1437,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
-            "version": "7.22.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz",
-            "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz",
+            "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1473,13 +1453,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-numeric-separator": {
-            "version": "7.22.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz",
-            "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==",
+            "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.22.5",
-                "@babel/plugin-syntax-numeric-separator": "^7.10.4"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1489,16 +1469,15 @@
             }
         },
         "node_modules/@babel/plugin-transform-object-rest-spread": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz",
-            "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==",
+            "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/compat-data": "^7.22.9",
-                "@babel/helper-compilation-targets": "^7.22.15",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
-                "@babel/plugin-transform-parameters": "^7.22.15"
+                "@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"
@@ -1508,13 +1487,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-object-super": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz",
-            "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==",
+            "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.22.5",
-                "@babel/helper-replace-supers": "^7.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9",
+                "@babel/helper-replace-supers": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1524,13 +1504,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-optional-catch-binding": {
-            "version": "7.22.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz",
-            "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==",
+            "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.22.5",
-                "@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1540,14 +1520,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-optional-chaining": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz",
-            "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==",
+            "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.22.5",
-                "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
-                "@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"
@@ -1557,12 +1537,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-parameters": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz",
-            "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1572,13 +1553,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-private-methods": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz",
-            "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-create-class-features-plugin": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1588,15 +1570,15 @@
             }
         },
         "node_modules/@babel/plugin-transform-private-property-in-object": {
-            "version": "7.22.11",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz",
-            "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==",
+            "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.22.5",
-                "@babel/helper-create-class-features-plugin": "^7.22.11",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@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"
@@ -1606,12 +1588,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-property-literals": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz",
-            "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1621,12 +1604,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-regenerator": {
-            "version": "7.22.10",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz",
-            "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==",
+            "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.22.5",
+                "@babel/helper-plugin-utils": "^7.25.9",
                 "regenerator-transform": "^0.15.2"
             },
             "engines": {
@@ -1636,13 +1620,31 @@
                 "@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,
+            "license": "MIT",
+            "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.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz",
-            "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1652,16 +1654,17 @@
             }
         },
         "node_modules/@babel/plugin-transform-runtime": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.15.tgz",
-            "integrity": "sha512-tEVLhk8NRZSmwQ0DJtxxhTrCht1HVo8VaMzYT4w6lwyKBuHsgoioAUA7/6eT2fRfc5/23fuGdlwIxXhRVgWr4g==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz",
+            "integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-module-imports": "^7.22.15",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "babel-plugin-polyfill-corejs2": "^0.4.5",
-                "babel-plugin-polyfill-corejs3": "^0.8.3",
-                "babel-plugin-polyfill-regenerator": "^0.5.2",
+                "@babel/helper-module-imports": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9",
+                "babel-plugin-polyfill-corejs2": "^0.4.10",
+                "babel-plugin-polyfill-corejs3": "^0.10.6",
+                "babel-plugin-polyfill-regenerator": "^0.6.1",
                 "semver": "^6.3.1"
             },
             "engines": {
@@ -1681,12 +1684,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-shorthand-properties": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz",
-            "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1696,13 +1700,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-spread": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz",
-            "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==",
+            "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.22.5",
-                "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9",
+                "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1712,12 +1717,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-sticky-regex": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz",
-            "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1727,12 +1733,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-template-literals": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz",
-            "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1742,12 +1749,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-typeof-symbol": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz",
-            "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1757,12 +1765,13 @@
             }
         },
         "node_modules/@babel/plugin-transform-unicode-escapes": {
-            "version": "7.22.10",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz",
-            "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==",
+            "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.22.5"
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1772,13 +1781,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-unicode-property-regex": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz",
-            "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1788,13 +1798,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-unicode-regex": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz",
-            "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1804,13 +1815,14 @@
             }
         },
         "node_modules/@babel/plugin-transform-unicode-sets-regex": {
-            "version": "7.22.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz",
-            "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==",
+            "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.22.5",
-                "@babel/helper-plugin-utils": "^7.22.5"
+                "@babel/helper-create-regexp-features-plugin": "^7.25.9",
+                "@babel/helper-plugin-utils": "^7.25.9"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1820,90 +1832,80 @@
             }
         },
         "node_modules/@babel/preset-env": {
-            "version": "7.22.20",
-            "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz",
-            "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==",
-            "dev": true,
-            "dependencies": {
-                "@babel/compat-data": "^7.22.20",
-                "@babel/helper-compilation-targets": "^7.22.15",
-                "@babel/helper-plugin-utils": "^7.22.5",
-                "@babel/helper-validator-option": "^7.22.15",
-                "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15",
-                "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15",
+            "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,
+            "license": "MIT",
+            "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.22.5",
-                "@babel/plugin-syntax-import-attributes": "^7.22.5",
-                "@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.22.5",
-                "@babel/plugin-transform-async-generator-functions": "^7.22.15",
-                "@babel/plugin-transform-async-to-generator": "^7.22.5",
-                "@babel/plugin-transform-block-scoped-functions": "^7.22.5",
-                "@babel/plugin-transform-block-scoping": "^7.22.15",
-                "@babel/plugin-transform-class-properties": "^7.22.5",
-                "@babel/plugin-transform-class-static-block": "^7.22.11",
-                "@babel/plugin-transform-classes": "^7.22.15",
-                "@babel/plugin-transform-computed-properties": "^7.22.5",
-                "@babel/plugin-transform-destructuring": "^7.22.15",
-                "@babel/plugin-transform-dotall-regex": "^7.22.5",
-                "@babel/plugin-transform-duplicate-keys": "^7.22.5",
-                "@babel/plugin-transform-dynamic-import": "^7.22.11",
-                "@babel/plugin-transform-exponentiation-operator": "^7.22.5",
-                "@babel/plugin-transform-export-namespace-from": "^7.22.11",
-                "@babel/plugin-transform-for-of": "^7.22.15",
-                "@babel/plugin-transform-function-name": "^7.22.5",
-                "@babel/plugin-transform-json-strings": "^7.22.11",
-                "@babel/plugin-transform-literals": "^7.22.5",
-                "@babel/plugin-transform-logical-assignment-operators": "^7.22.11",
-                "@babel/plugin-transform-member-expression-literals": "^7.22.5",
-                "@babel/plugin-transform-modules-amd": "^7.22.5",
-                "@babel/plugin-transform-modules-commonjs": "^7.22.15",
-                "@babel/plugin-transform-modules-systemjs": "^7.22.11",
-                "@babel/plugin-transform-modules-umd": "^7.22.5",
-                "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5",
-                "@babel/plugin-transform-new-target": "^7.22.5",
-                "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11",
-                "@babel/plugin-transform-numeric-separator": "^7.22.11",
-                "@babel/plugin-transform-object-rest-spread": "^7.22.15",
-                "@babel/plugin-transform-object-super": "^7.22.5",
-                "@babel/plugin-transform-optional-catch-binding": "^7.22.11",
-                "@babel/plugin-transform-optional-chaining": "^7.22.15",
-                "@babel/plugin-transform-parameters": "^7.22.15",
-                "@babel/plugin-transform-private-methods": "^7.22.5",
-                "@babel/plugin-transform-private-property-in-object": "^7.22.11",
-                "@babel/plugin-transform-property-literals": "^7.22.5",
-                "@babel/plugin-transform-regenerator": "^7.22.10",
-                "@babel/plugin-transform-reserved-words": "^7.22.5",
-                "@babel/plugin-transform-shorthand-properties": "^7.22.5",
-                "@babel/plugin-transform-spread": "^7.22.5",
-                "@babel/plugin-transform-sticky-regex": "^7.22.5",
-                "@babel/plugin-transform-template-literals": "^7.22.5",
-                "@babel/plugin-transform-typeof-symbol": "^7.22.5",
-                "@babel/plugin-transform-unicode-escapes": "^7.22.10",
-                "@babel/plugin-transform-unicode-property-regex": "^7.22.5",
-                "@babel/plugin-transform-unicode-regex": "^7.22.5",
-                "@babel/plugin-transform-unicode-sets-regex": "^7.22.5",
+                "@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/types": "^7.22.19",
-                "babel-plugin-polyfill-corejs2": "^0.4.5",
-                "babel-plugin-polyfill-corejs3": "^0.8.3",
-                "babel-plugin-polyfill-regenerator": "^0.5.2",
-                "core-js-compat": "^3.31.0",
+                "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.38.1",
                 "semver": "^6.3.1"
             },
             "engines": {
@@ -1937,15 +1939,16 @@
             }
         },
         "node_modules/@babel/register": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.15.tgz",
-            "integrity": "sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.25.9.tgz",
+            "integrity": "sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "clone-deep": "^4.0.1",
                 "find-cache-dir": "^2.0.0",
                 "make-dir": "^2.1.0",
-                "pirates": "^4.0.5",
+                "pirates": "^4.0.6",
                 "source-map-support": "^0.5.16"
             },
             "engines": {
@@ -1955,12 +1958,6 @@
                 "@babel/core": "^7.0.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
-        },
         "node_modules/@babel/runtime": {
             "version": "7.22.6",
             "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz",
@@ -1974,34 +1971,33 @@
             }
         },
         "node_modules/@babel/template": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
-            "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
+            "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@babel/code-frame": "^7.22.13",
-                "@babel/parser": "^7.22.15",
-                "@babel/types": "^7.22.15"
+                "@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.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz",
-            "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==",
-            "dev": true,
-            "dependencies": {
-                "@babel/code-frame": "^7.22.13",
-                "@babel/generator": "^7.23.0",
-                "@babel/helper-environment-visitor": "^7.22.20",
-                "@babel/helper-function-name": "^7.23.0",
-                "@babel/helper-hoist-variables": "^7.22.5",
-                "@babel/helper-split-export-declaration": "^7.22.6",
-                "@babel/parser": "^7.23.0",
-                "@babel/types": "^7.23.0",
-                "debug": "^4.1.0",
+            "version": "7.25.9",
+            "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz",
+            "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@babel/code-frame": "^7.25.9",
+                "@babel/generator": "^7.25.9",
+                "@babel/parser": "^7.25.9",
+                "@babel/template": "^7.25.9",
+                "@babel/types": "^7.25.9",
+                "debug": "^4.3.1",
                 "globals": "^11.1.0"
             },
             "engines": {
@@ -2009,14 +2005,13 @@
             }
         },
         "node_modules/@babel/types": {
-            "version": "7.23.0",
-            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
-            "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
-            "dev": true,
+            "version": "7.26.0",
+            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
+            "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+            "license": "MIT",
             "dependencies": {
-                "@babel/helper-string-parser": "^7.22.5",
-                "@babel/helper-validator-identifier": "^7.22.20",
-                "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"
@@ -2186,15 +2181,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/@ckeditor/ckeditor5-dev-translations/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/@ckeditor/ckeditor5-dev-translations/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2279,15 +2265,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/@ckeditor/ckeditor5-dev-utils/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/@ckeditor/ckeditor5-dev-utils/node_modules/postcss-loader": {
             "version": "4.3.0",
             "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-4.3.0.tgz",
@@ -2330,18 +2307,39 @@
                 "url": "https://opencollective.com/webpack"
             }
         },
-        "node_modules/@ckeditor/ckeditor5-dev-utils/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==",
+        "node_modules/@ckeditor/ckeditor5-dev-utils/node_modules/style-loader": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz",
+            "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "has-flag": "^4.0.0"
+                "loader-utils": "^2.0.0",
+                "schema-utils": "^3.0.0"
             },
             "engines": {
-                "node": ">=8"
-            }
-        },
+                "node": ">= 10.13.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/webpack"
+            },
+            "peerDependencies": {
+                "webpack": "^4.0.0 || ^5.0.0"
+            }
+        },
+        "node_modules/@ckeditor/ckeditor5-dev-utils/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"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
         "node_modules/@ckeditor/ckeditor5-easy-image": {
             "version": "36.0.1",
             "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-easy-image/-/ckeditor5-easy-image-36.0.1.tgz",
@@ -2731,13 +2729,15 @@
                 "npm": ">=5.7.1"
             }
         },
-        "node_modules/@ckeditor/ckeditor5-vue2": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-vue2/-/ckeditor5-vue2-3.0.1.tgz",
-            "integrity": "sha512-vS9ffP3rOFgM8oeG9XVFD+UtcYAhkgFDfBHjswJuCgUM0Iw8uqLlCiDPbs4PeJsend8GcmmtNeFdcQaSPkOtpw==",
+        "node_modules/@ckeditor/ckeditor5-vue": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-vue/-/ckeditor5-vue-5.1.0.tgz",
+            "integrity": "sha512-KEx4Tj2Irr4ZbLG8LnaKpb0Dgd8qmLmKFWeiKkQwM3RAAeYRYOCcBVB2Y168I9KA8wRosPxgOO9jbQ92yopYHA==",
             "dev": true,
+            "hasInstallScript": true,
+            "license": "GPL-2.0-or-later",
             "engines": {
-                "node": ">=14.0.0",
+                "node": ">=16.0.0",
                 "npm": ">=5.7.1"
             }
         },
@@ -2785,24 +2785,6 @@
                 "node": ">=10.0.0"
             }
         },
-        "node_modules/@elan-ev/reststate-client": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/@elan-ev/reststate-client/-/reststate-client-1.0.0.tgz",
-            "integrity": "sha512-mXnQAeGJ+MZ5eKlJOaZnmq27UbUMeztKm5tgRDIgA2s49KcGbMOsug5KuZZFZFOj/AKE/N7O04pVOWRlgHpIBg==",
-            "dev": true
-        },
-        "node_modules/@elan-ev/reststate-vuex": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/@elan-ev/reststate-vuex/-/reststate-vuex-1.0.6.tgz",
-            "integrity": "sha512-hj1MCzPx9dF2jhQ2rstTBqg4WGNT+cvfIBBpCgsAE9CSrroZlL0Gdb6+3XVnM9cOKQi7NdAnTy6b/FMC5+rlaw==",
-            "dev": true,
-            "dependencies": {
-                "@elan-ev/reststate-client": "^1.0.0"
-            },
-            "peerDependencies": {
-                "vuex": "^3.0.1"
-            }
-        },
         "node_modules/@eslint-community/eslint-utils": {
             "version": "4.4.0",
             "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -2831,39 +2813,52 @@
             }
         },
         "node_modules/@eslint-community/regexpp": {
-            "version": "4.9.1",
-            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz",
-            "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==",
+            "version": "4.11.1",
+            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
+            "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
             }
         },
         "node_modules/@eslint/eslintrc": {
-            "version": "0.4.3",
-            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
-            "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
+            "version": "2.1.4",
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+            "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "ajv": "^6.12.4",
-                "debug": "^4.1.1",
-                "espree": "^7.3.0",
-                "globals": "^13.9.0",
-                "ignore": "^4.0.6",
+                "debug": "^4.3.2",
+                "espree": "^9.6.0",
+                "globals": "^13.19.0",
+                "ignore": "^5.2.0",
                 "import-fresh": "^3.2.1",
-                "js-yaml": "^3.13.1",
-                "minimatch": "^3.0.4",
+                "js-yaml": "^4.1.0",
+                "minimatch": "^3.1.2",
                 "strip-json-comments": "^3.1.1"
             },
             "engines": {
-                "node": "^10.12.0 || >=12.0.0"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
             }
         },
+        "node_modules/@eslint/eslintrc/node_modules/argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+            "dev": true,
+            "license": "Python-2.0"
+        },
         "node_modules/@eslint/eslintrc/node_modules/globals": {
-            "version": "13.20.0",
-            "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
-            "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+            "version": "13.24.0",
+            "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+            "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "type-fest": "^0.20.2"
             },
@@ -2874,13 +2869,17 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/@eslint/eslintrc/node_modules/ignore": {
-            "version": "4.0.6",
-            "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
-            "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+        "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
             "dev": true,
-            "engines": {
-                "node": ">= 4"
+            "license": "MIT",
+            "dependencies": {
+                "argparse": "^2.0.1"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
             }
         },
         "node_modules/@eslint/eslintrc/node_modules/type-fest": {
@@ -2888,6 +2887,7 @@
             "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,
+            "license": "(MIT OR CC0-1.0)",
             "engines": {
                 "node": ">=10"
             },
@@ -2895,6 +2895,16 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/@eslint/js": {
+            "version": "8.57.1",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+            "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            }
+        },
         "node_modules/@fullcalendar/core": {
             "version": "4.4.2",
             "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-4.4.2.tgz",
@@ -2996,24 +3006,145 @@
             "dev": true
         },
         "node_modules/@humanwhocodes/config-array": {
-            "version": "0.5.0",
-            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
-            "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
+            "version": "0.13.0",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+            "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+            "deprecated": "Use @eslint/config-array instead",
             "dev": true,
+            "license": "Apache-2.0",
             "dependencies": {
-                "@humanwhocodes/object-schema": "^1.2.0",
-                "debug": "^4.1.1",
-                "minimatch": "^3.0.4"
+                "@humanwhocodes/object-schema": "^2.0.3",
+                "debug": "^4.3.1",
+                "minimatch": "^3.0.5"
             },
             "engines": {
                 "node": ">=10.10.0"
             }
         },
+        "node_modules/@humanwhocodes/module-importer": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "engines": {
+                "node": ">=12.22"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/nzakas"
+            }
+        },
         "node_modules/@humanwhocodes/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==",
-            "dev": true
+            "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,
+            "license": "BSD-3-Clause"
+        },
+        "node_modules/@isaacs/cliui": {
+            "version": "8.0.2",
+            "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+            "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "string-width": "^5.1.2",
+                "string-width-cjs": "npm:string-width@^4.2.0",
+                "strip-ansi": "^7.0.1",
+                "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+                "wrap-ansi": "^8.1.0",
+                "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+            "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+            }
+        },
+        "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+            "version": "6.2.1",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+            "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+            "version": "9.2.2",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+            "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/@isaacs/cliui/node_modules/string-width": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+            "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "eastasianwidth": "^0.2.0",
+                "emoji-regex": "^9.2.2",
+                "strip-ansi": "^7.0.1"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+            "version": "7.1.0",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+            "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "ansi-regex": "^6.0.1"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+            }
+        },
+        "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+            "version": "8.1.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+            "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "ansi-styles": "^6.1.0",
+                "string-width": "^5.0.1",
+                "strip-ansi": "^7.0.1"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+            }
         },
         "node_modules/@istanbuljs/load-nyc-config": {
             "version": "1.1.0",
@@ -3109,15 +3240,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/@jest/console/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/@jest/console/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3220,15 +3342,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/@jest/core/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/@jest/core/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3399,15 +3512,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/@jest/reporters/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/@jest/reporters/node_modules/jest-worker": {
             "version": "29.7.0",
             "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
@@ -3581,15 +3685,6 @@
             "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
             "dev": true
         },
-        "node_modules/@jest/transform/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/@jest/transform/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3662,15 +3757,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/@jest/types/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/@jest/types/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3683,27 +3769,16 @@
                 "node": ">=8"
             }
         },
-        "node_modules/@johmun/vue-tags-input": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/@johmun/vue-tags-input/-/vue-tags-input-2.1.0.tgz",
-            "integrity": "sha512-Fdwfss/TqCqMJbGAkmlzKbcG/ia1MstYjhqPBj+zG7h/166tIcE1TIftUxhT9LZ+RWjRSG0EFA1UyaHQSr3k3Q==",
-            "dev": true,
-            "dependencies": {
-                "vue": "^2.6.10"
-            },
-            "peerDependencies": {
-                "vue": "2.x"
-            }
-        },
         "node_modules/@jridgewell/gen-mapping": {
-            "version": "0.3.3",
-            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
-            "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+            "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.0.1",
+                "@jridgewell/set-array": "^1.2.1",
                 "@jridgewell/sourcemap-codec": "^1.4.10",
-                "@jridgewell/trace-mapping": "^0.3.9"
+                "@jridgewell/trace-mapping": "^0.3.24"
             },
             "engines": {
                 "node": ">=6.0.0"
@@ -3719,10 +3794,11 @@
             }
         },
         "node_modules/@jridgewell/set-array": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
-            "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+            "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=6.0.0"
             }
@@ -3738,27 +3814,22 @@
             }
         },
         "node_modules/@jridgewell/sourcemap-codec": {
-            "version": "1.4.15",
-            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
-            "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
-            "dev": true
+            "version": "1.5.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+            "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+            "license": "MIT"
         },
         "node_modules/@jridgewell/trace-mapping": {
-            "version": "0.3.18",
-            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
-            "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
+            "version": "0.3.25",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+            "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@jridgewell/resolve-uri": "3.1.0",
-                "@jridgewell/sourcemap-codec": "1.4.14"
+                "@jridgewell/resolve-uri": "^3.1.0",
+                "@jridgewell/sourcemap-codec": "^1.4.14"
             }
         },
-        "node_modules/@jridgewell/trace-mapping/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==",
-            "dev": true
-        },
         "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
             "version": "5.1.1-v1",
             "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -3768,15 +3839,6 @@
                 "eslint-scope": "5.1.1"
             }
         },
-        "node_modules/@nicolo-ribaudo/semver-v6": {
-            "version": "6.3.3",
-            "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz",
-            "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==",
-            "dev": true,
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
         "node_modules/@nodelib/fs.scandir": {
             "version": "2.1.5",
             "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3836,6 +3898,17 @@
                 "node": ">=10"
             }
         },
+        "node_modules/@pkgjs/parseargs": {
+            "version": "0.11.0",
+            "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+            "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "engines": {
+                "node": ">=14"
+            }
+        },
         "node_modules/@playwright/test": {
             "version": "1.38.1",
             "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
@@ -3945,30 +4018,33 @@
             }
         },
         "node_modules/@types/eslint": {
-            "version": "8.40.2",
-            "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz",
-            "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==",
+            "version": "8.56.12",
+            "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
+            "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@types/estree": "*",
                 "@types/json-schema": "*"
             }
         },
         "node_modules/@types/eslint-scope": {
-            "version": "3.7.4",
-            "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz",
-            "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==",
+            "version": "3.7.7",
+            "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+            "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@types/eslint": "*",
                 "@types/estree": "*"
             }
         },
         "node_modules/@types/estree": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
-            "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
-            "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==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/@types/glob": {
             "version": "7.2.0",
@@ -4072,11 +4148,12 @@
             "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
             "dev": true
         },
-        "node_modules/@types/q": {
-            "version": "1.5.5",
-            "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz",
-            "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==",
-            "dev": true
+        "node_modules/@types/parse5": {
+            "version": "5.0.3",
+            "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz",
+            "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/@types/raf": {
             "version": "3.4.0",
@@ -4085,12 +4162,6 @@
             "dev": true,
             "optional": true
         },
-        "node_modules/@types/semver": {
-            "version": "7.5.3",
-            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz",
-            "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==",
-            "dev": true
-        },
         "node_modules/@types/sizzle": {
             "version": "2.3.3",
             "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
@@ -4125,33 +4196,32 @@
             "dev": true
         },
         "node_modules/@typescript-eslint/eslint-plugin": {
-            "version": "6.7.5",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.5.tgz",
-            "integrity": "sha512-JhtAwTRhOUcP96D0Y6KYnwig/MRQbOoLGXTON2+LlyB/N35SP9j1boai2zzwXb7ypKELXMx3DVk9UTaEq1vHEw==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
+            "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@eslint-community/regexpp": "^4.5.1",
-                "@typescript-eslint/scope-manager": "6.7.5",
-                "@typescript-eslint/type-utils": "6.7.5",
-                "@typescript-eslint/utils": "6.7.5",
-                "@typescript-eslint/visitor-keys": "6.7.5",
-                "debug": "^4.3.4",
+                "@eslint-community/regexpp": "^4.10.0",
+                "@typescript-eslint/scope-manager": "7.18.0",
+                "@typescript-eslint/type-utils": "7.18.0",
+                "@typescript-eslint/utils": "7.18.0",
+                "@typescript-eslint/visitor-keys": "7.18.0",
                 "graphemer": "^1.4.0",
-                "ignore": "^5.2.4",
+                "ignore": "^5.3.1",
                 "natural-compare": "^1.4.0",
-                "semver": "^7.5.4",
-                "ts-api-utils": "^1.0.1"
+                "ts-api-utils": "^1.3.0"
             },
             "engines": {
-                "node": "^16.0.0 || >=18.0.0"
+                "node": "^18.18.0 || >=20.0.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/typescript-eslint"
             },
             "peerDependencies": {
-                "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
-                "eslint": "^7.0.0 || ^8.0.0"
+                "@typescript-eslint/parser": "^7.0.0",
+                "eslint": "^8.56.0"
             },
             "peerDependenciesMeta": {
                 "typescript": {
@@ -4160,26 +4230,27 @@
             }
         },
         "node_modules/@typescript-eslint/parser": {
-            "version": "6.7.5",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.5.tgz",
-            "integrity": "sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
+            "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
             "dev": true,
+            "license": "BSD-2-Clause",
             "dependencies": {
-                "@typescript-eslint/scope-manager": "6.7.5",
-                "@typescript-eslint/types": "6.7.5",
-                "@typescript-eslint/typescript-estree": "6.7.5",
-                "@typescript-eslint/visitor-keys": "6.7.5",
+                "@typescript-eslint/scope-manager": "7.18.0",
+                "@typescript-eslint/types": "7.18.0",
+                "@typescript-eslint/typescript-estree": "7.18.0",
+                "@typescript-eslint/visitor-keys": "7.18.0",
                 "debug": "^4.3.4"
             },
             "engines": {
-                "node": "^16.0.0 || >=18.0.0"
+                "node": "^18.18.0 || >=20.0.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/typescript-eslint"
             },
             "peerDependencies": {
-                "eslint": "^7.0.0 || ^8.0.0"
+                "eslint": "^8.56.0"
             },
             "peerDependenciesMeta": {
                 "typescript": {
@@ -4188,16 +4259,17 @@
             }
         },
         "node_modules/@typescript-eslint/scope-manager": {
-            "version": "6.7.5",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.5.tgz",
-            "integrity": "sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
+            "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/types": "6.7.5",
-                "@typescript-eslint/visitor-keys": "6.7.5"
+                "@typescript-eslint/types": "7.18.0",
+                "@typescript-eslint/visitor-keys": "7.18.0"
             },
             "engines": {
-                "node": "^16.0.0 || >=18.0.0"
+                "node": "^18.18.0 || >=20.0.0"
             },
             "funding": {
                 "type": "opencollective",
@@ -4205,25 +4277,26 @@
             }
         },
         "node_modules/@typescript-eslint/type-utils": {
-            "version": "6.7.5",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.5.tgz",
-            "integrity": "sha512-Gs0qos5wqxnQrvpYv+pf3XfcRXW6jiAn9zE/K+DlmYf6FcpxeNYN0AIETaPR7rHO4K2UY+D0CIbDP9Ut0U4m1g==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
+            "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/typescript-estree": "6.7.5",
-                "@typescript-eslint/utils": "6.7.5",
+                "@typescript-eslint/typescript-estree": "7.18.0",
+                "@typescript-eslint/utils": "7.18.0",
                 "debug": "^4.3.4",
-                "ts-api-utils": "^1.0.1"
+                "ts-api-utils": "^1.3.0"
             },
             "engines": {
-                "node": "^16.0.0 || >=18.0.0"
+                "node": "^18.18.0 || >=20.0.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/typescript-eslint"
             },
             "peerDependencies": {
-                "eslint": "^7.0.0 || ^8.0.0"
+                "eslint": "^8.56.0"
             },
             "peerDependenciesMeta": {
                 "typescript": {
@@ -4232,12 +4305,13 @@
             }
         },
         "node_modules/@typescript-eslint/types": {
-            "version": "6.7.5",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.5.tgz",
-            "integrity": "sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
+            "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
             "dev": true,
+            "license": "MIT",
             "engines": {
-                "node": "^16.0.0 || >=18.0.0"
+                "node": "^18.18.0 || >=20.0.0"
             },
             "funding": {
                 "type": "opencollective",
@@ -4245,21 +4319,23 @@
             }
         },
         "node_modules/@typescript-eslint/typescript-estree": {
-            "version": "6.7.5",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.5.tgz",
-            "integrity": "sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
+            "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
             "dev": true,
+            "license": "BSD-2-Clause",
             "dependencies": {
-                "@typescript-eslint/types": "6.7.5",
-                "@typescript-eslint/visitor-keys": "6.7.5",
+                "@typescript-eslint/types": "7.18.0",
+                "@typescript-eslint/visitor-keys": "7.18.0",
                 "debug": "^4.3.4",
                 "globby": "^11.1.0",
                 "is-glob": "^4.0.3",
-                "semver": "^7.5.4",
-                "ts-api-utils": "^1.0.1"
+                "minimatch": "^9.0.4",
+                "semver": "^7.6.0",
+                "ts-api-utils": "^1.3.0"
             },
             "engines": {
-                "node": "^16.0.0 || >=18.0.0"
+                "node": "^18.18.0 || >=20.0.0"
             },
             "funding": {
                 "type": "opencollective",
@@ -4271,42 +4347,67 @@
                 }
             }
         },
+        "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+            "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "balanced-match": "^1.0.0"
+            }
+        },
+        "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+            "version": "9.0.5",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+            "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
         "node_modules/@typescript-eslint/utils": {
-            "version": "6.7.5",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.5.tgz",
-            "integrity": "sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+            "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.4.0",
-                "@types/json-schema": "^7.0.12",
-                "@types/semver": "^7.5.0",
-                "@typescript-eslint/scope-manager": "6.7.5",
-                "@typescript-eslint/types": "6.7.5",
-                "@typescript-eslint/typescript-estree": "6.7.5",
-                "semver": "^7.5.4"
+                "@typescript-eslint/scope-manager": "7.18.0",
+                "@typescript-eslint/types": "7.18.0",
+                "@typescript-eslint/typescript-estree": "7.18.0"
             },
             "engines": {
-                "node": "^16.0.0 || >=18.0.0"
+                "node": "^18.18.0 || >=20.0.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/typescript-eslint"
             },
             "peerDependencies": {
-                "eslint": "^7.0.0 || ^8.0.0"
+                "eslint": "^8.56.0"
             }
         },
         "node_modules/@typescript-eslint/visitor-keys": {
-            "version": "6.7.5",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.5.tgz",
-            "integrity": "sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+            "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/types": "6.7.5",
-                "eslint-visitor-keys": "^3.4.1"
+                "@typescript-eslint/types": "7.18.0",
+                "eslint-visitor-keys": "^3.4.3"
             },
             "engines": {
-                "node": "^16.0.0 || >=18.0.0"
+                "node": "^18.18.0 || >=20.0.0"
             },
             "funding": {
                 "type": "opencollective",
@@ -4318,6 +4419,7 @@
             "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
             "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
             "dev": true,
+            "license": "Apache-2.0",
             "engines": {
                 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
             },
@@ -4325,135 +4427,102 @@
                 "url": "https://opencollective.com/eslint"
             }
         },
-        "node_modules/@vue/compiler-core": {
-            "version": "3.3.4",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
-            "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
+        "node_modules/@ungap/structured-clone": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+            "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
             "dev": true,
-            "optional": true,
+            "license": "ISC"
+        },
+        "node_modules/@vojtechlanka/vue-tags-input": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/@vojtechlanka/vue-tags-input/-/vue-tags-input-3.1.1.tgz",
+            "integrity": "sha512-GdREECH+k2pQCKdbHHh4/IxRXje3QQ8rXzXd9/6L1kzGYXqHlG1tbRoi1qC7enph67/g2nvGaZfpqLuuW+CX3g==",
+            "license": "MIT",
             "dependencies": {
-                "@babel/parser": "^7.21.3",
-                "@vue/shared": "3.3.4",
+                "fast-deep-equal": "^3.1.3",
+                "vue": "3.x",
+                "vuedraggable": "^4.1.0"
+            },
+            "peerDependencies": {
+                "vue": "3.x"
+            }
+        },
+        "node_modules/@vue/compiler-core": {
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
+            "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
+            "license": "MIT",
+            "dependencies": {
+                "@babel/parser": "^7.25.3",
+                "@vue/shared": "3.5.13",
+                "entities": "^4.5.0",
                 "estree-walker": "^2.0.2",
-                "source-map-js": "^1.0.2"
+                "source-map-js": "^1.2.0"
             }
         },
         "node_modules/@vue/compiler-dom": {
-            "version": "3.3.4",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
-            "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
-            "dev": true,
-            "optional": true,
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
+            "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
+            "license": "MIT",
             "dependencies": {
-                "@vue/compiler-core": "3.3.4",
-                "@vue/shared": "3.3.4"
+                "@vue/compiler-core": "3.5.13",
+                "@vue/shared": "3.5.13"
             }
         },
         "node_modules/@vue/compiler-sfc": {
-            "version": "3.3.4",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
-            "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
-            "dev": true,
-            "optional": true,
-            "dependencies": {
-                "@babel/parser": "^7.20.15",
-                "@vue/compiler-core": "3.3.4",
-                "@vue/compiler-dom": "3.3.4",
-                "@vue/compiler-ssr": "3.3.4",
-                "@vue/reactivity-transform": "3.3.4",
-                "@vue/shared": "3.3.4",
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
+            "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@babel/parser": "^7.25.3",
+                "@vue/compiler-core": "3.5.13",
+                "@vue/compiler-dom": "3.5.13",
+                "@vue/compiler-ssr": "3.5.13",
+                "@vue/shared": "3.5.13",
                 "estree-walker": "^2.0.2",
-                "magic-string": "^0.30.0",
-                "postcss": "^8.1.10",
-                "source-map-js": "^1.0.2"
+                "magic-string": "^0.30.11",
+                "postcss": "^8.4.48",
+                "source-map-js": "^1.2.0"
             }
         },
         "node_modules/@vue/compiler-ssr": {
-            "version": "3.3.4",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
-            "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
-            "dev": true,
-            "optional": true,
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
+            "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
+            "license": "MIT",
             "dependencies": {
-                "@vue/compiler-dom": "3.3.4",
-                "@vue/shared": "3.3.4"
+                "@vue/compiler-dom": "3.5.13",
+                "@vue/shared": "3.5.13"
             }
         },
-        "node_modules/@vue/component-compiler-utils": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz",
-            "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==",
+        "node_modules/@vue/devtools-api": {
+            "version": "6.6.4",
+            "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+            "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
             "dev": true,
-            "dependencies": {
-                "consolidate": "^0.15.1",
-                "hash-sum": "^1.0.2",
-                "lru-cache": "^4.1.2",
-                "merge-source-map": "^1.1.0",
-                "postcss": "^7.0.36",
-                "postcss-selector-parser": "^6.0.2",
-                "source-map": "~0.6.1",
-                "vue-template-es2015-compiler": "^1.9.0"
-            },
-            "optionalDependencies": {
-                "prettier": "^1.18.2 || ^2.0.0"
-            }
+            "license": "MIT"
         },
-        "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": {
-            "version": "4.1.5",
-            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
-            "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+        "node_modules/@vue/eslint-config-typescript": {
+            "version": "13.0.0",
+            "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz",
+            "integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "pseudomap": "^1.0.2",
-                "yallist": "^2.1.2"
-            }
-        },
-        "node_modules/@vue/component-compiler-utils/node_modules/picocolors": {
-            "version": "0.2.1",
-            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
-            "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
-            "dev": true
-        },
-        "node_modules/@vue/component-compiler-utils/node_modules/postcss": {
-            "version": "7.0.39",
-            "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
-            "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
-            "dev": true,
-            "dependencies": {
-                "picocolors": "^0.2.1",
-                "source-map": "^0.6.1"
-            },
-            "engines": {
-                "node": ">=6.0.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/postcss/"
-            }
-        },
-        "node_modules/@vue/component-compiler-utils/node_modules/yallist": {
-            "version": "2.1.2",
-            "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
-            "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
-            "dev": true
-        },
-        "node_modules/@vue/eslint-config-typescript": {
-            "version": "12.0.0",
-            "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz",
-            "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==",
-            "dev": true,
-            "dependencies": {
-                "@typescript-eslint/eslint-plugin": "^6.7.0",
-                "@typescript-eslint/parser": "^6.7.0",
+                "@typescript-eslint/eslint-plugin": "^7.1.1",
+                "@typescript-eslint/parser": "^7.1.1",
                 "vue-eslint-parser": "^9.3.1"
             },
             "engines": {
-                "node": "^14.17.0 || >=16.0.0"
+                "node": "^18.18.0 || >=20.0.0"
             },
             "peerDependencies": {
-                "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0",
+                "eslint": "^8.56.0",
                 "eslint-plugin-vue": "^9.0.0",
-                "typescript": "*"
+                "typescript": ">=4.7.4"
             },
             "peerDependenciesMeta": {
                 "typescript": {
@@ -4461,202 +4530,257 @@
                 }
             }
         },
-        "node_modules/@vue/reactivity-transform": {
-            "version": "3.3.4",
-            "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
-            "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
-            "dev": true,
-            "optional": true,
+        "node_modules/@vue/reactivity": {
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
+            "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
+            "license": "MIT",
             "dependencies": {
-                "@babel/parser": "^7.20.15",
-                "@vue/compiler-core": "3.3.4",
-                "@vue/shared": "3.3.4",
-                "estree-walker": "^2.0.2",
-                "magic-string": "^0.30.0"
+                "@vue/shared": "3.5.13"
+            }
+        },
+        "node_modules/@vue/runtime-core": {
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
+            "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
+            "license": "MIT",
+            "dependencies": {
+                "@vue/reactivity": "3.5.13",
+                "@vue/shared": "3.5.13"
+            }
+        },
+        "node_modules/@vue/runtime-dom": {
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
+            "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
+            "license": "MIT",
+            "dependencies": {
+                "@vue/reactivity": "3.5.13",
+                "@vue/runtime-core": "3.5.13",
+                "@vue/shared": "3.5.13",
+                "csstype": "^3.1.3"
+            }
+        },
+        "node_modules/@vue/server-renderer": {
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
+            "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
+            "license": "MIT",
+            "dependencies": {
+                "@vue/compiler-ssr": "3.5.13",
+                "@vue/shared": "3.5.13"
+            },
+            "peerDependencies": {
+                "vue": "3.5.13"
             }
         },
         "node_modules/@vue/shared": {
-            "version": "3.3.4",
-            "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-            "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-            "dev": true,
-            "optional": true
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
+            "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
+            "license": "MIT"
         },
         "node_modules/@webassemblyjs/ast": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
-            "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+            "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@webassemblyjs/helper-numbers": "1.11.6",
-                "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+                "@webassemblyjs/helper-numbers": "1.13.2",
+                "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
             }
         },
         "node_modules/@webassemblyjs/floating-point-hex-parser": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
-            "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
-            "dev": true
+            "version": "1.13.2",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+            "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/@webassemblyjs/helper-api-error": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
-            "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
-            "dev": true
+            "version": "1.13.2",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+            "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/@webassemblyjs/helper-buffer": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
-            "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==",
-            "dev": true
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+            "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/@webassemblyjs/helper-numbers": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
-            "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+            "version": "1.13.2",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+            "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@webassemblyjs/floating-point-hex-parser": "1.11.6",
-                "@webassemblyjs/helper-api-error": "1.11.6",
+                "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+                "@webassemblyjs/helper-api-error": "1.13.2",
                 "@xtuc/long": "4.2.2"
             }
         },
         "node_modules/@webassemblyjs/helper-wasm-bytecode": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
-            "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
-            "dev": true
+            "version": "1.13.2",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+            "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/@webassemblyjs/helper-wasm-section": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
-            "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+            "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@webassemblyjs/ast": "1.11.6",
-                "@webassemblyjs/helper-buffer": "1.11.6",
-                "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-                "@webassemblyjs/wasm-gen": "1.11.6"
+                "@webassemblyjs/ast": "1.14.1",
+                "@webassemblyjs/helper-buffer": "1.14.1",
+                "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+                "@webassemblyjs/wasm-gen": "1.14.1"
             }
         },
         "node_modules/@webassemblyjs/ieee754": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
-            "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+            "version": "1.13.2",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+            "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@xtuc/ieee754": "^1.2.0"
             }
         },
         "node_modules/@webassemblyjs/leb128": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
-            "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+            "version": "1.13.2",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+            "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
             "dev": true,
+            "license": "Apache-2.0",
             "dependencies": {
                 "@xtuc/long": "4.2.2"
             }
         },
         "node_modules/@webassemblyjs/utf8": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
-            "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
-            "dev": true
+            "version": "1.13.2",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+            "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/@webassemblyjs/wasm-edit": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
-            "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+            "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@webassemblyjs/ast": "1.11.6",
-                "@webassemblyjs/helper-buffer": "1.11.6",
-                "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-                "@webassemblyjs/helper-wasm-section": "1.11.6",
-                "@webassemblyjs/wasm-gen": "1.11.6",
-                "@webassemblyjs/wasm-opt": "1.11.6",
-                "@webassemblyjs/wasm-parser": "1.11.6",
-                "@webassemblyjs/wast-printer": "1.11.6"
+                "@webassemblyjs/ast": "1.14.1",
+                "@webassemblyjs/helper-buffer": "1.14.1",
+                "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+                "@webassemblyjs/helper-wasm-section": "1.14.1",
+                "@webassemblyjs/wasm-gen": "1.14.1",
+                "@webassemblyjs/wasm-opt": "1.14.1",
+                "@webassemblyjs/wasm-parser": "1.14.1",
+                "@webassemblyjs/wast-printer": "1.14.1"
             }
         },
         "node_modules/@webassemblyjs/wasm-gen": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
-            "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+            "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@webassemblyjs/ast": "1.11.6",
-                "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-                "@webassemblyjs/ieee754": "1.11.6",
-                "@webassemblyjs/leb128": "1.11.6",
-                "@webassemblyjs/utf8": "1.11.6"
+                "@webassemblyjs/ast": "1.14.1",
+                "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+                "@webassemblyjs/ieee754": "1.13.2",
+                "@webassemblyjs/leb128": "1.13.2",
+                "@webassemblyjs/utf8": "1.13.2"
             }
         },
         "node_modules/@webassemblyjs/wasm-opt": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
-            "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+            "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@webassemblyjs/ast": "1.11.6",
-                "@webassemblyjs/helper-buffer": "1.11.6",
-                "@webassemblyjs/wasm-gen": "1.11.6",
-                "@webassemblyjs/wasm-parser": "1.11.6"
+                "@webassemblyjs/ast": "1.14.1",
+                "@webassemblyjs/helper-buffer": "1.14.1",
+                "@webassemblyjs/wasm-gen": "1.14.1",
+                "@webassemblyjs/wasm-parser": "1.14.1"
             }
         },
         "node_modules/@webassemblyjs/wasm-parser": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
-            "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+            "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@webassemblyjs/ast": "1.11.6",
-                "@webassemblyjs/helper-api-error": "1.11.6",
-                "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
-                "@webassemblyjs/ieee754": "1.11.6",
-                "@webassemblyjs/leb128": "1.11.6",
-                "@webassemblyjs/utf8": "1.11.6"
+                "@webassemblyjs/ast": "1.14.1",
+                "@webassemblyjs/helper-api-error": "1.13.2",
+                "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+                "@webassemblyjs/ieee754": "1.13.2",
+                "@webassemblyjs/leb128": "1.13.2",
+                "@webassemblyjs/utf8": "1.13.2"
             }
         },
         "node_modules/@webassemblyjs/wast-printer": {
-            "version": "1.11.6",
-            "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
-            "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+            "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@webassemblyjs/ast": "1.11.6",
+                "@webassemblyjs/ast": "1.14.1",
                 "@xtuc/long": "4.2.2"
             }
         },
         "node_modules/@webpack-cli/configtest": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz",
-            "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==",
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
+            "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
             "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=14.15.0"
+            },
             "peerDependencies": {
-                "webpack": "4.x.x || 5.x.x",
-                "webpack-cli": "4.x.x"
+                "webpack": "5.x.x",
+                "webpack-cli": "5.x.x"
             }
         },
         "node_modules/@webpack-cli/info": {
-            "version": "1.5.0",
-            "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz",
-            "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==",
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
+            "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
             "dev": true,
-            "dependencies": {
-                "envinfo": "^7.7.3"
+            "license": "MIT",
+            "engines": {
+                "node": ">=14.15.0"
             },
             "peerDependencies": {
-                "webpack-cli": "4.x.x"
+                "webpack": "5.x.x",
+                "webpack-cli": "5.x.x"
             }
         },
         "node_modules/@webpack-cli/serve": {
-            "version": "1.7.0",
-            "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz",
-            "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==",
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
+            "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
             "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=14.15.0"
+            },
             "peerDependencies": {
-                "webpack-cli": "4.x.x"
+                "webpack": "5.x.x",
+                "webpack-cli": "5.x.x"
             },
             "peerDependenciesMeta": {
                 "webpack-dev-server": {
@@ -4668,13 +4792,15 @@
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
             "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
-            "dev": true
+            "dev": true,
+            "license": "BSD-3-Clause"
         },
         "node_modules/@xtuc/long": {
             "version": "4.2.2",
             "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
             "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
-            "dev": true
+            "dev": true,
+            "license": "Apache-2.0"
         },
         "node_modules/abab": {
             "version": "2.0.6",
@@ -4897,27 +5023,12 @@
                 "ajv": "^6.9.1"
             }
         },
-        "node_modules/alphanum-sort": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz",
-            "integrity": "sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==",
-            "dev": true
-        },
         "node_modules/altcha": {
             "version": "0.3.2",
             "resolved": "https://registry.npmjs.org/altcha/-/altcha-0.3.2.tgz",
             "integrity": "sha512-5UQP/fwgdlxfhgr4GADoPyMzHWTmDuWq3OloQlZsmUl3C/8+0huWdXW5S8FraA6GWK8iEwbG/2IR4TehLTY9cQ==",
             "dev": true
         },
-        "node_modules/ansi-colors": {
-            "version": "4.1.3",
-            "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
-            "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
-            "dev": true,
-            "engines": {
-                "node": ">=6"
-            }
-        },
         "node_modules/ansi-escapes": {
             "version": "4.3.2",
             "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -4942,18 +5053,6 @@
                 "node": ">=8"
             }
         },
-        "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==",
-            "dev": true,
-            "dependencies": {
-                "color-convert": "^1.9.0"
-            },
-            "engines": {
-                "node": ">=4"
-            }
-        },
         "node_modules/anymatch": {
             "version": "3.1.3",
             "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -4982,17 +5081,14 @@
             "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
             "dev": true
         },
-        "node_modules/array-buffer-byte-length": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
-            "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+        "node_modules/array-back": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
+            "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==",
             "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "is-array-buffer": "^3.0.1"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
+            "license": "MIT",
+            "engines": {
+                "node": ">=6"
             }
         },
         "node_modules/array-union": {
@@ -5004,25 +5100,6 @@
                 "node": ">=8"
             }
         },
-        "node_modules/array.prototype.reduce": {
-            "version": "1.0.5",
-            "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz",
-            "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.1.4",
-                "es-abstract": "^1.20.4",
-                "es-array-method-boxes-properly": "^1.0.0",
-                "is-string": "^1.0.7"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/asap": {
             "version": "2.0.6",
             "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
@@ -5037,15 +5114,6 @@
             "dev": true,
             "optional": true
         },
-        "node_modules/astral-regex": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
-            "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/asynckit": {
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -5065,9 +5133,9 @@
             }
         },
         "node_modules/autoprefixer": {
-            "version": "10.4.16",
-            "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
-            "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
+            "version": "10.4.20",
+            "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+            "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
             "dev": true,
             "funding": [
                 {
@@ -5083,12 +5151,13 @@
                     "url": "https://github.com/sponsors/ai"
                 }
             ],
+            "license": "MIT",
             "dependencies": {
-                "browserslist": "^4.21.10",
-                "caniuse-lite": "^1.0.30001538",
-                "fraction.js": "^4.3.6",
+                "browserslist": "^4.23.3",
+                "caniuse-lite": "^1.0.30001646",
+                "fraction.js": "^4.3.7",
                 "normalize-range": "^0.1.2",
-                "picocolors": "^1.0.0",
+                "picocolors": "^1.0.1",
                 "postcss-value-parser": "^4.2.0"
             },
             "bin": {
@@ -5101,18 +5170,6 @@
                 "postcss": "^8.1.0"
             }
         },
-        "node_modules/available-typed-arrays": {
-            "version": "1.0.5",
-            "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
-            "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
-            "dev": true,
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/axe-core": {
             "version": "4.7.2",
             "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz",
@@ -5195,15 +5252,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/babel-jest/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/babel-jest/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -5217,75 +5265,142 @@
             }
         },
         "node_modules/babel-loader": {
-            "version": "8.3.0",
-            "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz",
-            "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==",
+            "version": "9.2.1",
+            "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz",
+            "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "find-cache-dir": "^3.3.1",
-                "loader-utils": "^2.0.0",
-                "make-dir": "^3.1.0",
-                "schema-utils": "^2.6.5"
+                "find-cache-dir": "^4.0.0",
+                "schema-utils": "^4.0.0"
             },
             "engines": {
-                "node": ">= 8.9"
+                "node": ">= 14.15.0"
             },
             "peerDependencies": {
-                "@babel/core": "^7.0.0",
-                "webpack": ">=2"
+                "@babel/core": "^7.12.0",
+                "webpack": ">=5"
             }
         },
         "node_modules/babel-loader/node_modules/find-cache-dir": {
-            "version": "3.3.2",
-            "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
-            "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz",
+            "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "commondir": "^1.0.1",
-                "make-dir": "^3.0.2",
-                "pkg-dir": "^4.1.0"
+                "common-path-prefix": "^3.0.0",
+                "pkg-dir": "^7.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=14.16"
             },
             "funding": {
-                "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/babel-loader/node_modules/make-dir": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+        "node_modules/babel-loader/node_modules/find-up": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
+            "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "semver": "^6.0.0"
+                "locate-path": "^7.1.0",
+                "path-exists": "^5.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/babel-loader/node_modules/locate-path": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
+            "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "p-locate": "^6.0.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/babel-loader/node_modules/p-limit": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
+            "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "yocto-queue": "^1.0.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/babel-loader/node_modules/p-locate": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz",
+            "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "p-limit": "^4.0.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
             },
             "funding": {
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/babel-loader/node_modules/path-exists": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
+            "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            }
+        },
         "node_modules/babel-loader/node_modules/pkg-dir": {
-            "version": "4.2.0",
-            "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
-            "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz",
+            "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "find-up": "^4.0.0"
+                "find-up": "^6.3.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=14.16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/babel-loader/node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+        "node_modules/babel-loader/node_modules/yocto-queue": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
+            "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
             "dev": true,
-            "bin": {
-                "semver": "bin/semver.js"
+            "license": "MIT",
+            "engines": {
+                "node": ">=12.20"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
         "node_modules/babel-plugin-istanbul": {
@@ -5345,13 +5460,14 @@
             }
         },
         "node_modules/babel-plugin-polyfill-corejs2": {
-            "version": "0.4.5",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz",
-            "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==",
+            "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.4.2",
+                "@babel/helper-define-polyfill-provider": "^0.6.3",
                 "semver": "^6.3.1"
             },
             "peerDependencies": {
@@ -5363,30 +5479,33 @@
             "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
             "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
             "dev": true,
+            "license": "ISC",
             "bin": {
                 "semver": "bin/semver.js"
             }
         },
         "node_modules/babel-plugin-polyfill-corejs3": {
-            "version": "0.8.4",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz",
-            "integrity": "sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==",
+            "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": {
-                "@babel/helper-define-polyfill-provider": "^0.4.2",
-                "core-js-compat": "^3.32.2"
+                "@babel/helper-define-polyfill-provider": "^0.6.2",
+                "core-js-compat": "^3.38.0"
             },
             "peerDependencies": {
                 "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
             }
         },
         "node_modules/babel-plugin-polyfill-regenerator": {
-            "version": "0.5.2",
-            "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz",
-            "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==",
+            "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.4.2"
+                "@babel/helper-define-polyfill-provider": "^0.6.3"
             },
             "peerDependencies": {
                 "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -5498,12 +5617,6 @@
                 "node": ">=8"
             }
         },
-        "node_modules/bluebird": {
-            "version": "3.7.2",
-            "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
-            "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
-            "dev": true
-        },
         "node_modules/blueimp-canvas-to-blob": {
             "version": "3.29.0",
             "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
@@ -5571,9 +5684,9 @@
             }
         },
         "node_modules/browserslist": {
-            "version": "4.22.1",
-            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
-            "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+            "version": "4.24.2",
+            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
+            "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
             "dev": true,
             "funding": [
                 {
@@ -5589,11 +5702,12 @@
                     "url": "https://github.com/sponsors/ai"
                 }
             ],
+            "license": "MIT",
             "dependencies": {
-                "caniuse-lite": "^1.0.30001541",
-                "electron-to-chromium": "^1.4.535",
-                "node-releases": "^2.0.13",
-                "update-browserslist-db": "^1.0.13"
+                "caniuse-lite": "^1.0.30001669",
+                "electron-to-chromium": "^1.5.41",
+                "node-releases": "^2.0.18",
+                "update-browserslist-db": "^1.1.1"
             },
             "bin": {
                 "browserslist": "cli.js"
@@ -5711,6 +5825,7 @@
             "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
             "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
             "dev": true,
+            "optional": true,
             "dependencies": {
                 "function-bind": "^1.1.1",
                 "get-intrinsic": "^1.0.2"
@@ -5719,39 +5834,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/caller-callsite": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
-            "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==",
-            "dev": true,
-            "dependencies": {
-                "callsites": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/caller-callsite/node_modules/callsites": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
-            "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/caller-path": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
-            "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==",
-            "dev": true,
-            "dependencies": {
-                "caller-callsite": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=4"
-            }
-        },
         "node_modules/callsites": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -5832,20 +5914,6 @@
                 "node": ">=10.0.0"
             }
         },
-        "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==",
-            "dev": true,
-            "dependencies": {
-                "ansi-styles": "^3.2.1",
-                "escape-string-regexp": "^1.0.5",
-                "supports-color": "^5.3.0"
-            },
-            "engines": {
-                "node": ">=4"
-            }
-        },
         "node_modules/char-regex": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
@@ -6131,36 +6199,12 @@
                 "node": ">= 0.12.0"
             }
         },
-        "node_modules/coa": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
-            "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
-            "dev": true,
-            "dependencies": {
-                "@types/q": "^1.5.1",
-                "chalk": "^2.4.1",
-                "q": "^1.1.2"
-            },
-            "engines": {
-                "node": ">= 4.0"
-            }
-        },
         "node_modules/collect-v8-coverage": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
             "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
             "dev": true
         },
-        "node_modules/color": {
-            "version": "3.2.1",
-            "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
-            "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
-            "dev": true,
-            "dependencies": {
-                "color-convert": "^1.9.3",
-                "color-string": "^1.6.0"
-            }
-        },
         "node_modules/color-convert": {
             "version": "1.9.3",
             "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -6182,16 +6226,6 @@
             "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
             "dev": true
         },
-        "node_modules/color-string": {
-            "version": "1.9.1",
-            "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
-            "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
-            "dev": true,
-            "dependencies": {
-                "color-name": "^1.0.0",
-                "simple-swizzle": "^0.2.2"
-            }
-        },
         "node_modules/colord": {
             "version": "2.9.3",
             "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
@@ -6222,6 +6256,22 @@
                 "node": ">= 0.8"
             }
         },
+        "node_modules/command-line-args": {
+            "version": "5.2.1",
+            "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz",
+            "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "array-back": "^3.1.0",
+                "find-replace": "^3.0.0",
+                "lodash.camelcase": "^4.3.0",
+                "typical": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=4.0.0"
+            }
+        },
         "node_modules/commander": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -6231,6 +6281,13 @@
                 "node": ">= 10"
             }
         },
+        "node_modules/common-path-prefix": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
+            "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
+            "dev": true,
+            "license": "ISC"
+        },
         "node_modules/commondir": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -6243,19 +6300,6 @@
             "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
             "dev": true
         },
-        "node_modules/consolidate": {
-            "version": "0.15.1",
-            "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz",
-            "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==",
-            "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog",
-            "dev": true,
-            "dependencies": {
-                "bluebird": "^3.1.1"
-            },
-            "engines": {
-                "node": ">= 0.10.0"
-            }
-        },
         "node_modules/constantinople": {
             "version": "4.0.1",
             "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
@@ -6280,12 +6324,13 @@
             }
         },
         "node_modules/core-js-compat": {
-            "version": "3.33.0",
-            "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz",
-            "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==",
+            "version": "3.39.0",
+            "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
+            "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "browserslist": "^4.22.1"
+                "browserslist": "^4.24.2"
             },
             "funding": {
                 "type": "opencollective",
@@ -6378,15 +6423,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/create-jest/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/create-jest/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -6428,15 +6464,6 @@
                 "node": "*"
             }
         },
-        "node_modules/css-color-names": {
-            "version": "0.0.4",
-            "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
-            "integrity": "sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==",
-            "dev": true,
-            "engines": {
-                "node": "*"
-            }
-        },
         "node_modules/css-declaration-sorter": {
             "version": "6.4.0",
             "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz",
@@ -6460,971 +6487,670 @@
             }
         },
         "node_modules/css-loader": {
-            "version": "5.2.7",
-            "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz",
-            "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==",
+            "version": "7.1.2",
+            "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz",
+            "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "icss-utils": "^5.1.0",
-                "loader-utils": "^2.0.0",
-                "postcss": "^8.2.15",
-                "postcss-modules-extract-imports": "^3.0.0",
-                "postcss-modules-local-by-default": "^4.0.0",
-                "postcss-modules-scope": "^3.0.0",
+                "postcss": "^8.4.33",
+                "postcss-modules-extract-imports": "^3.1.0",
+                "postcss-modules-local-by-default": "^4.0.5",
+                "postcss-modules-scope": "^3.2.0",
                 "postcss-modules-values": "^4.0.0",
-                "postcss-value-parser": "^4.1.0",
-                "schema-utils": "^3.0.0",
-                "semver": "^7.3.5"
+                "postcss-value-parser": "^4.2.0",
+                "semver": "^7.5.4"
             },
             "engines": {
-                "node": ">= 10.13.0"
+                "node": ">= 18.12.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
             },
             "peerDependencies": {
-                "webpack": "^4.27.0 || ^5.0.0"
-            }
-        },
-        "node_modules/css-loader/node_modules/schema-utils": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-            "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
-            "dev": true,
-            "dependencies": {
-                "@types/json-schema": "^7.0.8",
-                "ajv": "^6.12.5",
-                "ajv-keywords": "^3.5.2"
+                "@rspack/core": "0.x || 1.x",
+                "webpack": "^5.27.0"
             },
-            "engines": {
-                "node": ">= 10.13.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/webpack"
+            "peerDependenciesMeta": {
+                "@rspack/core": {
+                    "optional": true
+                },
+                "webpack": {
+                    "optional": true
+                }
             }
         },
         "node_modules/css-minimizer-webpack-plugin": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-1.3.0.tgz",
-            "integrity": "sha512-jFa0Siplmfef4ndKglpVaduY47oHQwioAOEGK0f0vAX0s+vc+SmP6cCMoc+8Adau5600RnOEld5VVdC8CQau7w==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-7.0.0.tgz",
+            "integrity": "sha512-niy66jxsQHqO+EYbhPuIhqRQ1mNcNVUHrMnkzzir9kFOERJUaQDDRhh7dKDz33kBpkWMF9M8Vx0QlDbc5AHOsw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "cacache": "^15.0.5",
-                "cssnano": "^4.1.10",
-                "find-cache-dir": "^3.3.1",
-                "jest-worker": "^26.3.0",
-                "p-limit": "^3.0.2",
-                "schema-utils": "^3.0.0",
-                "serialize-javascript": "^5.0.1",
-                "source-map": "^0.6.1",
-                "webpack-sources": "^1.4.3"
+                "@jridgewell/trace-mapping": "^0.3.25",
+                "cssnano": "^7.0.1",
+                "jest-worker": "^29.7.0",
+                "postcss": "^8.4.38",
+                "schema-utils": "^4.2.0",
+                "serialize-javascript": "^6.0.2"
             },
             "engines": {
-                "node": ">= 10.13.0"
+                "node": ">= 18.12.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
             },
             "peerDependencies": {
-                "webpack": "^4.0.0 || ^5.0.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/cosmiconfig": {
-            "version": "5.2.1",
-            "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
-            "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
-            "dev": true,
-            "dependencies": {
-                "import-fresh": "^2.0.0",
-                "is-directory": "^0.3.1",
-                "js-yaml": "^3.13.1",
-                "parse-json": "^4.0.0"
+                "webpack": "^5.0.0"
             },
-            "engines": {
-                "node": ">=4"
+            "peerDependenciesMeta": {
+                "@parcel/css": {
+                    "optional": true
+                },
+                "@swc/css": {
+                    "optional": true
+                },
+                "clean-css": {
+                    "optional": true
+                },
+                "csso": {
+                    "optional": true
+                },
+                "esbuild": {
+                    "optional": true
+                },
+                "lightningcss": {
+                    "optional": true
+                }
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/css-declaration-sorter": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz",
-            "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==",
-            "dev": true,
-            "dependencies": {
-                "postcss": "^7.0.1",
-                "timsort": "^0.3.0"
-            },
-            "engines": {
-                "node": ">4"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/css-select": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
-            "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
-            "dev": true,
-            "dependencies": {
-                "boolbase": "^1.0.0",
-                "css-what": "^3.2.1",
-                "domutils": "^1.7.0",
-                "nth-check": "^1.0.2"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/css-tree": {
-            "version": "1.0.0-alpha.37",
-            "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
-            "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==",
-            "dev": true,
-            "dependencies": {
-                "mdn-data": "2.0.4",
-                "source-map": "^0.6.1"
-            },
-            "engines": {
-                "node": ">=8.0.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/css-what": {
-            "version": "3.4.2",
-            "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz",
-            "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==",
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz",
+            "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==",
             "dev": true,
+            "license": "ISC",
             "engines": {
-                "node": ">= 6"
+                "node": "^14 || ^16 || >=18"
             },
-            "funding": {
-                "url": "https://github.com/sponsors/fb55"
+            "peerDependencies": {
+                "postcss": "^8.0.9"
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano": {
-            "version": "4.1.11",
-            "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz",
-            "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==",
-            "dev": true,
-            "dependencies": {
-                "cosmiconfig": "^5.0.0",
-                "cssnano-preset-default": "^4.0.8",
-                "is-resolvable": "^1.0.0",
-                "postcss": "^7.0.0"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano-preset-default": {
-            "version": "4.0.8",
-            "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz",
-            "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==",
-            "dev": true,
-            "dependencies": {
-                "css-declaration-sorter": "^4.0.1",
-                "cssnano-util-raw-cache": "^4.0.1",
-                "postcss": "^7.0.0",
-                "postcss-calc": "^7.0.1",
-                "postcss-colormin": "^4.0.3",
-                "postcss-convert-values": "^4.0.1",
-                "postcss-discard-comments": "^4.0.2",
-                "postcss-discard-duplicates": "^4.0.2",
-                "postcss-discard-empty": "^4.0.1",
-                "postcss-discard-overridden": "^4.0.1",
-                "postcss-merge-longhand": "^4.0.11",
-                "postcss-merge-rules": "^4.0.3",
-                "postcss-minify-font-values": "^4.0.2",
-                "postcss-minify-gradients": "^4.0.2",
-                "postcss-minify-params": "^4.0.2",
-                "postcss-minify-selectors": "^4.0.2",
-                "postcss-normalize-charset": "^4.0.1",
-                "postcss-normalize-display-values": "^4.0.2",
-                "postcss-normalize-positions": "^4.0.2",
-                "postcss-normalize-repeat-style": "^4.0.2",
-                "postcss-normalize-string": "^4.0.2",
-                "postcss-normalize-timing-functions": "^4.0.2",
-                "postcss-normalize-unicode": "^4.0.1",
-                "postcss-normalize-url": "^4.0.1",
-                "postcss-normalize-whitespace": "^4.0.2",
-                "postcss-ordered-values": "^4.1.2",
-                "postcss-reduce-initial": "^4.0.3",
-                "postcss-reduce-transforms": "^4.0.2",
-                "postcss-svgo": "^4.0.3",
-                "postcss-unique-selectors": "^4.0.1"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/dom-serializer": {
-            "version": "0.2.2",
-            "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
-            "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
-            "dev": true,
-            "dependencies": {
-                "domelementtype": "^2.0.1",
-                "entities": "^2.0.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/domutils": {
-            "version": "1.7.0",
-            "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
-            "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
-            "dev": true,
-            "dependencies": {
-                "dom-serializer": "0",
-                "domelementtype": "1"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/domutils/node_modules/domelementtype": {
-            "version": "1.3.1",
-            "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
-            "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
-            "dev": true
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/entities": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
-            "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
-            "dev": true,
-            "funding": {
-                "url": "https://github.com/fb55/entities?sponsor=1"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/find-cache-dir": {
-            "version": "3.3.2",
-            "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
-            "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+            "version": "7.0.6",
+            "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.6.tgz",
+            "integrity": "sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "commondir": "^1.0.1",
-                "make-dir": "^3.0.2",
-                "pkg-dir": "^4.1.0"
+                "cssnano-preset-default": "^7.0.6",
+                "lilconfig": "^3.1.2"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
             },
             "funding": {
-                "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/import-fresh": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
-            "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==",
-            "dev": true,
-            "dependencies": {
-                "caller-path": "^2.0.0",
-                "resolve-from": "^3.0.0"
+                "type": "opencollective",
+                "url": "https://opencollective.com/cssnano"
             },
-            "engines": {
-                "node": ">=4"
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/make-dir": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+        "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano-preset-default": {
+            "version": "7.0.6",
+            "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.6.tgz",
+            "integrity": "sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "browserslist": "^4.23.3",
+                "css-declaration-sorter": "^7.2.0",
+                "cssnano-utils": "^5.0.0",
+                "postcss-calc": "^10.0.2",
+                "postcss-colormin": "^7.0.2",
+                "postcss-convert-values": "^7.0.4",
+                "postcss-discard-comments": "^7.0.3",
+                "postcss-discard-duplicates": "^7.0.1",
+                "postcss-discard-empty": "^7.0.0",
+                "postcss-discard-overridden": "^7.0.0",
+                "postcss-merge-longhand": "^7.0.4",
+                "postcss-merge-rules": "^7.0.4",
+                "postcss-minify-font-values": "^7.0.0",
+                "postcss-minify-gradients": "^7.0.0",
+                "postcss-minify-params": "^7.0.2",
+                "postcss-minify-selectors": "^7.0.4",
+                "postcss-normalize-charset": "^7.0.0",
+                "postcss-normalize-display-values": "^7.0.0",
+                "postcss-normalize-positions": "^7.0.0",
+                "postcss-normalize-repeat-style": "^7.0.0",
+                "postcss-normalize-string": "^7.0.0",
+                "postcss-normalize-timing-functions": "^7.0.0",
+                "postcss-normalize-unicode": "^7.0.2",
+                "postcss-normalize-url": "^7.0.0",
+                "postcss-normalize-whitespace": "^7.0.0",
+                "postcss-ordered-values": "^7.0.1",
+                "postcss-reduce-initial": "^7.0.2",
+                "postcss-reduce-transforms": "^7.0.0",
+                "postcss-svgo": "^7.0.1",
+                "postcss-unique-selectors": "^7.0.3"
+            },
+            "engines": {
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
+            }
+        },
+        "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano-utils": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz",
+            "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==",
             "dev": true,
-            "dependencies": {
-                "semver": "^6.0.0"
-            },
+            "license": "MIT",
             "engines": {
-                "node": ">=8"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
             },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/mdn-data": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
-            "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
-            "dev": true
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/mkdirp": {
-            "version": "0.5.6",
-            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
-            "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+        "node_modules/css-minimizer-webpack-plugin/node_modules/jest-worker": {
+            "version": "29.7.0",
+            "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
+            "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "minimist": "^1.2.6"
+                "@types/node": "*",
+                "jest-util": "^29.7.0",
+                "merge-stream": "^2.0.0",
+                "supports-color": "^8.0.0"
             },
-            "bin": {
-                "mkdirp": "bin/cmd.js"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/normalize-url": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz",
-            "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==",
-            "dev": true,
             "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/nth-check": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
-            "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
-            "dev": true,
-            "dependencies": {
-                "boolbase": "~1.0.0"
+                "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/parse-json": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
-            "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
+        "node_modules/css-minimizer-webpack-plugin/node_modules/lilconfig": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+            "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
             "dev": true,
-            "dependencies": {
-                "error-ex": "^1.3.1",
-                "json-parse-better-errors": "^1.0.1"
-            },
+            "license": "MIT",
             "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/picocolors": {
-            "version": "0.2.1",
-            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
-            "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
-            "dev": true
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/pkg-dir": {
-            "version": "4.2.0",
-            "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
-            "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
-            "dev": true,
-            "dependencies": {
-                "find-up": "^4.0.0"
+                "node": ">=14"
             },
-            "engines": {
-                "node": ">=8"
+            "funding": {
+                "url": "https://github.com/sponsors/antonk52"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss": {
-            "version": "7.0.39",
-            "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
-            "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
+        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-calc": {
+            "version": "10.0.2",
+            "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.2.tgz",
+            "integrity": "sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "picocolors": "^0.2.1",
-                "source-map": "^0.6.1"
+                "postcss-selector-parser": "^6.1.2",
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.0.0"
+                "node": "^18.12 || ^20.9 || >=22.0"
             },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/postcss/"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-calc": {
-            "version": "7.0.5",
-            "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz",
-            "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==",
-            "dev": true,
-            "dependencies": {
-                "postcss": "^7.0.27",
-                "postcss-selector-parser": "^6.0.2",
-                "postcss-value-parser": "^4.0.2"
+            "peerDependencies": {
+                "postcss": "^8.4.38"
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-colormin": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz",
-            "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==",
+            "version": "7.0.2",
+            "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz",
+            "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "browserslist": "^4.0.0",
-                "color": "^3.0.0",
-                "has": "^1.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "browserslist": "^4.23.3",
+                "caniuse-api": "^3.0.0",
+                "colord": "^2.9.3",
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-colormin/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-convert-values": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz",
-            "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==",
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.4.tgz",
+            "integrity": "sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "browserslist": "^4.23.3",
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-convert-values/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-comments": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz",
-            "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==",
+            "version": "7.0.3",
+            "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.3.tgz",
+            "integrity": "sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "postcss": "^7.0.0"
+                "postcss-selector-parser": "^6.1.2"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-duplicates": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz",
-            "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==",
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz",
+            "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==",
             "dev": true,
-            "dependencies": {
-                "postcss": "^7.0.0"
-            },
+            "license": "MIT",
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-empty": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz",
-            "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz",
+            "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==",
             "dev": true,
-            "dependencies": {
-                "postcss": "^7.0.0"
-            },
+            "license": "MIT",
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-overridden": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz",
-            "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz",
+            "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==",
             "dev": true,
-            "dependencies": {
-                "postcss": "^7.0.0"
-            },
+            "license": "MIT",
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-merge-longhand": {
-            "version": "4.0.11",
-            "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz",
-            "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==",
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.4.tgz",
+            "integrity": "sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "css-color-names": "0.0.4",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0",
-                "stylehacks": "^4.0.0"
+                "postcss-value-parser": "^4.2.0",
+                "stylehacks": "^7.0.4"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-merge-longhand/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-merge-rules": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz",
-            "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==",
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.4.tgz",
+            "integrity": "sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "browserslist": "^4.0.0",
+                "browserslist": "^4.23.3",
                 "caniuse-api": "^3.0.0",
-                "cssnano-util-same-parent": "^4.0.0",
-                "postcss": "^7.0.0",
-                "postcss-selector-parser": "^3.0.0",
-                "vendors": "^1.0.0"
+                "cssnano-utils": "^5.0.0",
+                "postcss-selector-parser": "^6.1.2"
             },
             "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
-            "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
-            "dev": true,
-            "dependencies": {
-                "dot-prop": "^5.2.0",
-                "indexes-of": "^1.0.1",
-                "uniq": "^1.0.1"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
             },
-            "engines": {
-                "node": ">=8"
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-font-values": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz",
-            "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz",
+            "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-font-values/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-gradients": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz",
-            "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz",
+            "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "cssnano-util-get-arguments": "^4.0.0",
-                "is-color-stop": "^1.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "colord": "^2.9.3",
+                "cssnano-utils": "^5.0.0",
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-gradients/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-params": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz",
-            "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==",
+            "version": "7.0.2",
+            "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz",
+            "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "alphanum-sort": "^1.0.0",
-                "browserslist": "^4.0.0",
-                "cssnano-util-get-arguments": "^4.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0",
-                "uniqs": "^2.0.0"
+                "browserslist": "^4.23.3",
+                "cssnano-utils": "^5.0.0",
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-params/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-selectors": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz",
-            "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==",
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.4.tgz",
+            "integrity": "sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "alphanum-sort": "^1.0.0",
-                "has": "^1.0.0",
-                "postcss": "^7.0.0",
-                "postcss-selector-parser": "^3.0.0"
+                "cssesc": "^3.0.0",
+                "postcss-selector-parser": "^6.1.2"
             },
             "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
-            "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
-            "dev": true,
-            "dependencies": {
-                "dot-prop": "^5.2.0",
-                "indexes-of": "^1.0.1",
-                "uniq": "^1.0.1"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
             },
-            "engines": {
-                "node": ">=8"
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-charset": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz",
-            "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz",
+            "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==",
             "dev": true,
-            "dependencies": {
-                "postcss": "^7.0.0"
-            },
+            "license": "MIT",
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-display-values": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz",
-            "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz",
+            "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "cssnano-util-get-match": "^4.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-display-values/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-positions": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz",
-            "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz",
+            "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "cssnano-util-get-arguments": "^4.0.0",
-                "has": "^1.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-positions/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-repeat-style": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz",
-            "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz",
+            "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "cssnano-util-get-arguments": "^4.0.0",
-                "cssnano-util-get-match": "^4.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-repeat-style/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-string": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz",
-            "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz",
+            "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "has": "^1.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-string/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-timing-functions": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz",
-            "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz",
+            "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "cssnano-util-get-match": "^4.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-timing-functions/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-unicode": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz",
-            "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==",
+            "version": "7.0.2",
+            "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz",
+            "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "browserslist": "^4.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "browserslist": "^4.23.3",
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-unicode/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-url": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz",
-            "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz",
+            "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "is-absolute-url": "^2.0.0",
-                "normalize-url": "^3.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-url/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-whitespace": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz",
-            "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz",
+            "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-whitespace/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-ordered-values": {
-            "version": "4.1.2",
-            "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz",
-            "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==",
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz",
+            "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "cssnano-util-get-arguments": "^4.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "cssnano-utils": "^5.0.0",
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-ordered-values/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
         "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-reduce-initial": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz",
-            "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==",
+            "version": "7.0.2",
+            "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz",
+            "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "browserslist": "^4.0.0",
-                "caniuse-api": "^3.0.0",
-                "has": "^1.0.0",
-                "postcss": "^7.0.0"
+                "browserslist": "^4.23.3",
+                "caniuse-api": "^3.0.0"
             },
             "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-reduce-transforms": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz",
-            "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==",
-            "dev": true,
-            "dependencies": {
-                "cssnano-util-get-match": "^4.0.0",
-                "has": "^1.0.0",
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
             },
-            "engines": {
-                "node": ">=6.9.0"
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-reduce-transforms/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-svgo": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz",
-            "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==",
+        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-reduce-transforms": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz",
+            "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "postcss": "^7.0.0",
-                "postcss-value-parser": "^3.0.0",
-                "svgo": "^1.0.0"
+                "postcss-value-parser": "^4.2.0"
             },
             "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-svgo/node_modules/postcss-value-parser": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-            "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-            "dev": true
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-unique-selectors": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz",
-            "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==",
-            "dev": true,
-            "dependencies": {
-                "alphanum-sort": "^1.0.0",
-                "postcss": "^7.0.0",
-                "uniqs": "^2.0.0"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
             },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/resolve-from": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
-            "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==",
-            "dev": true,
-            "engines": {
-                "node": ">=4"
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-            "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-svgo": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz",
+            "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@types/json-schema": "^7.0.8",
-                "ajv": "^6.12.5",
-                "ajv-keywords": "^3.5.2"
+                "postcss-value-parser": "^4.2.0",
+                "svgo": "^3.3.2"
             },
             "engines": {
-                "node": ">= 10.13.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/webpack"
-            }
-        },
-        "node_modules/css-minimizer-webpack-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/css-minimizer-webpack-plugin/node_modules/stylehacks": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
-            "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==",
-            "dev": true,
-            "dependencies": {
-                "browserslist": "^4.0.0",
-                "postcss": "^7.0.0",
-                "postcss-selector-parser": "^3.0.0"
+                "node": "^18.12.0 || ^20.9.0 || >= 18"
             },
-            "engines": {
-                "node": ">=6.9.0"
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/stylehacks/node_modules/postcss-selector-parser": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz",
-            "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==",
+        "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-unique-selectors": {
+            "version": "7.0.3",
+            "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.3.tgz",
+            "integrity": "sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "dot-prop": "^5.2.0",
-                "indexes-of": "^1.0.1",
-                "uniq": "^1.0.1"
+                "postcss-selector-parser": "^6.1.2"
             },
             "engines": {
-                "node": ">=8"
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/svgo": {
-            "version": "1.3.2",
-            "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
-            "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==",
-            "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.",
+        "node_modules/css-minimizer-webpack-plugin/node_modules/serialize-javascript": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+            "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
             "dev": true,
+            "license": "BSD-3-Clause",
             "dependencies": {
-                "chalk": "^2.4.1",
-                "coa": "^2.0.2",
-                "css-select": "^2.0.0",
-                "css-select-base-adapter": "^0.1.1",
-                "css-tree": "1.0.0-alpha.37",
-                "csso": "^4.0.2",
-                "js-yaml": "^3.13.1",
-                "mkdirp": "~0.5.1",
-                "object.values": "^1.1.0",
-                "sax": "~1.2.4",
-                "stable": "^0.1.8",
-                "unquote": "~1.1.1",
-                "util.promisify": "~1.0.0"
-            },
-            "bin": {
-                "svgo": "bin/svgo"
-            },
-            "engines": {
-                "node": ">=4.0.0"
+                "randombytes": "^2.1.0"
             }
         },
-        "node_modules/css-minimizer-webpack-plugin/node_modules/webpack-sources": {
-            "version": "1.4.3",
-            "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
-            "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==",
+        "node_modules/css-minimizer-webpack-plugin/node_modules/stylehacks": {
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz",
+            "integrity": "sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "source-list-map": "^2.0.0",
-                "source-map": "~0.6.1"
+                "browserslist": "^4.23.3",
+                "postcss-selector-parser": "^6.1.2"
+            },
+            "engines": {
+                "node": "^18.12.0 || ^20.9.0 || >=22.0"
+            },
+            "peerDependencies": {
+                "postcss": "^8.4.31"
             }
         },
         "node_modules/css-select": {
@@ -7443,11 +7169,12 @@
                 "url": "https://github.com/sponsors/fb55"
             }
         },
-        "node_modules/css-select-base-adapter": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
-            "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
-            "dev": true
+        "node_modules/css-selector-parser": {
+            "version": "1.4.1",
+            "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz",
+            "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/css-tree": {
             "version": "1.1.3",
@@ -7550,68 +7277,6 @@
                 "postcss": "^8.2.15"
             }
         },
-        "node_modules/cssnano-util-get-arguments": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz",
-            "integrity": "sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==",
-            "dev": true,
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/cssnano-util-get-match": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz",
-            "integrity": "sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==",
-            "dev": true,
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/cssnano-util-raw-cache": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz",
-            "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==",
-            "dev": true,
-            "dependencies": {
-                "postcss": "^7.0.0"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/cssnano-util-raw-cache/node_modules/picocolors": {
-            "version": "0.2.1",
-            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
-            "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
-            "dev": true
-        },
-        "node_modules/cssnano-util-raw-cache/node_modules/postcss": {
-            "version": "7.0.39",
-            "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
-            "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
-            "dev": true,
-            "dependencies": {
-                "picocolors": "^0.2.1",
-                "source-map": "^0.6.1"
-            },
-            "engines": {
-                "node": ">=6.0.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/postcss/"
-            }
-        },
-        "node_modules/cssnano-util-same-parent": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz",
-            "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==",
-            "dev": true,
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
         "node_modules/cssnano-utils": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
@@ -7661,10 +7326,10 @@
             "dev": true
         },
         "node_modules/csstype": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
-            "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
-            "dev": true
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+            "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+            "license": "MIT"
         },
         "node_modules/data-urls": {
             "version": "3.0.2",
@@ -7680,12 +7345,6 @@
                 "node": ">=12"
             }
         },
-        "node_modules/de-indent": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
-            "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
-            "dev": true
-        },
         "node_modules/debug": {
             "version": "4.3.4",
             "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -7738,22 +7397,6 @@
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/define-properties": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
-            "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
-            "dev": true,
-            "dependencies": {
-                "has-property-descriptors": "^1.0.0",
-                "object-keys": "^1.1.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/del": {
             "version": "5.1.0",
             "resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz",
@@ -7943,18 +7586,6 @@
                 "url": "https://github.com/fb55/domutils?sponsor=1"
             }
         },
-        "node_modules/dot-prop": {
-            "version": "5.3.0",
-            "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
-            "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
-            "dev": true,
-            "dependencies": {
-                "is-obj": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/dotenv": {
             "version": "16.3.1",
             "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
@@ -7967,6 +7598,13 @@
                 "url": "https://github.com/motdotla/dotenv?sponsor=1"
             }
         },
+        "node_modules/eastasianwidth": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+            "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/easygettext": {
             "version": "2.17.0",
             "resolved": "https://registry.npmjs.org/easygettext/-/easygettext-2.17.0.tgz",
@@ -7997,10 +7635,11 @@
             }
         },
         "node_modules/electron-to-chromium": {
-            "version": "1.4.544",
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.544.tgz",
-            "integrity": "sha512-54z7squS1FyFRSUqq/knOFSptjjogLZXbKcYk3B0qkE1KZzvqASwRZnY2KzZQJqIYLVD38XZeoiMRflYSwyO4w==",
-            "dev": true
+            "version": "1.5.68",
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz",
+            "integrity": "sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ==",
+            "dev": true,
+            "license": "ISC"
         },
         "node_modules/emittery": {
             "version": "0.13.1",
@@ -8030,10 +7669,11 @@
             }
         },
         "node_modules/enhanced-resolve": {
-            "version": "5.15.0",
-            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
-            "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+            "version": "5.17.1",
+            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
+            "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "graceful-fs": "^4.2.4",
                 "tapable": "^2.2.0"
@@ -8042,23 +7682,10 @@
                 "node": ">=10.13.0"
             }
         },
-        "node_modules/enquirer": {
-            "version": "2.3.6",
-            "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
-            "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
-            "dev": true,
-            "dependencies": {
-                "ansi-colors": "^4.1.1"
-            },
-            "engines": {
-                "node": ">=8.6"
-            }
-        },
         "node_modules/entities": {
             "version": "4.5.0",
             "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
             "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
-            "dev": true,
             "engines": {
                 "node": ">=0.12"
             },
@@ -8066,11 +7693,22 @@
                 "url": "https://github.com/fb55/entities?sponsor=1"
             }
         },
+        "node_modules/env-paths": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+            "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=6"
+            }
+        },
         "node_modules/envinfo": {
-            "version": "7.10.0",
-            "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz",
-            "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==",
+            "version": "7.14.0",
+            "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
+            "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
             "dev": true,
+            "license": "MIT",
             "bin": {
                 "envinfo": "dist/cli.js"
             },
@@ -8087,97 +7725,12 @@
                 "is-arrayish": "^0.2.1"
             }
         },
-        "node_modules/es-abstract": {
-            "version": "1.21.2",
-            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
-            "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==",
-            "dev": true,
-            "dependencies": {
-                "array-buffer-byte-length": "^1.0.0",
-                "available-typed-arrays": "^1.0.5",
-                "call-bind": "^1.0.2",
-                "es-set-tostringtag": "^2.0.1",
-                "es-to-primitive": "^1.2.1",
-                "function.prototype.name": "^1.1.5",
-                "get-intrinsic": "^1.2.0",
-                "get-symbol-description": "^1.0.0",
-                "globalthis": "^1.0.3",
-                "gopd": "^1.0.1",
-                "has": "^1.0.3",
-                "has-property-descriptors": "^1.0.0",
-                "has-proto": "^1.0.1",
-                "has-symbols": "^1.0.3",
-                "internal-slot": "^1.0.5",
-                "is-array-buffer": "^3.0.2",
-                "is-callable": "^1.2.7",
-                "is-negative-zero": "^2.0.2",
-                "is-regex": "^1.1.4",
-                "is-shared-array-buffer": "^1.0.2",
-                "is-string": "^1.0.7",
-                "is-typed-array": "^1.1.10",
-                "is-weakref": "^1.0.2",
-                "object-inspect": "^1.12.3",
-                "object-keys": "^1.1.1",
-                "object.assign": "^4.1.4",
-                "regexp.prototype.flags": "^1.4.3",
-                "safe-regex-test": "^1.0.0",
-                "string.prototype.trim": "^1.2.7",
-                "string.prototype.trimend": "^1.0.6",
-                "string.prototype.trimstart": "^1.0.6",
-                "typed-array-length": "^1.0.4",
-                "unbox-primitive": "^1.0.2",
-                "which-typed-array": "^1.1.9"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/es-array-method-boxes-properly": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
-            "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
-            "dev": true
-        },
         "node_modules/es-module-lexer": {
             "version": "1.3.0",
             "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
             "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==",
             "dev": true
         },
-        "node_modules/es-set-tostringtag": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
-            "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
-            "dev": true,
-            "dependencies": {
-                "get-intrinsic": "^1.1.3",
-                "has": "^1.0.3",
-                "has-tostringtag": "^1.0.0"
-            },
-            "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==",
-            "dev": true,
-            "dependencies": {
-                "is-callable": "^1.1.4",
-                "is-date-object": "^1.0.1",
-                "is-symbol": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/es6-promise": {
             "version": "4.2.8",
             "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
@@ -8185,23 +7738,15 @@
             "dev": true
         },
         "node_modules/escalade": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-            "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+            "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=6"
             }
         },
-        "node_modules/escape-string-regexp": {
-            "version": "1.0.5",
-            "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-            "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.8.0"
-            }
-        },
         "node_modules/escodegen": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
@@ -8233,251 +7778,174 @@
             }
         },
         "node_modules/eslint": {
-            "version": "7.32.0",
-            "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
-            "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
-            "dev": true,
-            "dependencies": {
-                "@babel/code-frame": "7.12.11",
-                "@eslint/eslintrc": "^0.4.3",
-                "@humanwhocodes/config-array": "^0.5.0",
-                "ajv": "^6.10.0",
+            "version": "8.57.1",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+            "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@eslint-community/eslint-utils": "^4.2.0",
+                "@eslint-community/regexpp": "^4.6.1",
+                "@eslint/eslintrc": "^2.1.4",
+                "@eslint/js": "8.57.1",
+                "@humanwhocodes/config-array": "^0.13.0",
+                "@humanwhocodes/module-importer": "^1.0.1",
+                "@nodelib/fs.walk": "^1.2.8",
+                "@ungap/structured-clone": "^1.2.0",
+                "ajv": "^6.12.4",
                 "chalk": "^4.0.0",
                 "cross-spawn": "^7.0.2",
-                "debug": "^4.0.1",
+                "debug": "^4.3.2",
                 "doctrine": "^3.0.0",
-                "enquirer": "^2.3.5",
                 "escape-string-regexp": "^4.0.0",
-                "eslint-scope": "^5.1.1",
-                "eslint-utils": "^2.1.0",
-                "eslint-visitor-keys": "^2.0.0",
-                "espree": "^7.3.1",
-                "esquery": "^1.4.0",
+                "eslint-scope": "^7.2.2",
+                "eslint-visitor-keys": "^3.4.3",
+                "espree": "^9.6.1",
+                "esquery": "^1.4.2",
                 "esutils": "^2.0.2",
                 "fast-deep-equal": "^3.1.3",
                 "file-entry-cache": "^6.0.1",
-                "functional-red-black-tree": "^1.0.1",
-                "glob-parent": "^5.1.2",
-                "globals": "^13.6.0",
-                "ignore": "^4.0.6",
-                "import-fresh": "^3.0.0",
+                "find-up": "^5.0.0",
+                "glob-parent": "^6.0.2",
+                "globals": "^13.19.0",
+                "graphemer": "^1.4.0",
+                "ignore": "^5.2.0",
                 "imurmurhash": "^0.1.4",
                 "is-glob": "^4.0.0",
-                "js-yaml": "^3.13.1",
+                "is-path-inside": "^3.0.3",
+                "js-yaml": "^4.1.0",
                 "json-stable-stringify-without-jsonify": "^1.0.1",
                 "levn": "^0.4.1",
                 "lodash.merge": "^4.6.2",
-                "minimatch": "^3.0.4",
+                "minimatch": "^3.1.2",
                 "natural-compare": "^1.4.0",
-                "optionator": "^0.9.1",
-                "progress": "^2.0.0",
-                "regexpp": "^3.1.0",
-                "semver": "^7.2.1",
-                "strip-ansi": "^6.0.0",
-                "strip-json-comments": "^3.1.0",
-                "table": "^6.0.9",
-                "text-table": "^0.2.0",
-                "v8-compile-cache": "^2.0.3"
+                "optionator": "^0.9.3",
+                "strip-ansi": "^6.0.1",
+                "text-table": "^0.2.0"
             },
             "bin": {
                 "eslint": "bin/eslint.js"
             },
             "engines": {
-                "node": "^10.12.0 || >=12.0.0"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
             },
             "funding": {
                 "url": "https://opencollective.com/eslint"
             }
         },
         "node_modules/eslint-plugin-vue": {
-            "version": "9.17.0",
-            "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.17.0.tgz",
-            "integrity": "sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==",
+            "version": "9.28.0",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz",
+            "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.4.0",
+                "globals": "^13.24.0",
                 "natural-compare": "^1.4.0",
                 "nth-check": "^2.1.1",
-                "postcss-selector-parser": "^6.0.13",
-                "semver": "^7.5.4",
-                "vue-eslint-parser": "^9.3.1",
+                "postcss-selector-parser": "^6.0.15",
+                "semver": "^7.6.3",
+                "vue-eslint-parser": "^9.4.3",
                 "xml-name-validator": "^4.0.0"
             },
             "engines": {
                 "node": "^14.17.0 || >=16.0.0"
             },
             "peerDependencies": {
-                "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0"
-            }
-        },
-        "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/eslint-utils": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
-            "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
-            "dev": true,
-            "dependencies": {
-                "eslint-visitor-keys": "^1.1.0"
-            },
-            "engines": {
-                "node": ">=6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/mysticatea"
-            }
-        },
-        "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
-            "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/eslint-visitor-keys": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
-            "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
-            "dev": true,
-            "engines": {
-                "node": ">=10"
+                "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
             }
         },
-        "node_modules/eslint-webpack-plugin": {
-            "version": "3.2.0",
-            "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz",
-            "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==",
+        "node_modules/eslint-plugin-vue/node_modules/globals": {
+            "version": "13.24.0",
+            "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+            "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@types/eslint": "^7.29.0 || ^8.4.1",
-                "jest-worker": "^28.0.2",
-                "micromatch": "^4.0.5",
-                "normalize-path": "^3.0.0",
-                "schema-utils": "^4.0.0"
+                "type-fest": "^0.20.2"
             },
             "engines": {
-                "node": ">= 12.13.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/webpack"
-            },
-            "peerDependencies": {
-                "eslint": "^7.0.0 || ^8.0.0",
-                "webpack": "^5.0.0"
-            }
-        },
-        "node_modules/eslint-webpack-plugin/node_modules/ajv": {
-            "version": "8.12.0",
-            "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
-            "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
-            "dev": true,
-            "dependencies": {
-                "fast-deep-equal": "^3.1.1",
-                "json-schema-traverse": "^1.0.0",
-                "require-from-string": "^2.0.2",
-                "uri-js": "^4.2.2"
+                "node": ">=8"
             },
             "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/epoberezkin"
-            }
-        },
-        "node_modules/eslint-webpack-plugin/node_modules/ajv-keywords": {
-            "version": "5.1.0",
-            "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
-            "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
-            "dev": true,
-            "dependencies": {
-                "fast-deep-equal": "^3.1.3"
-            },
-            "peerDependencies": {
-                "ajv": "^8.8.2"
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/eslint-webpack-plugin/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-plugin-vue/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,
+            "license": "(MIT OR CC0-1.0)",
             "engines": {
-                "node": ">=8"
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/eslint-webpack-plugin/node_modules/jest-worker": {
-            "version": "28.1.3",
-            "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz",
-            "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==",
+        "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": {
-                "@types/node": "*",
-                "merge-stream": "^2.0.0",
-                "supports-color": "^8.0.0"
+                "esrecurse": "^4.3.0",
+                "estraverse": "^4.1.1"
             },
             "engines": {
-                "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+                "node": ">=8.0.0"
             }
         },
-        "node_modules/eslint-webpack-plugin/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
+        "node_modules/eslint-visitor-keys": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+            "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            }
         },
-        "node_modules/eslint-webpack-plugin/node_modules/schema-utils": {
+        "node_modules/eslint-webpack-plugin": {
             "version": "4.2.0",
-            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
-            "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
+            "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.2.0.tgz",
+            "integrity": "sha512-rsfpFQ01AWQbqtjgPRr2usVRxhWDuG0YDYcG8DJOteD3EFnpeuYuOwk0PQiN7PRBTqS6ElNdtPZPggj8If9WnA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@types/json-schema": "^7.0.9",
-                "ajv": "^8.9.0",
-                "ajv-formats": "^2.1.1",
-                "ajv-keywords": "^5.1.0"
+                "@types/eslint": "^8.56.10",
+                "jest-worker": "^29.7.0",
+                "micromatch": "^4.0.5",
+                "normalize-path": "^3.0.0",
+                "schema-utils": "^4.2.0"
             },
             "engines": {
-                "node": ">= 12.13.0"
+                "node": ">= 14.15.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
+            },
+            "peerDependencies": {
+                "eslint": "^8.0.0 || ^9.0.0",
+                "webpack": "^5.0.0"
             }
         },
-        "node_modules/eslint-webpack-plugin/node_modules/supports-color": {
-            "version": "8.1.1",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-            "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+        "node_modules/eslint-webpack-plugin/node_modules/jest-worker": {
+            "version": "29.7.0",
+            "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
+            "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "has-flag": "^4.0.0"
+                "@types/node": "*",
+                "jest-util": "^29.7.0",
+                "merge-stream": "^2.0.0",
+                "supports-color": "^8.0.0"
             },
             "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/supports-color?sponsor=1"
-            }
-        },
-        "node_modules/eslint/node_modules/@babel/code-frame": {
-            "version": "7.12.11",
-            "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
-            "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
-            "dev": true,
-            "dependencies": {
-                "@babel/highlight": "^7.10.4"
+                "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
             }
         },
         "node_modules/eslint/node_modules/ansi-styles": {
@@ -8495,6 +7963,13 @@
                 "url": "https://github.com/chalk/ansi-styles?sponsor=1"
             }
         },
+        "node_modules/eslint/node_modules/argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+            "dev": true,
+            "license": "Python-2.0"
+        },
         "node_modules/eslint/node_modules/chalk": {
             "version": "4.1.2",
             "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -8535,6 +8010,76 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "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,
+            "license": "BSD-2-Clause",
+            "dependencies": {
+                "esrecurse": "^4.3.0",
+                "estraverse": "^5.2.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/eslint/node_modules/eslint-visitor-keys": {
+            "version": "3.4.3",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+            "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/eslint/node_modules/estraverse": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+            "dev": true,
+            "license": "BSD-2-Clause",
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/eslint/node_modules/find-up": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "locate-path": "^6.0.0",
+                "path-exists": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/eslint/node_modules/glob-parent": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+            "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "is-glob": "^4.0.3"
+            },
+            "engines": {
+                "node": ">=10.13.0"
+            }
+        },
         "node_modules/eslint/node_modules/globals": {
             "version": "13.20.0",
             "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
@@ -8550,22 +8095,49 @@
                 "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/js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "argparse": "^2.0.1"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
+            }
+        },
+        "node_modules/eslint/node_modules/locate-path": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
             "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "p-locate": "^5.0.0"
+            },
             "engines": {
-                "node": ">=8"
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/eslint/node_modules/ignore": {
-            "version": "4.0.6",
-            "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
-            "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+        "node_modules/eslint/node_modules/p-locate": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
             "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "p-limit": "^3.0.2"
+            },
             "engines": {
-                "node": ">= 4"
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
         "node_modules/eslint/node_modules/supports-color": {
@@ -8593,26 +8165,47 @@
             }
         },
         "node_modules/espree": {
-            "version": "7.3.1",
-            "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
-            "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
+            "version": "9.6.1",
+            "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+            "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
             "dev": true,
+            "license": "BSD-2-Clause",
             "dependencies": {
-                "acorn": "^7.4.0",
-                "acorn-jsx": "^5.3.1",
-                "eslint-visitor-keys": "^1.3.0"
+                "acorn": "^8.9.0",
+                "acorn-jsx": "^5.3.2",
+                "eslint-visitor-keys": "^3.4.1"
             },
             "engines": {
-                "node": "^10.12.0 || >=12.0.0"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/espree/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",
+            "bin": {
+                "acorn": "bin/acorn"
+            },
+            "engines": {
+                "node": ">=0.4.0"
             }
         },
         "node_modules/espree/node_modules/eslint-visitor-keys": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
-            "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+            "version": "3.4.3",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+            "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
             "dev": true,
+            "license": "Apache-2.0",
             "engines": {
-                "node": ">=4"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
             }
         },
         "node_modules/esprima": {
@@ -8682,8 +8275,7 @@
         "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
+            "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
         },
         "node_modules/esutils": {
             "version": "2.0.3",
@@ -8752,48 +8344,26 @@
             }
         },
         "node_modules/expose-loader": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-1.0.1.tgz",
-            "integrity": "sha512-FcxYU+tfzik+0Ve6Ymw8lKJNWhMQpuJOEkLhz0fGIgfDdFKxkr+BSSTkw+MRkK51hemQ+1eR7c2H46E5CxtBZw==",
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz",
+            "integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==",
             "dev": true,
-            "dependencies": {
-                "loader-utils": "^2.0.0",
-                "schema-utils": "^3.0.0"
-            },
+            "license": "MIT",
             "engines": {
-                "node": ">= 10.13.0"
+                "node": ">= 18.12.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
             },
             "peerDependencies": {
-                "webpack": "^4.0.0 || ^5.0.0"
-            }
-        },
-        "node_modules/expose-loader/node_modules/schema-utils": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-            "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
-            "dev": true,
-            "dependencies": {
-                "@types/json-schema": "^7.0.8",
-                "ajv": "^6.12.5",
-                "ajv-keywords": "^3.5.2"
-            },
-            "engines": {
-                "node": ">= 10.13.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/webpack"
+                "webpack": "^5.0.0"
             }
         },
         "node_modules/fast-deep-equal": {
             "version": "3.1.3",
             "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-            "dev": true
+            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
         },
         "node_modules/fast-glob": {
             "version": "3.3.0",
@@ -8823,6 +8393,13 @@
             "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
             "dev": true
         },
+        "node_modules/fast-uri": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
+            "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==",
+            "dev": true,
+            "license": "BSD-3-Clause"
+        },
         "node_modules/fastest-levenshtein": {
             "version": "1.0.16",
             "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
@@ -8906,6 +8483,19 @@
                 "node": ">=6"
             }
         },
+        "node_modules/find-replace": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
+            "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "array-back": "^3.0.1"
+            },
+            "engines": {
+                "node": ">=4.0.0"
+            }
+        },
         "node_modules/find-up": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -8919,6 +8509,16 @@
                 "node": ">=8"
             }
         },
+        "node_modules/flat": {
+            "version": "5.0.2",
+            "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+            "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+            "dev": true,
+            "license": "BSD-3-Clause",
+            "bin": {
+                "flat": "cli.js"
+            }
+        },
         "node_modules/flat-cache": {
             "version": "3.0.4",
             "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -8978,23 +8578,24 @@
             }
         },
         "node_modules/focus-trap": {
-            "version": "6.9.4",
-            "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz",
-            "integrity": "sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==",
+            "version": "7.6.2",
+            "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.2.tgz",
+            "integrity": "sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w==",
             "dev": true,
-            "peer": true,
+            "license": "MIT",
             "dependencies": {
-                "tabbable": "^5.3.3"
+                "tabbable": "^6.2.0"
             }
         },
         "node_modules/focus-trap-vue": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/focus-trap-vue/-/focus-trap-vue-1.1.1.tgz",
-            "integrity": "sha512-N+M4d4uYymCogct417gUL7wWSMIW/oUcCicfg3eRdo+gz7jlQnIGwUwViFxPkKV7iyzpc81g6JeSxRWiYWU3eQ==",
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/focus-trap-vue/-/focus-trap-vue-4.0.3.tgz",
+            "integrity": "sha512-cIX5rybkCAlNZ4IHYJ3nCFIsipDDljJHHjtTO2IgYWkVYg7X9ipUVdab3HzYp88kmHgMwjcB71LYnXRRsF6ZqQ==",
             "dev": true,
+            "license": "MIT",
             "peerDependencies": {
-                "focus-trap": "^6.0.1",
-                "vue": "^2.6.0"
+                "focus-trap": "^7.0.0",
+                "vue": "^3.0.0"
             }
         },
         "node_modules/follow-redirects": {
@@ -9017,13 +8618,34 @@
                 }
             }
         },
-        "node_modules/for-each": {
-            "version": "0.3.3",
-            "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
-            "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+        "node_modules/foreground-child": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
+            "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
             "dev": true,
+            "license": "ISC",
             "dependencies": {
-                "is-callable": "^1.1.3"
+                "cross-spawn": "^7.0.0",
+                "signal-exit": "^4.0.1"
+            },
+            "engines": {
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/foreground-child/node_modules/signal-exit": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+            "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+            "dev": true,
+            "license": "ISC",
+            "engines": {
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
         "node_modules/form-data": {
@@ -9041,10 +8663,11 @@
             }
         },
         "node_modules/fraction.js": {
-            "version": "4.3.6",
-            "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz",
-            "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==",
+            "version": "4.3.7",
+            "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+            "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": "*"
             },
@@ -9105,39 +8728,6 @@
             "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
             "dev": true
         },
-        "node_modules/function.prototype.name": {
-            "version": "1.1.5",
-            "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
-            "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.1.3",
-                "es-abstract": "^1.19.0",
-                "functions-have-names": "^1.2.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/functional-red-black-tree": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
-            "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
-            "dev": true
-        },
-        "node_modules/functions-have-names": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
-            "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
-            "dev": true,
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/gensync": {
             "version": "1.0.0-beta.2",
             "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -9161,6 +8751,7 @@
             "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
             "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
             "dev": true,
+            "optional": true,
             "dependencies": {
                 "function-bind": "^1.1.1",
                 "has": "^1.0.3",
@@ -9192,22 +8783,38 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/get-symbol-description": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
-            "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+        "node_modules/gettext-extractor": {
+            "version": "3.8.0",
+            "resolved": "https://registry.npmjs.org/gettext-extractor/-/gettext-extractor-3.8.0.tgz",
+            "integrity": "sha512-i/3mDQufQoJd2/EKm/B+VlaYrt3yGjVfLZu8DQpESKH29klNiW6z2S89FVCIEB85bDNgtGCeM/3A/yR1njr/Lw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "call-bind": "^1.0.2",
-                "get-intrinsic": "^1.1.1"
+                "@types/glob": "5 - 7",
+                "@types/parse5": "^5",
+                "css-selector-parser": "^1.3",
+                "glob": "5 - 7",
+                "parse5": "5 - 6",
+                "pofile": "1.0.x",
+                "typescript": "4 - 5"
             },
             "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
+                "node": ">=6"
             }
         },
+        "node_modules/gettext-extractor/node_modules/parse5": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+            "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/gettext-extractor/node_modules/pofile": {
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/pofile/-/pofile-1.0.11.tgz",
+            "integrity": "sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==",
+            "dev": true
+        },
         "node_modules/glob": {
             "version": "7.2.3",
             "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -9255,26 +8862,12 @@
                 "node": ">=4"
             }
         },
-        "node_modules/globalthis": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
-            "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
-            "dev": true,
-            "dependencies": {
-                "define-properties": "^1.1.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/globby": {
             "version": "11.1.0",
             "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
             "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "array-union": "^2.1.0",
                 "dir-glob": "^3.0.1",
@@ -9290,18 +8883,6 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/gopd": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
-            "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
-            "dev": true,
-            "dependencies": {
-                "get-intrinsic": "^1.1.3"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/graceful-fs": {
             "version": "4.2.11",
             "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -9312,7 +8893,8 @@
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
             "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-            "dev": true
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/growly": {
             "version": "1.3.0",
@@ -9332,34 +8914,14 @@
                 "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==",
-            "dev": true,
-            "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==",
+            "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": ">=4"
-            }
-        },
-        "node_modules/has-property-descriptors": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
-            "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
-            "dev": true,
-            "dependencies": {
-                "get-intrinsic": "^1.1.1"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
+                "node": ">=8"
             }
         },
         "node_modules/has-proto": {
@@ -9367,6 +8929,7 @@
             "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
             "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
             "dev": true,
+            "optional": true,
             "engines": {
                 "node": ">= 0.4"
             },
@@ -9379,6 +8942,7 @@
             "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
             "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
             "dev": true,
+            "optional": true,
             "engines": {
                 "node": ">= 0.4"
             },
@@ -9391,6 +8955,7 @@
             "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
             "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
             "dev": true,
+            "optional": true,
             "dependencies": {
                 "has-symbols": "^1.0.2"
             },
@@ -9402,25 +8967,11 @@
             }
         },
         "node_modules/hash-sum": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
-            "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
-            "dev": true
-        },
-        "node_modules/he": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
-            "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
+            "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
             "dev": true,
-            "bin": {
-                "he": "bin/he"
-            }
-        },
-        "node_modules/hex-color-regex": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
-            "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==",
-            "dev": true
+            "license": "MIT"
         },
         "node_modules/highlight.js": {
             "version": "10.5.0",
@@ -9431,18 +8982,6 @@
                 "node": "*"
             }
         },
-        "node_modules/hsl-regex": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz",
-            "integrity": "sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==",
-            "dev": true
-        },
-        "node_modules/hsla-regex": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz",
-            "integrity": "sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==",
-            "dev": true
-        },
         "node_modules/html-encoding-sniffer": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
@@ -9575,10 +9114,11 @@
             ]
         },
         "node_modules/ignore": {
-            "version": "5.2.4",
-            "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
-            "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+            "version": "5.3.2",
+            "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+            "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">= 4"
             }
@@ -9660,12 +9200,6 @@
                 "node": ">=8"
             }
         },
-        "node_modules/indexes-of": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
-            "integrity": "sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==",
-            "dev": true
-        },
         "node_modules/infer-owner": {
             "version": "1.0.4",
             "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
@@ -9688,20 +9222,6 @@
             "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
             "dev": true
         },
-        "node_modules/internal-slot": {
-            "version": "1.0.5",
-            "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
-            "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==",
-            "dev": true,
-            "dependencies": {
-                "get-intrinsic": "^1.2.0",
-                "has": "^1.0.3",
-                "side-channel": "^1.0.4"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
         "node_modules/interpret": {
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
@@ -9711,47 +9231,12 @@
                 "node": ">= 0.10"
             }
         },
-        "node_modules/is-absolute-url": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz",
-            "integrity": "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-array-buffer": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
-            "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "get-intrinsic": "^1.2.0",
-                "is-typed-array": "^1.1.10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/is-arrayish": {
             "version": "0.2.1",
             "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
             "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
             "dev": true
         },
-        "node_modules/is-bigint": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
-            "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
-            "dev": true,
-            "dependencies": {
-                "has-bigints": "^1.0.1"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/is-binary-path": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -9764,54 +9249,12 @@
                 "node": ">=8"
             }
         },
-        "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==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "has-tostringtag": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/is-buffer": {
             "version": "1.1.6",
             "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
             "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
             "dev": true
         },
-        "node_modules/is-callable": {
-            "version": "1.2.7",
-            "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
-            "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
-            "dev": true,
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-color-stop": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz",
-            "integrity": "sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==",
-            "dev": true,
-            "dependencies": {
-                "css-color-names": "^0.0.4",
-                "hex-color-regex": "^1.1.0",
-                "hsl-regex": "^1.0.0",
-                "hsla-regex": "^1.0.0",
-                "rgb-regex": "^1.0.1",
-                "rgba-regex": "^1.0.0"
-            }
-        },
         "node_modules/is-core-module": {
             "version": "2.12.1",
             "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
@@ -9824,30 +9267,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "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==",
-            "dev": true,
-            "dependencies": {
-                "has-tostringtag": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-directory": {
-            "version": "0.3.1",
-            "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
-            "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/is-docker": {
             "version": "2.2.1",
             "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
@@ -9922,18 +9341,6 @@
                 "node": ">=8"
             }
         },
-        "node_modules/is-negative-zero": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
-            "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
-            "dev": true,
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/is-number": {
             "version": "7.0.0",
             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -9943,30 +9350,6 @@
                 "node": ">=0.12.0"
             }
         },
-        "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==",
-            "dev": true,
-            "dependencies": {
-                "has-tostringtag": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-obj": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
-            "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/is-path-cwd": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
@@ -9981,121 +9364,43 @@
             "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
             "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
             "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/is-plain-object": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
-            "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
-            "dev": true,
-            "dependencies": {
-                "isobject": "^3.0.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-potential-custom-element-name": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
-            "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
-            "dev": true
-        },
-        "node_modules/is-promise": {
-            "version": "2.2.2",
-            "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
-            "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
-            "dev": true,
-            "optional": true
-        },
-        "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==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "has-tostringtag": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-resolvable": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
-            "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
-            "dev": true
-        },
-        "node_modules/is-shared-array-buffer": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
-            "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-stream": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
-            "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/is-string": {
-            "version": "1.0.7",
-            "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
-            "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
-            "dev": true,
-            "dependencies": {
-                "has-tostringtag": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
+            "engines": {
+                "node": ">=8"
             }
         },
-        "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==",
+        "node_modules/is-plain-object": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+            "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
             "dev": true,
             "dependencies": {
-                "has-symbols": "^1.0.2"
+                "isobject": "^3.0.1"
             },
             "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
+                "node": ">=0.10.0"
             }
         },
-        "node_modules/is-typed-array": {
-            "version": "1.1.10",
-            "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
-            "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+        "node_modules/is-potential-custom-element-name": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+            "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+            "dev": true
+        },
+        "node_modules/is-promise": {
+            "version": "2.2.2",
+            "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+            "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
+            "dev": true,
+            "optional": true
+        },
+        "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==",
             "dev": true,
+            "optional": true,
             "dependencies": {
-                "available-typed-arrays": "^1.0.5",
                 "call-bind": "^1.0.2",
-                "for-each": "^0.3.3",
-                "gopd": "^1.0.1",
                 "has-tostringtag": "^1.0.0"
             },
             "engines": {
@@ -10105,16 +9410,16 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "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==",
+        "node_modules/is-stream": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+            "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
             "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2"
+            "engines": {
+                "node": ">=8"
             },
             "funding": {
-                "url": "https://github.com/sponsors/ljharb"
+                "url": "https://github.com/sponsors/sindresorhus"
             }
         },
         "node_modules/is-wsl": {
@@ -10189,15 +9494,6 @@
                 "node": ">=10"
             }
         },
-        "node_modules/istanbul-lib-report/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/istanbul-lib-report/node_modules/make-dir": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@@ -10252,6 +9548,25 @@
                 "node": ">=8"
             }
         },
+        "node_modules/jackspeak": {
+            "version": "3.4.0",
+            "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz",
+            "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==",
+            "dev": true,
+            "license": "BlueOak-1.0.0",
+            "dependencies": {
+                "@isaacs/cliui": "^8.0.2"
+            },
+            "engines": {
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            },
+            "optionalDependencies": {
+                "@pkgjs/parseargs": "^0.11.0"
+            }
+        },
         "node_modules/javascript-stringify": {
             "version": "1.6.0",
             "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-1.6.0.tgz",
@@ -10372,15 +9687,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-circus/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-circus/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -10469,15 +9775,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-cli/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-cli/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -10578,15 +9875,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-config/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-config/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -10657,15 +9945,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-diff/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-diff/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -10749,15 +10028,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-each/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-each/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -10848,15 +10118,6 @@
                 "fsevents": "^2.3.2"
             }
         },
-        "node_modules/jest-haste-map/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-haste-map/node_modules/jest-worker": {
             "version": "29.7.0",
             "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
@@ -10872,21 +10133,6 @@
                 "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
             }
         },
-        "node_modules/jest-haste-map/node_modules/supports-color": {
-            "version": "8.1.1",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-            "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
-            "dev": true,
-            "dependencies": {
-                "has-flag": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/supports-color?sponsor=1"
-            }
-        },
         "node_modules/jest-junit": {
             "version": "16.0.0",
             "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz",
@@ -10973,15 +10219,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-matcher-utils/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-matcher-utils/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11057,15 +10294,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-message-util/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-message-util/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11194,15 +10422,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-resolve/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-resolve/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11290,15 +10509,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-runner/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-runner/node_modules/jest-worker": {
             "version": "29.7.0",
             "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
@@ -11427,15 +10637,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-runtime/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-runtime/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11522,15 +10723,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-snapshot/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-snapshot/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11603,15 +10795,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-util/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-util/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11696,15 +10879,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-validate/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-validate/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11779,15 +10953,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/jest-watcher/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-watcher/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11814,15 +10979,6 @@
                 "node": ">= 10.13.0"
             }
         },
-        "node_modules/jest-worker/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/jest-worker/node_modules/supports-color": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11835,6 +10991,16 @@
                 "node": ">=8"
             }
         },
+        "node_modules/jiti": {
+            "version": "1.21.6",
+            "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
+            "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
+            "dev": true,
+            "license": "MIT",
+            "bin": {
+                "jiti": "bin/jiti.js"
+            }
+        },
         "node_modules/jquery": {
             "version": "3.5.1",
             "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
@@ -11897,7 +11063,8 @@
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
             "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-            "dev": true
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/js-yaml": {
             "version": "3.14.1",
@@ -11970,23 +11137,18 @@
             }
         },
         "node_modules/jsesc": {
-            "version": "2.5.2",
-            "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-            "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+            "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,
+            "license": "MIT",
             "bin": {
                 "jsesc": "bin/jsesc"
             },
             "engines": {
-                "node": ">=4"
+                "node": ">=6"
             }
         },
-        "node_modules/json-parse-better-errors": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
-            "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
-            "dev": true
-        },
         "node_modules/json-parse-even-better-errors": {
             "version": "2.3.1",
             "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -12187,11 +11349,19 @@
             "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
             "dev": true
         },
+        "node_modules/lodash.camelcase": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+            "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/lodash.debounce": {
             "version": "4.0.8",
             "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
             "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
-            "dev": true
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/lodash.memoize": {
             "version": "4.1.2",
@@ -12211,12 +11381,6 @@
             "integrity": "sha512-kn1IDX0aHfg0FsnPIyxCHTamZXt3YK3aExRH1LW8YhzP6+sCldTm8+E4aIg+nSmM6R4eqdWGrXWtfYI961bwIw==",
             "dev": true
         },
-        "node_modules/lodash.truncate": {
-            "version": "4.4.2",
-            "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
-            "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
-            "dev": true
-        },
         "node_modules/lodash.uniq": {
             "version": "4.5.0",
             "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -12228,21 +11392,18 @@
             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
             "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
             "dev": true,
+            "license": "ISC",
             "dependencies": {
                 "yallist": "^3.0.2"
             }
         },
         "node_modules/magic-string": {
-            "version": "0.30.1",
-            "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz",
-            "integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==",
-            "dev": true,
-            "optional": true,
+            "version": "0.30.11",
+            "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
+            "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+            "license": "MIT",
             "dependencies": {
-                "@jridgewell/sourcemap-codec": "^1.4.15"
-            },
-            "engines": {
-                "node": ">=12"
+                "@jridgewell/sourcemap-codec": "^1.5.0"
             }
         },
         "node_modules/make-dir": {
@@ -12293,15 +11454,6 @@
             "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
             "dev": true
         },
-        "node_modules/merge-source-map": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz",
-            "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==",
-            "dev": true,
-            "dependencies": {
-                "source-map": "^0.6.1"
-            }
-        },
         "node_modules/merge-stream": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -12359,54 +11511,26 @@
             "engines": {
                 "node": ">=6"
             }
-        },
-        "node_modules/mini-css-extract-plugin": {
-            "version": "1.3.1",
-            "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.1.tgz",
-            "integrity": "sha512-jIOheqh9EU98rqj6ZaFTYNNDSFqdakNqaUZfkYwaXPjI9batmXVXX+K71NrqRAgtoGefELBMld1EQ7dqSAD5SQ==",
-            "dev": true,
-            "dependencies": {
-                "loader-utils": "^2.0.0",
-                "schema-utils": "^3.0.0",
-                "webpack-sources": "^1.1.0"
-            },
-            "engines": {
-                "node": ">= 10.13.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/webpack"
-            },
-            "peerDependencies": {
-                "webpack": "^4.4.0 || ^5.0.0"
-            }
-        },
-        "node_modules/mini-css-extract-plugin/node_modules/schema-utils": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-            "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+        },
+        "node_modules/mini-css-extract-plugin": {
+            "version": "2.9.2",
+            "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz",
+            "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@types/json-schema": "^7.0.8",
-                "ajv": "^6.12.5",
-                "ajv-keywords": "^3.5.2"
+                "schema-utils": "^4.0.0",
+                "tapable": "^2.2.1"
             },
             "engines": {
-                "node": ">= 10.13.0"
+                "node": ">= 12.13.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
-            }
-        },
-        "node_modules/mini-css-extract-plugin/node_modules/webpack-sources": {
-            "version": "1.4.3",
-            "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
-            "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==",
-            "dev": true,
-            "dependencies": {
-                "source-list-map": "^2.0.0",
-                "source-map": "~0.6.1"
+            },
+            "peerDependencies": {
+                "webpack": "^5.0.0"
             }
         },
         "node_modules/minimatch": {
@@ -12504,10 +11628,11 @@
             "dev": true
         },
         "node_modules/mitt": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz",
-            "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==",
-            "dev": true
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+            "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/mkdirp": {
             "version": "1.0.4",
@@ -12552,16 +11677,16 @@
             }
         },
         "node_modules/nanoid": {
-            "version": "3.3.6",
-            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
-            "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
-            "dev": true,
+            "version": "3.3.7",
+            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+            "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
             "funding": [
                 {
                     "type": "github",
                     "url": "https://github.com/sponsors/ai"
                 }
             ],
+            "license": "MIT",
             "bin": {
                 "nanoid": "bin/nanoid.cjs"
             },
@@ -12611,10 +11736,11 @@
             }
         },
         "node_modules/node-releases": {
-            "version": "2.0.13",
-            "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
-            "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
-            "dev": true
+            "version": "2.0.18",
+            "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+            "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/normalize-path": {
             "version": "3.0.0",
@@ -12686,78 +11812,6 @@
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/object-inspect": {
-            "version": "1.12.3",
-            "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
-            "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
-            "dev": true,
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/object-keys": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
-            "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-            "dev": true,
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/object.assign": {
-            "version": "4.1.4",
-            "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
-            "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.1.4",
-                "has-symbols": "^1.0.3",
-                "object-keys": "^1.1.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/object.getownpropertydescriptors": {
-            "version": "2.1.6",
-            "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz",
-            "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==",
-            "dev": true,
-            "dependencies": {
-                "array.prototype.reduce": "^1.0.5",
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.2.0",
-                "es-abstract": "^1.21.2",
-                "safe-array-concat": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/object.values": {
-            "version": "1.1.6",
-            "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
-            "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.1.4",
-                "es-abstract": "^1.20.4"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/once": {
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -12965,6 +12019,43 @@
             "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
             "dev": true
         },
+        "node_modules/path-scurry": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+            "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+            "dev": true,
+            "license": "BlueOak-1.0.0",
+            "dependencies": {
+                "lru-cache": "^10.2.0",
+                "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.18"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/path-scurry/node_modules/lru-cache": {
+            "version": "10.2.2",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
+            "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
+            "dev": true,
+            "license": "ISC",
+            "engines": {
+                "node": "14 || >=16.14"
+            }
+        },
+        "node_modules/path-scurry/node_modules/minipass": {
+            "version": "7.1.2",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+            "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+            "dev": true,
+            "license": "ISC",
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
+            }
+        },
         "node_modules/path-type": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -13000,10 +12091,10 @@
             "optional": true
         },
         "node_modules/picocolors": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-            "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
-            "dev": true
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+            "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+            "license": "ISC"
         },
         "node_modules/picomatch": {
             "version": "2.3.1",
@@ -13026,6 +12117,60 @@
                 "node": ">=6"
             }
         },
+        "node_modules/pinia": {
+            "version": "2.2.8",
+            "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.8.tgz",
+            "integrity": "sha512-NRTYy2g+kju5tBRe0oNlriZIbMNvma8ZJrpHsp3qudyiMEA8jMmPPKQ2QMHg0Oc4BkUyQYWagACabrwriCK9HQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@vue/devtools-api": "^6.6.3",
+                "vue-demi": "^0.14.10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/posva"
+            },
+            "peerDependencies": {
+                "@vue/composition-api": "^1.4.0",
+                "typescript": ">=4.4.4",
+                "vue": "^2.6.14 || ^3.5.11"
+            },
+            "peerDependenciesMeta": {
+                "@vue/composition-api": {
+                    "optional": true
+                },
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/pinia/node_modules/vue-demi": {
+            "version": "0.14.10",
+            "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+            "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+            "dev": true,
+            "hasInstallScript": true,
+            "license": "MIT",
+            "bin": {
+                "vue-demi-fix": "bin/vue-demi-fix.js",
+                "vue-demi-switch": "bin/vue-demi-switch.js"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            },
+            "peerDependencies": {
+                "@vue/composition-api": "^1.0.0-rc.1",
+                "vue": "^3.0.0-0 || ^2.6.0"
+            },
+            "peerDependenciesMeta": {
+                "@vue/composition-api": {
+                    "optional": true
+                }
+            }
+        },
         "node_modules/pirates": {
             "version": "4.0.6",
             "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -13145,19 +12290,27 @@
             "dev": true
         },
         "node_modules/portal-vue": {
-            "version": "2.1.7",
-            "resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-2.1.7.tgz",
-            "integrity": "sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g==",
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-3.0.0.tgz",
+            "integrity": "sha512-9eprMxNURLx6ijbcgkWjYNcTWJYu/H8QF8nyAeBzOmk9lKCea01BW1hYBeLkgz+AestmPOvznAEOFmNuO4Adjw==",
             "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=14.19"
+            },
             "peerDependencies": {
-                "vue": "^2.5.18"
+                "vue": "^3.0.4"
+            },
+            "peerDependenciesMeta": {
+                "vue": {
+                    "optional": true
+                }
             }
         },
         "node_modules/postcss": {
-            "version": "8.4.31",
-            "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
-            "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
-            "dev": true,
+            "version": "8.4.49",
+            "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+            "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
             "funding": [
                 {
                     "type": "opencollective",
@@ -13172,10 +12325,11 @@
                     "url": "https://github.com/sponsors/ai"
                 }
             ],
+            "license": "MIT",
             "dependencies": {
-                "nanoid": "^3.3.6",
-                "picocolors": "^1.0.0",
-                "source-map-js": "^1.0.2"
+                "nanoid": "^3.3.7",
+                "picocolors": "^1.1.1",
+                "source-map-js": "^1.2.1"
             },
             "engines": {
                 "node": "^10 || ^12 || >=14"
@@ -13313,45 +12467,82 @@
             }
         },
         "node_modules/postcss-loader": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-4.1.0.tgz",
-            "integrity": "sha512-vbCkP70F3Q9PIk6d47aBwjqAMI4LfkXCoyxj+7NPNuVIwfTGdzv2KVQes59/RuxMniIgsYQCFSY42P3+ykJfaw==",
+            "version": "8.1.1",
+            "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
+            "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "cosmiconfig": "^7.0.0",
-                "klona": "^2.0.4",
-                "loader-utils": "^2.0.0",
-                "schema-utils": "^3.0.0",
-                "semver": "^7.3.2"
+                "cosmiconfig": "^9.0.0",
+                "jiti": "^1.20.0",
+                "semver": "^7.5.4"
             },
             "engines": {
-                "node": ">= 10.13.0"
+                "node": ">= 18.12.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
             },
             "peerDependencies": {
+                "@rspack/core": "0.x || 1.x",
                 "postcss": "^7.0.0 || ^8.0.1",
-                "webpack": "^4.0.0 || ^5.0.0"
+                "webpack": "^5.0.0"
+            },
+            "peerDependenciesMeta": {
+                "@rspack/core": {
+                    "optional": true
+                },
+                "webpack": {
+                    "optional": true
+                }
             }
         },
-        "node_modules/postcss-loader/node_modules/schema-utils": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-            "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+        "node_modules/postcss-loader/node_modules/argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+            "dev": true,
+            "license": "Python-2.0"
+        },
+        "node_modules/postcss-loader/node_modules/cosmiconfig": {
+            "version": "9.0.0",
+            "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+            "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@types/json-schema": "^7.0.8",
-                "ajv": "^6.12.5",
-                "ajv-keywords": "^3.5.2"
+                "env-paths": "^2.2.1",
+                "import-fresh": "^3.3.0",
+                "js-yaml": "^4.1.0",
+                "parse-json": "^5.2.0"
             },
             "engines": {
-                "node": ">= 10.13.0"
+                "node": ">=14"
             },
             "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/webpack"
+                "url": "https://github.com/sponsors/d-fischer"
+            },
+            "peerDependencies": {
+                "typescript": ">=4.9.5"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/postcss-loader/node_modules/js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "argparse": "^2.0.1"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
             }
         },
         "node_modules/postcss-merge-longhand": {
@@ -13475,10 +12666,11 @@
             }
         },
         "node_modules/postcss-modules-extract-imports": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
-            "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+            "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
             "dev": true,
+            "license": "ISC",
             "engines": {
                 "node": "^10 || ^12 || >= 14"
             },
@@ -13487,13 +12679,14 @@
             }
         },
         "node_modules/postcss-modules-local-by-default": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz",
-            "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==",
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz",
+            "integrity": "sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "icss-utils": "^5.0.0",
-                "postcss-selector-parser": "^6.0.2",
+                "postcss-selector-parser": "^7.0.0",
                 "postcss-value-parser": "^4.1.0"
             },
             "engines": {
@@ -13503,13 +12696,28 @@
                 "postcss": "^8.1.0"
             }
         },
+        "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz",
+            "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "cssesc": "^3.0.0",
+                "util-deprecate": "^1.0.2"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
         "node_modules/postcss-modules-scope": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
-            "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
+            "version": "3.2.1",
+            "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
+            "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
             "dev": true,
+            "license": "ISC",
             "dependencies": {
-                "postcss-selector-parser": "^6.0.4"
+                "postcss-selector-parser": "^7.0.0"
             },
             "engines": {
                 "node": "^10 || ^12 || >= 14"
@@ -13518,6 +12726,20 @@
                 "postcss": "^8.1.0"
             }
         },
+        "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz",
+            "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "cssesc": "^3.0.0",
+                "util-deprecate": "^1.0.2"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
         "node_modules/postcss-modules-values": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
@@ -13761,10 +12983,11 @@
             }
         },
         "node_modules/postcss-selector-parser": {
-            "version": "6.0.13",
-            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
-            "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
+            "version": "6.1.2",
+            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+            "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "cssesc": "^3.0.0",
                 "util-deprecate": "^1.0.2"
@@ -13924,22 +13147,6 @@
                 "node": ">= 0.8.0"
             }
         },
-        "node_modules/prettier": {
-            "version": "2.8.8",
-            "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
-            "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
-            "dev": true,
-            "optional": true,
-            "bin": {
-                "prettier": "bin-prettier.js"
-            },
-            "engines": {
-                "node": ">=10.13.0"
-            },
-            "funding": {
-                "url": "https://github.com/prettier/prettier?sponsor=1"
-            }
-        },
         "node_modules/pretty-format": {
             "version": "29.7.0",
             "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@@ -13972,15 +13179,6 @@
             "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
             "dev": true
         },
-        "node_modules/progress": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
-            "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.4.0"
-            }
-        },
         "node_modules/promise": {
             "version": "7.3.1",
             "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
@@ -14010,12 +13208,6 @@
                 "node": ">= 6"
             }
         },
-        "node_modules/pseudomap": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
-            "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
-            "dev": true
-        },
         "node_modules/psl": {
             "version": "1.9.0",
             "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -14183,16 +13375,6 @@
                 }
             ]
         },
-        "node_modules/q": {
-            "version": "1.5.1",
-            "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
-            "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.6.0",
-                "teleport": ">=0.2.0"
-            }
-        },
         "node_modules/querystringify": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -14343,13 +13525,15 @@
             "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
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/regenerate-unicode-properties": {
-            "version": "10.1.0",
-            "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz",
-            "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==",
+            "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"
             },
@@ -14359,58 +13543,31 @@
         },
         "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/regenerator-transform": {
-            "version": "0.15.2",
-            "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
-            "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==",
-            "dev": true,
-            "dependencies": {
-                "@babel/runtime": "^7.8.4"
-            }
-        },
-        "node_modules/regexp.prototype.flags": {
-            "version": "1.5.0",
-            "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
-            "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.2.0",
-                "functions-have-names": "^1.2.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
+            "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+            "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+            "dev": true
         },
-        "node_modules/regexpp": {
-            "version": "3.2.0",
-            "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
-            "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+        "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,
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/mysticatea"
+            "license": "MIT",
+            "dependencies": {
+                "@babel/runtime": "^7.8.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"
             },
@@ -14418,27 +13575,26 @@
                 "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,
+            "license": "MIT"
+        },
         "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==",
-            "dev": true,
-            "bin": {
-                "jsesc": "bin/jsesc"
-            }
-        },
         "node_modules/require-directory": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -14542,18 +13698,6 @@
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/rgb-regex": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
-            "integrity": "sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==",
-            "dev": true
-        },
-        "node_modules/rgba-regex": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
-            "integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==",
-            "dev": true
-        },
         "node_modules/rgbcolor": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
@@ -14602,50 +13746,12 @@
                 "queue-microtask": "^1.2.2"
             }
         },
-        "node_modules/safe-array-concat": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz",
-            "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "get-intrinsic": "^1.2.0",
-                "has-symbols": "^1.0.3",
-                "isarray": "^2.0.5"
-            },
-            "engines": {
-                "node": ">=0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/safe-array-concat/node_modules/isarray": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
-            "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
-            "dev": true
-        },
         "node_modules/safe-buffer": {
             "version": "5.1.2",
             "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
             "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
             "dev": true
         },
-        "node_modules/safe-regex-test": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
-            "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "get-intrinsic": "^1.1.3",
-                "is-regex": "^1.1.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/safer-buffer": {
             "version": "2.1.2",
             "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -14705,32 +13811,30 @@
             }
         },
         "node_modules/sass-loader": {
-            "version": "10.4.1",
-            "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.4.1.tgz",
-            "integrity": "sha512-aX/iJZTTpNUNx/OSYzo2KsjIUQHqvWsAhhUijFjAPdZTEhstjZI9zTNvkTTwsx+uNUJqUwOw5gacxQMx4hJxGQ==",
+            "version": "16.0.4",
+            "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz",
+            "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "klona": "^2.0.4",
-                "loader-utils": "^2.0.0",
-                "neo-async": "^2.6.2",
-                "schema-utils": "^3.0.0",
-                "semver": "^7.3.2"
+                "neo-async": "^2.6.2"
             },
             "engines": {
-                "node": ">= 10.13.0"
+                "node": ">= 18.12.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
             },
             "peerDependencies": {
-                "fibers": ">= 3.1.0",
-                "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+                "@rspack/core": "0.x || 1.x",
+                "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
                 "sass": "^1.3.0",
-                "webpack": "^4.36.0 || ^5.0.0"
+                "sass-embedded": "*",
+                "webpack": "^5.0.0"
             },
             "peerDependenciesMeta": {
-                "fibers": {
+                "@rspack/core": {
                     "optional": true
                 },
                 "node-sass": {
@@ -14738,33 +13842,15 @@
                 },
                 "sass": {
                     "optional": true
+                },
+                "sass-embedded": {
+                    "optional": true
+                },
+                "webpack": {
+                    "optional": true
                 }
             }
         },
-        "node_modules/sass-loader/node_modules/schema-utils": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-            "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
-            "dev": true,
-            "dependencies": {
-                "@types/json-schema": "^7.0.8",
-                "ajv": "^6.12.5",
-                "ajv-keywords": "^3.5.2"
-            },
-            "engines": {
-                "node": ">= 10.13.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/webpack"
-            }
-        },
-        "node_modules/sax": {
-            "version": "1.2.4",
-            "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
-            "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
-            "dev": true
-        },
         "node_modules/saxes": {
             "version": "6.0.0",
             "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -14778,23 +13864,62 @@
             }
         },
         "node_modules/schema-utils": {
-            "version": "2.7.1",
-            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
-            "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==",
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
+            "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@types/json-schema": "^7.0.5",
-                "ajv": "^6.12.4",
-                "ajv-keywords": "^3.5.2"
+                "@types/json-schema": "^7.0.9",
+                "ajv": "^8.9.0",
+                "ajv-formats": "^2.1.1",
+                "ajv-keywords": "^5.1.0"
             },
             "engines": {
-                "node": ">= 8.9.0"
+                "node": ">= 12.13.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
             }
         },
+        "node_modules/schema-utils/node_modules/ajv": {
+            "version": "8.17.1",
+            "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+            "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "fast-deep-equal": "^3.1.3",
+                "fast-uri": "^3.0.1",
+                "json-schema-traverse": "^1.0.0",
+                "require-from-string": "^2.0.2"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/epoberezkin"
+            }
+        },
+        "node_modules/schema-utils/node_modules/ajv-keywords": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+            "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "fast-deep-equal": "^3.1.3"
+            },
+            "peerDependencies": {
+                "ajv": "^8.8.2"
+            }
+        },
+        "node_modules/schema-utils/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"
+        },
         "node_modules/select2": {
             "version": "4.0.13",
             "resolved": "https://registry.npmjs.org/select2/-/select2-4.0.13.tgz",
@@ -14802,13 +13927,11 @@
             "dev": true
         },
         "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==",
+            "version": "7.6.3",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+            "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
             "dev": true,
-            "dependencies": {
-                "lru-cache": "^6.0.0"
-            },
+            "license": "ISC",
             "bin": {
                 "semver": "bin/semver.js"
             },
@@ -14816,24 +13939,6 @@
                 "node": ">=10"
             }
         },
-        "node_modules/semver/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==",
-            "dev": true,
-            "dependencies": {
-                "yallist": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            }
-        },
-        "node_modules/semver/node_modules/yallist": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-            "dev": true
-        },
         "node_modules/serialize-javascript": {
             "version": "5.0.1",
             "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
@@ -14905,41 +14010,12 @@
             "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
             "dev": true
         },
-        "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==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.0",
-                "get-intrinsic": "^1.0.2",
-                "object-inspect": "^1.9.0"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/signal-exit": {
             "version": "3.0.7",
             "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
             "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
             "dev": true
         },
-        "node_modules/simple-swizzle": {
-            "version": "0.2.2",
-            "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
-            "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
-            "dev": true,
-            "dependencies": {
-                "is-arrayish": "^0.3.1"
-            }
-        },
-        "node_modules/simple-swizzle/node_modules/is-arrayish": {
-            "version": "0.3.2",
-            "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
-            "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
-            "dev": true
-        },
         "node_modules/sisteransi": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -14955,55 +14031,11 @@
                 "node": ">=8"
             }
         },
-        "node_modules/slice-ansi": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
-            "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
-            "dev": true,
-            "dependencies": {
-                "ansi-styles": "^4.0.0",
-                "astral-regex": "^2.0.0",
-                "is-fullwidth-code-point": "^3.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/slice-ansi?sponsor=1"
-            }
-        },
-        "node_modules/slice-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==",
-            "dev": true,
-            "dependencies": {
-                "color-convert": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-            }
-        },
-        "node_modules/slice-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==",
-            "dev": true,
-            "dependencies": {
-                "color-name": "~1.1.4"
-            },
-            "engines": {
-                "node": ">=7.0.0"
-            }
-        },
         "node_modules/sortablejs": {
-            "version": "1.10.2",
-            "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
-            "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==",
-            "dev": true
+            "version": "1.14.0",
+            "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
+            "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
+            "license": "MIT"
         },
         "node_modules/source-list-map": {
             "version": "2.0.1",
@@ -15021,10 +14053,10 @@
             }
         },
         "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==",
-            "dev": true,
+            "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==",
+            "license": "BSD-3-Clause",
             "engines": {
                 "node": ">=0.10.0"
             }
@@ -15155,56 +14187,41 @@
                 "node": ">=8"
             }
         },
-        "node_modules/string.prototype.trim": {
-            "version": "1.2.7",
-            "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
-            "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==",
+        "node_modules/string-width-cjs": {
+            "name": "string-width",
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.1.4",
-                "es-abstract": "^1.20.4"
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
             },
             "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/string.prototype.trimend": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
-            "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.1.4",
-                "es-abstract": "^1.20.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
+                "node": ">=8"
             }
         },
-        "node_modules/string.prototype.trimstart": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
-            "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
             "dev": true,
             "dependencies": {
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.1.4",
-                "es-abstract": "^1.20.4"
+                "ansi-regex": "^5.0.1"
             },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
+            "engines": {
+                "node": ">=8"
             }
         },
-        "node_modules/strip-ansi": {
+        "node_modules/strip-ansi-cjs": {
+            "name": "strip-ansi",
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
             "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "ansi-regex": "^5.0.1"
             },
@@ -15243,41 +14260,20 @@
             }
         },
         "node_modules/style-loader": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz",
-            "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==",
-            "dev": true,
-            "dependencies": {
-                "loader-utils": "^2.0.0",
-                "schema-utils": "^3.0.0"
-            },
-            "engines": {
-                "node": ">= 10.13.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/webpack"
-            },
-            "peerDependencies": {
-                "webpack": "^4.0.0 || ^5.0.0"
-            }
-        },
-        "node_modules/style-loader/node_modules/schema-utils": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
-            "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz",
+            "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==",
             "dev": true,
-            "dependencies": {
-                "@types/json-schema": "^7.0.8",
-                "ajv": "^6.12.5",
-                "ajv-keywords": "^3.5.2"
-            },
+            "license": "MIT",
             "engines": {
-                "node": ">= 10.13.0"
+                "node": ">= 18.12.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
+            },
+            "peerDependencies": {
+                "webpack": "^5.27.0"
             }
         },
         "node_modules/stylehacks": {
@@ -15313,15 +14309,19 @@
             }
         },
         "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==",
+            "version": "8.1.1",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+            "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "has-flag": "^3.0.0"
+                "has-flag": "^4.0.0"
             },
             "engines": {
-                "node": ">=4"
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/supports-color?sponsor=1"
             }
         },
         "node_modules/supports-preserve-symlinks-flag": {
@@ -15430,49 +14430,11 @@
             "dev": true
         },
         "node_modules/tabbable": {
-            "version": "5.3.3",
-            "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
-            "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==",
-            "dev": true,
-            "peer": true
-        },
-        "node_modules/table": {
-            "version": "6.8.1",
-            "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
-            "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
-            "dev": true,
-            "dependencies": {
-                "ajv": "^8.0.1",
-                "lodash.truncate": "^4.4.2",
-                "slice-ansi": "^4.0.0",
-                "string-width": "^4.2.3",
-                "strip-ansi": "^6.0.1"
-            },
-            "engines": {
-                "node": ">=10.0.0"
-            }
-        },
-        "node_modules/table/node_modules/ajv": {
-            "version": "8.12.0",
-            "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
-            "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+            "version": "6.2.0",
+            "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+            "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
             "dev": true,
-            "dependencies": {
-                "fast-deep-equal": "^3.1.1",
-                "json-schema-traverse": "^1.0.0",
-                "require-from-string": "^2.0.2",
-                "uri-js": "^4.2.2"
-            },
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/epoberezkin"
-            }
-        },
-        "node_modules/table/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"
         },
         "node_modules/tablesorter": {
             "version": "2.31.3",
@@ -15525,10 +14487,11 @@
             "dev": true
         },
         "node_modules/terser": {
-            "version": "5.18.2",
-            "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz",
-            "integrity": "sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w==",
+            "version": "5.34.1",
+            "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz",
+            "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==",
             "dev": true,
+            "license": "BSD-2-Clause",
             "dependencies": {
                 "@jridgewell/source-map": "^0.3.3",
                 "acorn": "^8.8.2",
@@ -15708,27 +14671,12 @@
                 "readable-stream": "2 || 3"
             }
         },
-        "node_modules/timsort": {
-            "version": "0.3.0",
-            "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
-            "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
-            "dev": true
-        },
         "node_modules/tmpl": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
             "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
             "dev": true
         },
-        "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==",
-            "dev": true,
-            "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",
@@ -15785,22 +14733,24 @@
             }
         },
         "node_modules/ts-api-utils": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
-            "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+            "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
             "dev": true,
+            "license": "MIT",
             "engines": {
-                "node": ">=16.13.0"
+                "node": ">=16"
             },
             "peerDependencies": {
                 "typescript": ">=4.2.0"
             }
         },
         "node_modules/ts-loader": {
-            "version": "9.5.0",
-            "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.0.tgz",
-            "integrity": "sha512-LLlB/pkB4q9mW2yLdFMnK3dEHbrBjeZTYguaaIfusyojBgAGf5kF+O6KcWqiGzWqHk0LBsoolrp4VftEURhybg==",
+            "version": "9.5.1",
+            "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
+            "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "chalk": "^4.1.0",
                 "enhanced-resolve": "^5.0.0",
@@ -15859,15 +14809,6 @@
                 "node": ">=7.0.0"
             }
         },
-        "node_modules/ts-loader/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/ts-loader/node_modules/source-map": {
             "version": "0.7.4",
             "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@@ -15889,6 +14830,13 @@
                 "node": ">=8"
             }
         },
+        "node_modules/tslib": {
+            "version": "2.6.3",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
+            "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
+            "dev": true,
+            "license": "0BSD"
+        },
         "node_modules/type-check": {
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -15922,25 +14870,12 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
-        "node_modules/typed-array-length": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
-            "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "for-each": "^0.3.3",
-                "is-typed-array": "^1.1.9"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/typescript": {
-            "version": "5.2.2",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
-            "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
-            "dev": true,
+            "version": "5.7.2",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+            "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+            "devOptional": true,
+            "license": "Apache-2.0",
             "bin": {
                 "tsc": "bin/tsc",
                 "tsserver": "bin/tsserver"
@@ -15949,26 +14884,22 @@
                 "node": ">=14.17"
             }
         },
-        "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==",
+        "node_modules/typical": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
+            "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==",
             "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "has-bigints": "^1.0.2",
-                "has-symbols": "^1.0.3",
-                "which-boxed-primitive": "^1.0.2"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
+            "license": "MIT",
+            "engines": {
+                "node": ">=8"
             }
         },
         "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"
             }
@@ -15978,6 +14909,7 @@
             "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"
@@ -15987,10 +14919,11 @@
             }
         },
         "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"
             }
@@ -16000,22 +14933,11 @@
             "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"
             }
         },
-        "node_modules/uniq": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
-            "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==",
-            "dev": true
-        },
-        "node_modules/uniqs": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz",
-            "integrity": "sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==",
-            "dev": true
-        },
         "node_modules/unique-filename": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
@@ -16043,16 +14965,10 @@
                 "node": ">= 4.0.0"
             }
         },
-        "node_modules/unquote": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
-            "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==",
-            "dev": true
-        },
         "node_modules/update-browserslist-db": {
-            "version": "1.0.13",
-            "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
-            "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+            "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
             "dev": true,
             "funding": [
                 {
@@ -16068,9 +14984,10 @@
                     "url": "https://github.com/sponsors/ai"
                 }
             ],
+            "license": "MIT",
             "dependencies": {
-                "escalade": "^3.1.1",
-                "picocolors": "^1.0.0"
+                "escalade": "^3.2.0",
+                "picocolors": "^1.1.0"
             },
             "bin": {
                 "update-browserslist-db": "cli.js"
@@ -16104,21 +15021,6 @@
             "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
             "dev": true
         },
-        "node_modules/util.promisify": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz",
-            "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==",
-            "dev": true,
-            "dependencies": {
-                "define-properties": "^1.1.3",
-                "es-abstract": "^1.17.2",
-                "has-symbols": "^1.0.1",
-                "object.getownpropertydescriptors": "^2.1.0"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/utrie": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
@@ -16138,12 +15040,6 @@
                 "uuid": "dist/bin/uuid"
             }
         },
-        "node_modules/v8-compile-cache": {
-            "version": "2.3.0",
-            "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
-            "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
-            "dev": true
-        },
         "node_modules/v8-to-istanbul": {
             "version": "9.1.3",
             "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz",
@@ -16164,16 +15060,6 @@
             "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
             "dev": true
         },
-        "node_modules/vendors": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz",
-            "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==",
-            "dev": true,
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/wooorm"
-            }
-        },
         "node_modules/vlq": {
             "version": "0.2.3",
             "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz",
@@ -16190,36 +15076,43 @@
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/vrp-vue-resizable": {
-            "version": "1.2.7",
-            "resolved": "https://registry.npmjs.org/vrp-vue-resizable/-/vrp-vue-resizable-1.2.7.tgz",
-            "integrity": "sha512-pmhapoAQeTd/ci7pkyeALW98O9hNRqb84vLQqvCOTDnJZBhxq59qGCegKDrYOvI/KBkeaxdQOEfSHD9i1Bkk7A==",
-            "dev": true,
+        "node_modules/vue": {
+            "version": "3.5.13",
+            "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
+            "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@vue/compiler-dom": "3.5.13",
+                "@vue/compiler-sfc": "3.5.13",
+                "@vue/runtime-dom": "3.5.13",
+                "@vue/server-renderer": "3.5.13",
+                "@vue/shared": "3.5.13"
+            },
             "peerDependencies": {
-                "vue": "2.x"
+                "typescript": "*"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
             }
         },
-        "node_modules/vue": {
-            "version": "2.7.14",
-            "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz",
-            "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==",
+        "node_modules/vue-dragscroll": {
+            "version": "4.0.6",
+            "resolved": "https://registry.npmjs.org/vue-dragscroll/-/vue-dragscroll-4.0.6.tgz",
+            "integrity": "sha512-zW1k58p41yhmFhmg/JxfesUM4Srl0JfXg7xSINqffVGpHJKvnEHMK4QgF6mUVkPMTgibn976fhPYkomcXPvvFA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@vue/compiler-sfc": "2.7.14",
-                "csstype": "^3.1.0"
+                "vue": "^3.2.37"
             }
         },
-        "node_modules/vue-dragscroll": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/vue-dragscroll/-/vue-dragscroll-3.0.1.tgz",
-            "integrity": "sha512-4Oq3S1RGm94YMy/DcjcvM+itRLoTGnKaxlpUUVICPPkNEvMMKzArZUNDXxmDInVa8t4GtrfM0uGAShFP+0JIUw==",
-            "dev": true
-        },
         "node_modules/vue-eslint-parser": {
-            "version": "9.3.1",
-            "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz",
-            "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==",
+            "version": "9.4.3",
+            "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
+            "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "debug": "^4.3.4",
                 "eslint-scope": "^7.1.1",
@@ -16239,18 +15132,6 @@
                 "eslint": ">=6.0.0"
             }
         },
-        "node_modules/vue-eslint-parser/node_modules/acorn": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
-            "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
-            "dev": true,
-            "bin": {
-                "acorn": "bin/acorn"
-            },
-            "engines": {
-                "node": ">=0.4.0"
-            }
-        },
         "node_modules/vue-eslint-parser/node_modules/eslint-scope": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
@@ -16267,248 +15148,385 @@
                 "url": "https://opencollective.com/eslint"
             }
         },
-        "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": {
-            "version": "3.4.1",
-            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
-            "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
+        "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": {
+            "version": "3.4.1",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
+            "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
+            "dev": true,
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/vue-eslint-parser/node_modules/estraverse": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+            "dev": true,
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/vue-loader": {
+            "version": "17.4.2",
+            "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.4.2.tgz",
+            "integrity": "sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "chalk": "^4.1.0",
+                "hash-sum": "^2.0.0",
+                "watchpack": "^2.4.0"
+            },
+            "peerDependencies": {
+                "webpack": "^4.1.0 || ^5.0.0-0"
+            },
+            "peerDependenciesMeta": {
+                "@vue/compiler-sfc": {
+                    "optional": true
+                },
+                "vue": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/vue-loader/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/vue-loader/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/vue-loader/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/vue-loader/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/vue-resizable": {
+            "version": "2.1.7",
+            "resolved": "https://registry.npmjs.org/vue-resizable/-/vue-resizable-2.1.7.tgz",
+            "integrity": "sha512-zEbWhRR8iXT8+nt3u8rkfrNpkPNsPkf7HteBh+AlPIsJ7rf9fyNwMqr0Q4FRzIpNIpZD5Zrr4+3+YELU0vc1Iw==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/vue-router": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",
+            "integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@vue/devtools-api": "^6.6.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/posva"
+            },
+            "peerDependencies": {
+                "vue": "^3.2.0"
+            }
+        },
+        "node_modules/vue-select": {
+            "version": "4.0.0-beta.6",
+            "resolved": "https://registry.npmjs.org/vue-select/-/vue-select-4.0.0-beta.6.tgz",
+            "integrity": "sha512-K+zrNBSpwMPhAxYLTCl56gaMrWZGgayoWCLqe5rWwkB8aUbAUh7u6sXjIR7v4ckp2WKC7zEEUY27g6h1MRsIHw==",
+            "dev": true,
+            "license": "MIT",
+            "peerDependencies": {
+                "vue": "3.x"
+            }
+        },
+        "node_modules/vue-typer": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/vue-typer/-/vue-typer-1.2.0.tgz",
+            "integrity": "sha512-o0n2F9yOnbdQak1OiPFbZonIzysL5jiS1OPgaEX0KnMlKqXRKi808QHRdoMuqw44oYQM/vtxCt3AaNb9OzKH1Q==",
+            "dev": true,
+            "dependencies": {
+                "lodash.split": "^4.4.2"
+            }
+        },
+        "node_modules/vue3-gettext": {
+            "version": "3.0.0-beta.5",
+            "resolved": "https://registry.npmjs.org/vue3-gettext/-/vue3-gettext-3.0.0-beta.5.tgz",
+            "integrity": "sha512-Dn1qZrFgtaKg/9mMMypvQn+lyyHPAAylQBN4kPGqqr8oAMOJGyKg+rCLCcoyuLGLDvPGBgtawOTQ/0dFmGbDrw==",
             "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "chalk": "^4.1.2",
+                "command-line-args": "^5.2.1",
+                "cosmiconfig": "^9.0.0",
+                "gettext-extractor": "^3.8.0",
+                "glob": "^10.4.1",
+                "parse5": "^6.0.1",
+                "parse5-htmlparser2-tree-adapter": "^6.0.1",
+                "pofile": "^1.1.4",
+                "tslib": "^2.6.2"
+            },
+            "bin": {
+                "vue-gettext-compile": "dist/bin/gettext_compile.js",
+                "vue-gettext-extract": "dist/bin/gettext_extract.js"
+            },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": ">= 18.0.0"
             },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
+            "peerDependencies": {
+                "@vue/compiler-sfc": ">=3.0.0",
+                "vue": ">=3.0.0"
             }
         },
-        "node_modules/vue-eslint-parser/node_modules/espree": {
-            "version": "9.6.0",
-            "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz",
-            "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==",
+        "node_modules/vue3-gettext/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": {
-                "acorn": "^8.9.0",
-                "acorn-jsx": "^5.3.2",
-                "eslint-visitor-keys": "^3.4.1"
+                "color-convert": "^2.0.1"
             },
             "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+                "node": ">=8"
             },
             "funding": {
-                "url": "https://opencollective.com/eslint"
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
             }
         },
-        "node_modules/vue-eslint-parser/node_modules/estraverse": {
-            "version": "5.3.0",
-            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
-            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+        "node_modules/vue3-gettext/node_modules/argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
             "dev": true,
-            "engines": {
-                "node": ">=4.0"
-            }
+            "license": "Python-2.0"
         },
-        "node_modules/vue-gettext": {
-            "version": "2.1.12",
-            "resolved": "https://registry.npmjs.org/vue-gettext/-/vue-gettext-2.1.12.tgz",
-            "integrity": "sha512-7Kw36xtKvARp8ZafQGPK9WR6EM+dhFUikR5f0+etSkiHuvUM3yf1HsRDLYoLLdJ0AMaXxKwgekumzvCk6KX8rA==",
+        "node_modules/vue3-gettext/node_modules/brace-expansion": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+            "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
             "dev": true,
-            "engines": {
-                "npm": ">= 3.0.0"
+            "license": "MIT",
+            "dependencies": {
+                "balanced-match": "^1.0.0"
             }
         },
-        "node_modules/vue-hot-reload-api": {
-            "version": "2.3.4",
-            "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
-            "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
-            "dev": true
-        },
-        "node_modules/vue-loader": {
-            "version": "15.10.2",
-            "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.2.tgz",
-            "integrity": "sha512-ndeSe/8KQc/nlA7TJ+OBhv2qalmj1s+uBs7yHDRFaAXscFTApBzY9F1jES3bautmgWjDlDct0fw8rPuySDLwxw==",
+        "node_modules/vue3-gettext/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": {
-                "@vue/component-compiler-utils": "^3.1.0",
-                "hash-sum": "^1.0.2",
-                "loader-utils": "^1.1.0",
-                "vue-hot-reload-api": "^2.3.0",
-                "vue-style-loader": "^4.1.0"
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
             },
-            "peerDependencies": {
-                "css-loader": "*",
-                "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0"
+            "engines": {
+                "node": ">=10"
             },
-            "peerDependenciesMeta": {
-                "cache-loader": {
-                    "optional": true
-                },
-                "prettier": {
-                    "optional": true
-                },
-                "vue-template-compiler": {
-                    "optional": true
-                }
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
             }
         },
-        "node_modules/vue-loader/node_modules/json5": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
-            "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+        "node_modules/vue3-gettext/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": {
-                "minimist": "^1.2.0"
+                "color-name": "~1.1.4"
             },
-            "bin": {
-                "json5": "lib/cli.js"
+            "engines": {
+                "node": ">=7.0.0"
             }
         },
-        "node_modules/vue-loader/node_modules/loader-utils": {
-            "version": "1.4.2",
-            "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
-            "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+        "node_modules/vue3-gettext/node_modules/cosmiconfig": {
+            "version": "9.0.0",
+            "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+            "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "big.js": "^5.2.2",
-                "emojis-list": "^3.0.0",
-                "json5": "^1.0.1"
+                "env-paths": "^2.2.1",
+                "import-fresh": "^3.3.0",
+                "js-yaml": "^4.1.0",
+                "parse-json": "^5.2.0"
             },
             "engines": {
-                "node": ">=4.0.0"
-            }
-        },
-        "node_modules/vue-router": {
-            "version": "3.6.5",
-            "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz",
-            "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==",
-            "dev": true
-        },
-        "node_modules/vue-select": {
-            "version": "3.20.2",
-            "resolved": "https://registry.npmjs.org/vue-select/-/vue-select-3.20.2.tgz",
-            "integrity": "sha512-ZSzIDzyYsWZULGUxVp1h6u3yi9IZQBWX8r6kSudUI/I5J1HQKpBjRntvkrg6pr87xmm16kdChvHCDN+W84vTKw==",
-            "dev": true,
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/d-fischer"
+            },
             "peerDependencies": {
-                "vue": "2.x"
-            }
-        },
-        "node_modules/vue-style-loader": {
-            "version": "4.1.3",
-            "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
-            "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==",
-            "dev": true,
-            "dependencies": {
-                "hash-sum": "^1.0.2",
-                "loader-utils": "^1.0.2"
+                "typescript": ">=4.9.5"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
             }
         },
-        "node_modules/vue-style-loader/node_modules/json5": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
-            "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+        "node_modules/vue3-gettext/node_modules/glob": {
+            "version": "10.4.1",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
+            "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
             "dev": true,
+            "license": "ISC",
             "dependencies": {
-                "minimist": "^1.2.0"
+                "foreground-child": "^3.1.0",
+                "jackspeak": "^3.1.2",
+                "minimatch": "^9.0.4",
+                "minipass": "^7.1.2",
+                "path-scurry": "^1.11.1"
             },
             "bin": {
-                "json5": "lib/cli.js"
+                "glob": "dist/esm/bin.mjs"
+            },
+            "engines": {
+                "node": ">=16 || 14 >=14.18"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/vue-style-loader/node_modules/loader-utils": {
-            "version": "1.4.2",
-            "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
-            "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+        "node_modules/vue3-gettext/node_modules/js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "big.js": "^5.2.2",
-                "emojis-list": "^3.0.0",
-                "json5": "^1.0.1"
+                "argparse": "^2.0.1"
             },
-            "engines": {
-                "node": ">=4.0.0"
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
             }
         },
-        "node_modules/vue-template-babel-compiler": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/vue-template-babel-compiler/-/vue-template-babel-compiler-1.2.0.tgz",
-            "integrity": "sha512-CScBSX1/wCdmmZ/Lvj/63p2CCVTS0FMj0F69VRBo73CuJrjvPAPGmeNJ7D/cwt/VS2PduowRWbO8N4Zh4Z3b0g==",
+        "node_modules/vue3-gettext/node_modules/minimatch": {
+            "version": "9.0.4",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+            "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
             "dev": true,
+            "license": "ISC",
             "dependencies": {
-                "@babel/core": "^7.14.3",
-                "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
-                "@babel/plugin-proposal-object-rest-spread": "^7.15.6",
-                "@babel/plugin-proposal-optional-chaining": "^7.14.2",
-                "@babel/plugin-transform-arrow-functions": "^7.14.5",
-                "@babel/plugin-transform-block-scoping": "^7.14.5",
-                "@babel/plugin-transform-computed-properties": "^7.14.5",
-                "@babel/plugin-transform-destructuring": "^7.14.5",
-                "@babel/plugin-transform-parameters": "^7.14.5",
-                "@babel/plugin-transform-spread": "^7.14.5",
-                "@babel/types": "^7.14.5",
-                "deepmerge": "^4.2.2"
+                "brace-expansion": "^2.0.1"
             },
             "engines": {
-                "node": ">=12.0.0"
+                "node": ">=16 || 14 >=14.17"
             },
-            "peerDependencies": {
-                "vue-template-compiler": "^2.6.0"
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
             }
         },
-        "node_modules/vue-template-compiler": {
-            "version": "2.7.14",
-            "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
-            "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==",
+        "node_modules/vue3-gettext/node_modules/minipass": {
+            "version": "7.1.2",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+            "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
             "dev": true,
-            "dependencies": {
-                "de-indent": "^1.0.2",
-                "he": "^1.2.0"
+            "license": "ISC",
+            "engines": {
+                "node": ">=16 || 14 >=14.17"
             }
         },
-        "node_modules/vue-template-es2015-compiler": {
-            "version": "1.9.1",
-            "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
-            "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
-            "dev": true
-        },
-        "node_modules/vue-twentytwenty": {
-            "version": "0.10.1",
-            "resolved": "https://registry.npmjs.org/vue-twentytwenty/-/vue-twentytwenty-0.10.1.tgz",
-            "integrity": "sha512-QyfmJ5agedN3rbNZwhKLQFTSScGXReaI6FIHtoCm0kyPgm7n0IzoMg1opXXz+SRCFXm7jjxOArtQta9Nv9u+cQ==",
-            "dev": true
+        "node_modules/vue3-gettext/node_modules/parse5": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+            "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+            "dev": true,
+            "license": "MIT"
         },
-        "node_modules/vue-typer": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/vue-typer/-/vue-typer-1.2.0.tgz",
-            "integrity": "sha512-o0n2F9yOnbdQak1OiPFbZonIzysL5jiS1OPgaEX0KnMlKqXRKi808QHRdoMuqw44oYQM/vtxCt3AaNb9OzKH1Q==",
+        "node_modules/vue3-gettext/node_modules/parse5-htmlparser2-tree-adapter": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
+            "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "lodash.split": "^4.4.2"
+                "parse5": "^6.0.1"
             }
         },
-        "node_modules/vue/node_modules/@vue/compiler-sfc": {
-            "version": "2.7.14",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz",
-            "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==",
+        "node_modules/vue3-gettext/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": {
-                "@babel/parser": "^7.18.4",
-                "postcss": "^8.4.14",
-                "source-map": "^0.6.1"
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
             }
         },
         "node_modules/vuedraggable": {
-            "version": "2.24.3",
-            "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
-            "integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
-            "dev": true,
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
+            "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
+            "license": "MIT",
             "dependencies": {
-                "sortablejs": "1.10.2"
+                "sortablejs": "1.14.0"
+            },
+            "peerDependencies": {
+                "vue": "^3.0.1"
             }
         },
         "node_modules/vuex": {
-            "version": "3.6.2",
-            "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
-            "integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==",
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
+            "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
             "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@vue/devtools-api": "^6.0.0-beta.11"
+            },
             "peerDependencies": {
-                "vue": "^2.0.0"
+                "vue": "^3.2.0"
             }
         },
         "node_modules/w3c-xmlserializer": {
@@ -16533,10 +15551,11 @@
             }
         },
         "node_modules/watchpack": {
-            "version": "2.4.0",
-            "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
-            "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+            "version": "2.4.2",
+            "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
+            "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "glob-to-regexp": "^0.4.1",
                 "graceful-fs": "^4.1.2"
@@ -16564,34 +15583,34 @@
             }
         },
         "node_modules/webpack": {
-            "version": "5.88.2",
-            "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz",
-            "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==",
-            "dev": true,
-            "dependencies": {
-                "@types/eslint-scope": "^3.7.3",
-                "@types/estree": "^1.0.0",
-                "@webassemblyjs/ast": "^1.11.5",
-                "@webassemblyjs/wasm-edit": "^1.11.5",
-                "@webassemblyjs/wasm-parser": "^1.11.5",
-                "acorn": "^8.7.1",
-                "acorn-import-assertions": "^1.9.0",
-                "browserslist": "^4.14.5",
+            "version": "5.97.0",
+            "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.0.tgz",
+            "integrity": "sha512-CWT8v7ShSfj7tGs4TLRtaOLmOCPWhoKEvp+eA7FVx8Xrjb3XfT0aXdxDItnRZmE8sHcH+a8ayDrJCOjXKxVFfQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@types/eslint-scope": "^3.7.7",
+                "@types/estree": "^1.0.6",
+                "@webassemblyjs/ast": "^1.14.1",
+                "@webassemblyjs/wasm-edit": "^1.14.1",
+                "@webassemblyjs/wasm-parser": "^1.14.1",
+                "acorn": "^8.14.0",
+                "browserslist": "^4.24.0",
                 "chrome-trace-event": "^1.0.2",
-                "enhanced-resolve": "^5.15.0",
+                "enhanced-resolve": "^5.17.1",
                 "es-module-lexer": "^1.2.1",
                 "eslint-scope": "5.1.1",
                 "events": "^3.2.0",
                 "glob-to-regexp": "^0.4.1",
-                "graceful-fs": "^4.2.9",
+                "graceful-fs": "^4.2.11",
                 "json-parse-even-better-errors": "^2.3.1",
                 "loader-runner": "^4.2.0",
                 "mime-types": "^2.1.27",
                 "neo-async": "^2.6.2",
                 "schema-utils": "^3.2.0",
                 "tapable": "^2.1.1",
-                "terser-webpack-plugin": "^5.3.7",
-                "watchpack": "^2.4.0",
+                "terser-webpack-plugin": "^5.3.10",
+                "watchpack": "^2.4.1",
                 "webpack-sources": "^3.2.3"
             },
             "bin": {
@@ -16611,44 +15630,43 @@
             }
         },
         "node_modules/webpack-cli": {
-            "version": "4.10.0",
-            "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz",
-            "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==",
+            "version": "5.1.4",
+            "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
+            "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@discoveryjs/json-ext": "^0.5.0",
-                "@webpack-cli/configtest": "^1.2.0",
-                "@webpack-cli/info": "^1.5.0",
-                "@webpack-cli/serve": "^1.7.0",
+                "@webpack-cli/configtest": "^2.1.1",
+                "@webpack-cli/info": "^2.0.2",
+                "@webpack-cli/serve": "^2.0.5",
                 "colorette": "^2.0.14",
-                "commander": "^7.0.0",
+                "commander": "^10.0.1",
                 "cross-spawn": "^7.0.3",
+                "envinfo": "^7.7.3",
                 "fastest-levenshtein": "^1.0.12",
                 "import-local": "^3.0.2",
-                "interpret": "^2.2.0",
-                "rechoir": "^0.7.0",
+                "interpret": "^3.1.1",
+                "rechoir": "^0.8.0",
                 "webpack-merge": "^5.7.3"
             },
             "bin": {
                 "webpack-cli": "bin/cli.js"
             },
             "engines": {
-                "node": ">=10.13.0"
+                "node": ">=14.15.0"
             },
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/webpack"
             },
             "peerDependencies": {
-                "webpack": "4.x.x || 5.x.x"
+                "webpack": "5.x.x"
             },
             "peerDependenciesMeta": {
                 "@webpack-cli/generators": {
                     "optional": true
                 },
-                "@webpack-cli/migrate": {
-                    "optional": true
-                },
                 "webpack-bundle-analyzer": {
                     "optional": true
                 },
@@ -16657,25 +15675,37 @@
                 }
             }
         },
+        "node_modules/webpack-cli/node_modules/commander": {
+            "version": "10.0.1",
+            "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+            "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=14"
+            }
+        },
         "node_modules/webpack-cli/node_modules/interpret": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
-            "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==",
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+            "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
             "dev": true,
+            "license": "MIT",
             "engines": {
-                "node": ">= 0.10"
+                "node": ">=10.13.0"
             }
         },
         "node_modules/webpack-cli/node_modules/rechoir": {
-            "version": "0.7.1",
-            "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
-            "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==",
+            "version": "0.8.0",
+            "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+            "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "resolve": "^1.9.0"
+                "resolve": "^1.20.0"
             },
             "engines": {
-                "node": ">= 0.10"
+                "node": ">= 10.13.0"
             }
         },
         "node_modules/webpack-cli/node_modules/webpack-merge": {
@@ -16692,16 +15722,18 @@
             }
         },
         "node_modules/webpack-merge": {
-            "version": "5.4.0",
-            "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.4.0.tgz",
-            "integrity": "sha512-/scBgu8LVPlHDgqH95Aw1xS+L+PHrpHKOwYVGFaNOQl4Q4wwwWDarwB1WdZAbLQ24SKhY3Awe7VZGYAdp+N+gQ==",
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz",
+            "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "clone-deep": "^4.0.1",
-                "wildcard": "^2.0.0"
+                "flat": "^5.0.2",
+                "wildcard": "^2.0.1"
             },
             "engines": {
-                "node": ">=10.0.0"
+                "node": ">=18.0.0"
             }
         },
         "node_modules/webpack-notifier": {
@@ -16736,10 +15768,11 @@
             }
         },
         "node_modules/webpack/node_modules/acorn": {
-            "version": "8.9.0",
-            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
-            "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
+            "version": "8.14.0",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+            "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
             "dev": true,
+            "license": "MIT",
             "bin": {
                 "acorn": "bin/acorn"
             },
@@ -16747,29 +15780,12 @@
                 "node": ">=0.4.0"
             }
         },
-        "node_modules/webpack/node_modules/acorn-import-assertions": {
-            "version": "1.9.0",
-            "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
-            "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
-            "dev": true,
-            "peerDependencies": {
-                "acorn": "^8"
-            }
-        },
-        "node_modules/webpack/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,
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/webpack/node_modules/jest-worker": {
             "version": "27.5.1",
             "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
             "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@types/node": "*",
                 "merge-stream": "^2.0.0",
@@ -16784,6 +15800,7 @@
             "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
             "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@types/json-schema": "^7.0.8",
                 "ajv": "^6.12.5",
@@ -16798,40 +15815,27 @@
             }
         },
         "node_modules/webpack/node_modules/serialize-javascript": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
-            "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
+            "version": "6.0.2",
+            "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_modules/webpack/node_modules/supports-color": {
-            "version": "8.1.1",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-            "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
-            "dev": true,
-            "dependencies": {
-                "has-flag": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/supports-color?sponsor=1"
-            }
-        },
         "node_modules/webpack/node_modules/terser-webpack-plugin": {
-            "version": "5.3.9",
-            "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
-            "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
+            "version": "5.3.10",
+            "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
+            "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@jridgewell/trace-mapping": "^0.3.17",
+                "@jridgewell/trace-mapping": "^0.3.20",
                 "jest-worker": "^27.4.5",
                 "schema-utils": "^3.1.1",
                 "serialize-javascript": "^6.0.1",
-                "terser": "^5.16.8"
+                "terser": "^5.26.0"
             },
             "engines": {
                 "node": ">= 10.13.0"
@@ -16913,42 +15917,6 @@
                 "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.9",
-            "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
-            "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
-            "dev": true,
-            "dependencies": {
-                "available-typed-arrays": "^1.0.5",
-                "call-bind": "^1.0.2",
-                "for-each": "^0.3.3",
-                "gopd": "^1.0.1",
-                "has-tostringtag": "^1.0.0",
-                "is-typed-array": "^1.1.10"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/wildcard": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
@@ -16988,6 +15956,54 @@
                 "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
             }
         },
+        "node_modules/wrap-ansi-cjs": {
+            "name": "wrap-ansi",
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+            }
+        },
+        "node_modules/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi/node_modules/ansi-styles": {
             "version": "4.3.0",
             "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -17089,7 +16105,8 @@
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
             "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-            "dev": true
+            "dev": true,
+            "license": "ISC"
         },
         "node_modules/yaml": {
             "version": "1.10.2",
diff --git a/package.json b/package.json
index b716f57ec3ab88dfbe7ea7fbe51711794005e9fe..89c6b41fbb9b0b90413a26d0ae6b2ec6fc88ad6b 100644
--- a/package.json
+++ b/package.json
@@ -20,12 +20,12 @@
     },
     "devDependencies": {
         "@axe-core/playwright": "^4.6.1",
-        "@babel/core": "^7.17.9",
-        "@babel/eslint-parser": "^7.17.0",
+        "@babel/core": "^7.26.0",
+        "@babel/eslint-parser": "^7.25.9",
         "@babel/plugin-syntax-dynamic-import": "^7.8.3",
-        "@babel/plugin-transform-runtime": "^7.12.1",
-        "@babel/preset-env": "^7.12.1",
-        "@babel/register": "^7.12.1",
+        "@babel/plugin-transform-runtime": "^7.25.9",
+        "@babel/preset-env": "^7.26.0",
+        "@babel/register": "^7.25.9",
         "@ckeditor/ckeditor5-alignment": "^36.x",
         "@ckeditor/ckeditor5-autoformat": "^36.x",
         "@ckeditor/ckeditor5-basic-styles": "^36.x",
@@ -55,8 +55,7 @@
         "@ckeditor/ckeditor5-theme-lark": "^36.x",
         "@ckeditor/ckeditor5-typing": "^36.x",
         "@ckeditor/ckeditor5-upload": "^36.x",
-        "@ckeditor/ckeditor5-vue2": "^3.0.1",
-        "@elan-ev/reststate-vuex": "~1.0.5",
+        "@ckeditor/ckeditor5-vue": "^5.1.0",
         "@fullcalendar/core": "^4.3.1",
         "@fullcalendar/daygrid": "^4.3.0",
         "@fullcalendar/interaction": "^4.3.0",
@@ -64,17 +63,17 @@
         "@fullcalendar/resource-timegrid": "^4.3.0",
         "@fullcalendar/resource-timeline": "^4.3.0",
         "@fullcalendar/timegrid": "^4.3.0",
-        "@johmun/vue-tags-input": "^2.1.0",
         "@playwright/test": "^1.33.0",
         "@popperjs/core": "^2.11.2",
         "@types/jquery": "^3.5.16",
         "@types/jqueryui": "^1.12.16",
         "@types/lodash": "^4.14.191",
-        "@vue/eslint-config-typescript": "^12.0.0",
+        "@vue/compiler-sfc": "^3.5.13",
+        "@vue/eslint-config-typescript": "^13.0.0",
         "altcha": "^0.3.2",
-        "autoprefixer": "^10.2.5",
+        "autoprefixer": "^10.4.20",
         "axios": "^0.21.0",
-        "babel-loader": "^8.2.1",
+        "babel-loader": "^9.2.1",
         "blueimp-file-upload": "10.31.0",
         "buffer": "^6.0.3",
         "chart.js": "^2.9.4",
@@ -82,18 +81,19 @@
         "ckeditor5-math": "^36.x",
         "colorpare": "^2.2.0",
         "cropperjs": "1.5.9",
-        "css-loader": "^5.0.1",
-        "css-minimizer-webpack-plugin": "^1.1.5",
+        "css-loader": "^7.1.2",
+        "css-minimizer-webpack-plugin": "^7.0.0",
         "dotenv": "^16.0.3",
         "easygettext": "^2.17.0",
         "es6-promise": "4.2.8",
-        "eslint": "^7.32.0",
-        "eslint-plugin-vue": "^9.10.0",
-        "eslint-webpack-plugin": "^3.1.1",
-        "expose-loader": "1.0.1",
+        "eslint": "^8.57.1",
+        "eslint-plugin-vue": "^9.28.0",
+        "eslint-webpack-plugin": "^4.2.0",
+        "expose-loader": "^5.0.0",
         "favico.js": "0.3.10",
         "file-saver": "^2.0.5",
-        "focus-trap-vue": "^1.1.1",
+        "focus-trap": "^7.6.2",
+        "focus-trap-vue": "^4.0.3",
         "highlight.js": "10.5.0",
         "jest": "^29.5.0",
         "jest-environment-jsdom": "^29.5.0",
@@ -109,43 +109,41 @@
         "jszip": "^3.8.0",
         "lodash": "^4.17.20",
         "md5": "^2.3.0",
-        "mini-css-extract-plugin": "1.3.1",
-        "mitt": "2.1.0",
+        "mini-css-extract-plugin": "^2.9.2",
+        "mitt": "^3.0.1",
         "mp3tag.js": "3.7.1",
         "multiselect": "0.9.12",
         "pdfjs-dist": "^2.6.347",
-        "portal-vue": "^2.1.7",
-        "postcss": "^8.1.8",
-        "postcss-loader": "4.1.0",
+        "pinia": "^2.2.8",
+        "portal-vue": "^3.0.0",
+        "postcss": "^8.4.49",
+        "postcss-loader": "^8.1.1",
         "postcss-scss": "^4.0.4",
         "raw-loader": "^4.0.2",
         "sanitize-html": "^2.7.0",
         "sass": "^1.29.0",
-        "sass-loader": "^10.1.0",
+        "sass-loader": "^16.0.4",
         "select2": "4.0.13",
         "sprintf-js": "^1.0.3",
         "stream-browserify": "^3.0.0",
-        "style-loader": "^2.0.0",
+        "style-loader": "^4.0.0",
         "svgo": "3.3.2",
         "tablesorter": "2.31.3",
-        "ts-loader": "^9.4.2",
-        "typescript": "^5.0.2",
-        "vrp-vue-resizable": "1.2.7",
-        "vue": "^2.7.14",
-        "vue-dragscroll": "^3.0.1",
-        "vue-gettext": "^2.1.12",
-        "vue-loader": "^15.9.8",
-        "vue-router": "^3.5.1",
-        "vue-select": "^3.11.2",
-        "vue-template-babel-compiler": "^1.2.0",
-        "vue-template-compiler": "^2.6.12",
-        "vue-twentytwenty": "^0.10.1",
+        "ts-loader": "^9.5.1",
+        "typescript": "^5.7.2",
+        "vue": "^3.5.13",
+        "vue-dragscroll": "^4.0.6",
+        "vue-loader": "^17.4.2",
+        "vue-resizable": "^2.1.7",
+        "vue-router": "^4.5.0",
+        "vue-select": "^4.0.0-beta.6",
         "vue-typer": "^1.2.0",
-        "vuedraggable": "^2.24.3",
-        "vuex": "^3.6.2",
-        "webpack": "^5.70.0",
-        "webpack-cli": "^4.10.0",
-        "webpack-merge": "5.4.0",
+        "vue3-gettext": "^3.0.0-beta.5",
+        "vuedraggable": "^4.1.0",
+        "vuex": "^4.1.0",
+        "webpack": "^5.97.0",
+        "webpack-cli": "^5.1.4",
+        "webpack-merge": "^6.0.1",
         "webpack-notifier": "^1.15.0"
     },
     "babel": {
@@ -171,5 +169,8 @@
     },
     "eslint-junit": {
         "output": "./.reports/eslint-report.xml"
+    },
+    "dependencies": {
+        "@vojtechlanka/vue-tags-input": "^3.1.1"
     }
 }
diff --git a/resources/assets/javascripts/bootstrap/avatar.js b/resources/assets/javascripts/bootstrap/avatar.js
index 31d724dec535e9ae5235975688370fdb07c31021..9c1de1c7a0ccb659528806c1162b5f148236904c 100644
--- a/resources/assets/javascripts/bootstrap/avatar.js
+++ b/resources/assets/javascripts/bootstrap/avatar.js
@@ -4,13 +4,13 @@ STUDIP.domReady(() => {
     avatarTypes.forEach((type) => {
         if (document.getElementById(`avatar-${type}-app`)) {
             Promise.all([
-                STUDIP.loadChunk('avatar'),
+                STUDIP.loadChunk('vue'),
                 import(
                     /* webpackChunkName: "avatar-app" */
                     '@/vue/avatar-app.js'
                 ),
-            ]).then(([{ createApp }, { default: mountApp }]) => {
-                return mountApp(STUDIP, createApp, `#avatar-${type}-app`);
+            ]).then(([{ createApp, store }, { default: mountApp }]) => {
+                return mountApp(STUDIP, createApp, store, `#avatar-${type}-app`);
             });
         }
     });
diff --git a/resources/assets/javascripts/bootstrap/courseware.js b/resources/assets/javascripts/bootstrap/courseware.js
index b90cc454b47e6955bcea91bd2a5ed789d1a7b005..72503e0deebfe208378c2c2a449d88535e5cecae 100644
--- a/resources/assets/javascripts/bootstrap/courseware.js
+++ b/resources/assets/javascripts/bootstrap/courseware.js
@@ -6,8 +6,8 @@ STUDIP.domReady(() => {
                 /* webpackChunkName: "courseware-shelf-app" */
                 '@/vue/courseware-shelf-app.js'
             ),
-        ]).then(([{ createApp }, { default: mountApp }]) => {
-            return mountApp(STUDIP, createApp, '#courseware-shelf-app');
+        ]).then(([{ createApp, store }, { default: mountApp }]) => {
+            return mountApp(STUDIP, createApp, store, '#courseware-shelf-app');
         });
     }
 
@@ -18,8 +18,8 @@ STUDIP.domReady(() => {
                 /* webpackChunkName: "courseware-index-app" */
                 '@/vue/courseware-index-app.js'
             ),
-        ]).then(([{ createApp }, { default: mountApp }]) => {
-            return mountApp(STUDIP, createApp, '#courseware-index-app');
+        ]).then(([{ createApp, store }, { default: mountApp }]) => {
+            return mountApp(STUDIP, createApp, store, '#courseware-index-app');
         });
     }
 
@@ -30,8 +30,8 @@ STUDIP.domReady(() => {
                 /* webpackChunkName: "courseware-activities-app" */
                 '@/vue/courseware-activities-app.js'
                 ),
-        ]).then(([{ createApp }, { default: mountApp }]) => {
-            return mountApp(STUDIP, createApp, '#courseware-activities-app');
+        ]).then(([{ createApp, store }, { default: mountApp }]) => {
+            return mountApp(STUDIP, createApp, store, '#courseware-activities-app');
         });
     }
 
@@ -42,8 +42,8 @@ STUDIP.domReady(() => {
                 /* webpackChunkName: "courseware-tasks-app" */
                 '@/vue/courseware-tasks-app.js'
             ),
-        ]).then(([{ createApp }, { default: mountApp }]) => {
-            return mountApp(STUDIP, createApp, '#courseware-tasks-app');
+        ]).then(([{ createApp, store }, { default: mountApp }]) => {
+            return mountApp(STUDIP, createApp, store, '#courseware-tasks-app');
         });
     }
 
@@ -54,8 +54,8 @@ STUDIP.domReady(() => {
                 /* webpackChunkName: "courseware-content-bookmark-app" */
                 '@/vue/courseware-content-bookmark-app.js'
             ),
-        ]).then(([{ createApp }, { default: mountApp }]) => {
-            return mountApp(STUDIP, createApp, '#courseware-content-bookmark-app');
+        ]).then(([{ createApp, store }, { default: mountApp }]) => {
+            return mountApp(STUDIP, createApp, store, '#courseware-content-bookmark-app');
         });
     }
 
@@ -66,8 +66,8 @@ STUDIP.domReady(() => {
                 /* webpackChunkName: "courseware-content-bookmark-app" */
                 '@/vue/courseware-admin-app.js'
             ),
-        ]).then(([{ createApp }, { default: mountApp }]) => {
-            return mountApp(STUDIP, createApp, '#courseware-admin-app');
+        ]).then(([{ createApp, store }, { default: mountApp }]) => {
+            return mountApp(STUDIP, createApp, store, '#courseware-admin-app');
         });
     }
 
@@ -78,8 +78,8 @@ STUDIP.domReady(() => {
                 /* webpackChunkName: "courseware-public-app" */
                 '@/vue/courseware-public-app.js'
             ),
-        ]).then(([{ createApp }, { default: mountApp }]) => {
-            return mountApp(STUDIP, createApp, '#courseware-public-app');
+        ]).then(([{ createApp, store }, { default: mountApp }]) => {
+            return mountApp(STUDIP, createApp, store, '#courseware-public-app');
         });
     }
 
@@ -90,8 +90,8 @@ STUDIP.domReady(() => {
                 /* webpackChunkName: "courseware-content-releases-app" */
                 '@/vue/courseware-content-releases-app.js'
             ),
-        ]).then(([{ createApp }, { default: mountApp }]) => {
-            return mountApp(STUDIP, createApp, '#courseware-content-releases-app');
+        ]).then(([{ createApp, store }, { default: mountApp }]) => {
+            return mountApp(STUDIP, createApp, store, '#courseware-content-releases-app');
         });
     }
 
@@ -102,8 +102,8 @@ STUDIP.domReady(() => {
                 /* webpackChunkName: "courseware-comments-app" */
                 '@/vue/courseware-comments-app.js'
             ),
-        ]).then(([{ createApp }, { default: mountApp }]) => {
-            return mountApp(STUDIP, createApp, '#courseware-comments-app');
+        ]).then(([{ createApp, store }, { default: mountApp }]) => {
+            return mountApp(STUDIP, createApp, store, '#courseware-comments-app');
         });
     }
 
diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js
index e159699421b56a57a8797974e4f505215749b4f9..bd863eb576ecc7a5ff0fb8a3ead7980bd873f207 100644
--- a/resources/assets/javascripts/bootstrap/forms.js
+++ b/resources/assets/javascripts/bootstrap/forms.js
@@ -1,5 +1,6 @@
-import { $gettext, $gettextInterpolate } from '../lib/gettext';
+import { $gettext } from '../lib/gettext';
 import Report from '../lib/report.ts';
+import Dialog from "../lib/dialog";
 
 // Allow fieldsets to collapse
 $(document).on(
@@ -242,12 +243,13 @@ function createSelect2(element) {
 }
 
 STUDIP.ready(function () {
-    let forms = window.document.querySelectorAll('form.default.studipform:not(.vueified)');
+    let forms = window.document.querySelectorAll('.studipform:not(.vueified)');
     if (forms.length > 0) {
         STUDIP.Vue.load().then(({createApp}) => {
             forms.forEach(f => {
-                createApp({
-                    el: f,
+                f.classList.add('vueified');
+
+                const app = createApp({
                     data() {
                         let params = JSON.parse(f.dataset.inputs);
                         params.STUDIPFORM_REQUIRED = f.dataset.required ? JSON.parse(f.dataset.required) : [];
@@ -343,9 +345,9 @@ STUDIP.ready(function () {
                                             note.description = $(this).data('validation_requirement');
                                         }
                                         if (this.validity.tooShort) {
-                                            note.description = $gettextInterpolate(
-                                                $gettext('Geben Sie mindestens %{min} Zeichen ein.'),
-                                                {min: this.minLength}
+                                            note.description = $gettext(
+                                                'Geben Sie mindestens %{min} Zeichen ein.',
+                                                { min: this.minLength }
                                             );
                                         }
                                         if (this.validity.valueMissing) {
@@ -353,9 +355,9 @@ STUDIP.ready(function () {
                                                 note.description = $gettext('Dieses Feld muss ausgewählt sein.');
                                             } else {
                                                 if (this.minLength > 0) {
-                                                    note.description = $gettextInterpolate(
-                                                        $gettext('Hier muss ein Wert mit mindestens %{min} Zeichen eingetragen werden.'),
-                                                        {min: this.minLength}
+                                                    note.description = $gettext(
+                                                        'Hier muss ein Wert mit mindestens %{min} Zeichen eingetragen werden.',
+                                                        { min: this.minLength }
                                                     );
                                                 } else {
                                                     note.description = $gettext('Hier muss ein Wert eingetragen werden.');
@@ -419,10 +421,19 @@ STUDIP.ready(function () {
                             return orderedNotes;
                         }
                     },
-                    mounted () {
-                        $(this.$el).addClass("vueified");
+                    mounted() {
+                        if (this.$el.closest('.ui-dialog')) {
+                            const cancelButton = this.$el.querySelector('footer .button.cancel:last-of-type');
+                            if (cancelButton) {
+                                cancelButton.addEventListener('click', (e) => {
+                                    Dialog.close();
+                                    e.preventDefault();
+                                })
+                            }
+                        }
                     }
                 });
+                app.mount(f);
             });
         });
     }
@@ -435,12 +446,8 @@ STUDIP.ready(function () {
     if (simple_vue_items.length > 0) {
         STUDIP.Vue.load().then(({createApp}) => {
             simple_vue_items.forEach(f => {
-                createApp({
-                    el: f,
-                    mounted() {
-                        this.$el.classList.add('vueified');
-                    }
-                });
+                f.classList.add('vueified');
+                createApp().mount(f);
             });
         });
     }
diff --git a/resources/assets/javascripts/bootstrap/mvv_difflog.js b/resources/assets/javascripts/bootstrap/mvv_difflog.js
index 8ade9181925dd45cd2980a6c81cd719e686ca9ed..0d970ec6f0ff0d1092d289be35c3219d9badf4f8 100644
--- a/resources/assets/javascripts/bootstrap/mvv_difflog.js
+++ b/resources/assets/javascripts/bootstrap/mvv_difflog.js
@@ -1,4 +1,4 @@
-import { $gettext, $gettextInterpolate } from  '../lib/gettext';
+import { $gettext } from  '../lib/gettext';
 
 STUDIP.domReady(() => {
     $('del.diffdel').each(function() {
@@ -44,8 +44,8 @@ STUDIP.domReady(() => {
                     senddata,
                     function(data) {
                         if (data) {
-                            var info = $gettextInterpolate(
-                                $gettext('Entfernt von %{user} am %{time}'),
+                            var info = $gettext(
+                                'Entfernt von %{user} am %{time}',
                                 data
                             );
                             del.attr('title', info);
@@ -140,8 +140,8 @@ STUDIP.domReady(() => {
                     senddata,
                     function(data) {
                         if (data) {
-                            var info = $gettextInterpolate(
-                                $gettext('Änderung durch %{user} am %{time}'),
+                            var info = $gettext(
+                                'Änderung durch %{user} am %{time}',
                                 data
                             );
                             ins.attr('title', info);
@@ -175,8 +175,8 @@ STUDIP.domReady(() => {
                 );
                 function onSuccess(data) {
                     if (data) {
-                        var info = $gettextInterpolate(
-                            $gettext('Hinzugefügt von %{user} am %{time}'),
+                        var info = $gettext(
+                            'Hinzugefügt von %{user} am %{time}',
                             data
                         );
                         curtable.attr('title', info);
@@ -210,8 +210,8 @@ STUDIP.domReady(() => {
                 );
                 function onSuccess(data) {
                     if (data) {
-                        var info = $gettextInterpolate(
-                            $gettext('Entfernt von %{user} am %{time}'),
+                        var info = $gettext(
+                            'Entfernt von %{user} am %{time}',
                             data
                         );
                         curtable.attr('title', info);
diff --git a/resources/assets/javascripts/bootstrap/oer.js b/resources/assets/javascripts/bootstrap/oer.js
index 2b471496a64a13ba051119b69d50593b4c974831..0854e8bf289918c6de3a286b696265b52dddc468 100644
--- a/resources/assets/javascripts/bootstrap/oer.js
+++ b/resources/assets/javascripts/bootstrap/oer.js
@@ -1,5 +1,3 @@
-import Quicksearch from '../../../vue/components/Quicksearch.vue';
-
 STUDIP.domReady(() => {
     if (jQuery(".oer_search").length) {
         STUDIP.OER.initSearch();
@@ -55,16 +53,15 @@ STUDIP.ready(() => {
     if ($('.oercampus_editmaterial').length) {
 
         STUDIP.Vue.load().then(({createApp}) => {
-            STUDIP.OER.EditApp = createApp({
-                el: '.oercampus_editmaterial',
+            const app = createApp({
                 data() {
                     return {
                         name: $('.oercampus_editmaterial input.oername').val(),
-                        logo_url: $('.oercampus_editmaterial .logo_file').data("oldurl"),
-                        customlogo: $('.oercampus_editmaterial .logo_file').data("customlogo"),
+                        logo_url: $('.oercampus_editmaterial .logo_file').data("oldurl") ?? null,
+                        customlogo: $('.oercampus_editmaterial .logo_file').data("customlogo") == '1',
                         filename: $('.oercampus_editmaterial .file.drag-and-drop').data("filename"),
                         filesize: $('.oercampus_editmaterial .file.drag-and-drop').data("filesize"),
-                        tags: $('.oercampus_editmaterial .oer_tags').data("defaulttags"),
+                        tags: $('.oercampus_editmaterial .oer_tags').data("defaulttags") ?? [],
                         minimumTags: 5
                     };
                 },
@@ -87,10 +84,9 @@ STUDIP.ready(() => {
                     },
                     editImage: function (event) {
                         let reader = new FileReader();
-                        let vue = this;
-                        reader.addEventListener("load", function () {
-                            vue.logo_url = reader.result;
-                            vue.customlogo = true;
+                        reader.addEventListener("load", () => {
+                            this.logo_url = reader.result;
+                            this.customlogo = true;
                         }, false);
                         reader.readAsDataURL(
                             event.target.files.length > 0
@@ -137,8 +133,9 @@ STUDIP.ready(() => {
                         return result;
                     }
                 },
-                components: { Quicksearch }
             });
+            app.mount('.oercampus_editmaterial');
+            STUDIP.OER.EditApp = app;
         });
     }
 });
diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js
index 513c796cb4322d456ceae43e85cbd0a19accb55a..4b1ac6daf249abd87a62d8ff59c7cc7da809cad9 100644
--- a/resources/assets/javascripts/bootstrap/vue.js
+++ b/resources/assets/javascripts/bootstrap/vue.js
@@ -1,5 +1,37 @@
+import { defineAsyncComponent } from 'vue';
+
+function attachComponents(app, configuredComponents) {
+    configuredComponents.forEach(component => {
+        const name = component.split('/').reverse()[0];
+        app.component(name, defineAsyncComponent(() => {
+            const temp = import(`../../../vue/components/${component}.vue`);
+            temp.then(({default: c}) => {
+                const mounted = c.mounted ?? null;
+                c.mounted = function (...args) {
+                    if (
+                        this.$el instanceof Element
+                        && this.$el.closest('.studip-dialog')
+                        && this.$el.querySelector('[data-dialog-button]')
+                    ) {
+                        this.$el.closest('.studip-dialog')
+                            .querySelector('.ui-dialog-buttonpane')
+                            .remove();
+                    }
+                    if (mounted) {
+                        mounted.call(this, args);
+                    }
+                };
+                return c;
+            })
+            return temp;
+        }));
+    });
+}
+
 STUDIP.ready(() => {
-    document.querySelectorAll('[data-vue-app]:not([data-vue-app-created])').forEach((node) => {
+    document.querySelectorAll('[data-vue-app]:not([data-vue-app-created])').forEach(async (node) => {
+        node.dataset.vueAppCreated = 'true';
+
         const config = Object.assign(
             {
                 components: [],
@@ -9,96 +41,70 @@ STUDIP.ready(() => {
             JSON.parse(node.dataset.vueApp)
         );
 
-        let components = {};
-        config.components.forEach(component => {
-            const name = component.split('/').reverse()[0];
-            components[name] = () => {
-                // TODO: I wonder if this works with Vue3
+        const { createApp, store } = await STUDIP.Vue.load();
 
-                const temp = import(`../../../vue/components/${component}.vue`);
-                temp.then(({default: c}) => {
-                    const mounted = c.mounted ?? null;
-                    c.mounted = function (...args) {
-                        if (
-                            this.$el instanceof Element
-                            && this.$el.closest('.studip-dialog')
-                            && this.$el.querySelector('[data-dialog-button]')
-                        ) {
-                            this.$el.closest('.studip-dialog')
-                                .querySelector('.ui-dialog-buttonpane')
-                                .remove();
-                        }
-                        if (mounted) {
-                            mounted.call(this, args);
-                        }
-                    };
-                    return c;
-                })
-                return temp;
-            };
-        });
+        const promises = [Promise.resolve()];
 
-        STUDIP.Vue.load().then(({createApp, store, Vue}) => {
-            const promises = [Promise.resolve()];
+        for (const [index, name] of Object.entries(config.stores)) {
+            promises.push(
+                import(`../../../vue/store/${name}.js`).then(storeConfig => {
+                    store.registerModule(index, storeConfig.default);
 
-            for (const [index, name] of Object.entries(config.stores)) {
-                promises.push(
-                    import(`../../../vue/store/${name}.js`).then(storeConfig => {
-                        store.registerModule(index, storeConfig.default);
+                    const dataElement = document.getElementById(`vue-store-data-${index}`);
+                    if (dataElement) {
+                        const data = JSON.parse(dataElement.innerText);
+                        Object.keys(data).forEach(command => {
+                            store.commit(`${index}/${command}`, data[command]);
+                        });
 
-                        const dataElement = document.getElementById(`vue-store-data-${index}`);
-                        if (dataElement) {
-                            const data = JSON.parse(dataElement.innerText);
-                            Object.keys(data).forEach(command => {
-                                store.commit(`${index}/${command}`, data[command]);
-                            });
+                        dataElement.remove();
+                    }
+                })
+            );
+        }
 
-                            dataElement.remove();
-                        }
-                    })
-                );
-            }
+        const plugins = [];
+        for (const [plugin, filename] of Object.entries(config.plugins)) {
+            promises.push(
+                import(`../../../vue/plugins/${filename}.js`)
+                .then((temp) => plugins.push(temp[plugin]))
+            );
+        }
 
-            for (const [plugin, filename] of Object.entries(config.plugins)) {
-                promises.push(
-                    import(`../../../vue/plugins/${filename}.js`)
-                    .then((temp) => Vue.use(temp[plugin], { store }))
-                );
-            }
+        await Promise.all(promises);
 
-            Promise.all(promises).then(() => {
-                createApp({
-                    components,
-                    store,
+        const app = createApp({
+            store,
 
-                    beforeCreate() {
-                        STUDIP.Vue.emit('VueAppWillCreate', this);
-                    },
-                    created() {
-                        STUDIP.Vue.emit('VueAppDidCreate', this);
-                    },
-                    beforeMount() {
-                        STUDIP.Vue.emit('VueAppWillMount', this);
-                    },
-                    mounted() {
-                        STUDIP.Vue.emit('VueAppDidMount', this);
-                    },
-                    beforeUpdate() {
-                        STUDIP.Vue.emit('VueAppWillUpdate', this);
-                    },
-                    updated() {
-                        STUDIP.Vue.emit('VueAppDidUpdate', this);
-                    },
-                    beforeDestroy() {
-                        STUDIP.Vue.emit('VueAppWillDestroy', this);
-                    },
-                    destroyed() {
-                        STUDIP.Vue.emit('VueAppDidDestroy', this);
-                    },
-                }).$mount(node);
-            });
+            beforeCreate() {
+                STUDIP.Vue.emit('VueAppWillCreate', this);
+            },
+            created() {
+                STUDIP.Vue.emit('VueAppDidCreate', this);
+            },
+            beforeMount() {
+                STUDIP.Vue.emit('VueAppWillMount', this);
+            },
+            mounted() {
+                STUDIP.Vue.emit('VueAppDidMount', this);
+            },
+            beforeUpdate() {
+                STUDIP.Vue.emit('VueAppWillUpdate', this);
+            },
+            updated() {
+                STUDIP.Vue.emit('VueAppDidUpdate', this);
+            },
+            beforeUnmount() {
+                STUDIP.Vue.emit('VueAppWillUnmount', this);
+            },
+            unmounted() {
+                STUDIP.Vue.emit('VueAppDidUnmount', this);
+            },
         });
 
-        node.dataset.vueAppCreated = 'true';
+        attachComponents(app, config.components);
+        plugins.forEach(plugin => app.use(plugin, { store }))
+
+        app.mount(node);
     });
 });
diff --git a/resources/assets/javascripts/chunk-loader.js b/resources/assets/javascripts/chunk-loader.js
index 995b35f4d8f4cadaf5ef8f347b9eba9971ddd659..8cb692cf87eba19430c2c4c8419aea3854929545 100644
--- a/resources/assets/javascripts/chunk-loader.js
+++ b/resources/assets/javascripts/chunk-loader.js
@@ -31,16 +31,6 @@ export const loadChunk = function (chunk, { silent = false } = {}) {
             ]).then(([Vue]) => Vue);
             break;
 
-        case 'avatar':
-            promise = Promise.all([
-                STUDIP.loadChunk('vue'),
-                import(
-                    /* webpackChunkName: "avatar" */
-                    './chunks/avatar'
-                ),
-            ]).then(([Vue]) => Vue);
-            break;
-
         case 'code-highlight':
             promise = import(
                 /* webpackChunkName: "code-highlight" */
diff --git a/resources/assets/javascripts/chunks/avatar.js b/resources/assets/javascripts/chunks/avatar.js
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/resources/assets/javascripts/chunks/vue.js b/resources/assets/javascripts/chunks/vue.js
index 8d506a1dfc8c797a254db29ec796ce1107ff2e1f..80b111ee920231bdb04e95b4fabf91fd5289162a 100644
--- a/resources/assets/javascripts/chunks/vue.js
+++ b/resources/assets/javascripts/chunks/vue.js
@@ -1,70 +1,89 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import Router from "vue-router";
-import eventBus from '../lib/event-bus.ts';
-import GetTextPlugin from 'vue-gettext';
-import { getLocale, getVueConfig } from '../lib/gettext';
+import { createApp as vueCreateApp } from 'vue';
+import { createStore as vuexCreateStore } from 'vuex';
+import eventBus from '../lib/event-bus';
+import gettext from '../lib/gettext';
 import PortalVue from 'portal-vue';
 import BaseComponents from '../../../vue/base-components.js';
 import BaseDirectives from "../../../vue/base-directives.js";
 import StudipStore from "../../../vue/store/StudipStore.js";
-import CKEditor from '@ckeditor/ckeditor5-vue2';
+import { resourceModule } from '@/assets/javascripts/lib/reststate-vuex.js';
+import axios from 'axios';
 
-// Setup gettext
-Vue.use(GetTextPlugin, getVueConfig());
-eventBus.on('studip:set-locale', (locale) => {
-    Vue.config.language = locale;
-})
+import CKEditor from '@ckeditor/ckeditor5-vue';
 
-// Register global components and directives
-registerGlobalComponents();
-registerGlobalDirectives();
+const getHttpClient = () =>
+    axios.create({
+        baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
+        headers: {
+            'Content-Type': 'application/vnd.api+json',
+        },
+    });
 
-// Setup store and default Stud.IP store
-Vue.use(Vuex);
-const store = new Vuex.Store({});
+const httpClient = getHttpClient();
 
-store.registerModule('studip', StudipStore);
+const createStore = () => {
+    const store = vuexCreateStore({});
 
-// Setup router and PortalVue
-Vue.use(Router);
-Vue.use(PortalVue);
+    store.registerModule('studip', StudipStore);
 
-// Define our own global mixin for Vue
-Vue.mixin({
-    methods: {
-        globalEmit(...args) {
-            eventBus.emit(...args);
-        },
-        globalOn(...args) {
-            eventBus.on(...args);
-        },
-        globalOff(...args) {
-            eventBus.off(...args);
-        },
-        getStudipConfig: store.getters['studip/getConfig']
-    },
-});
+    STUDIP.jsonapi_schemas.forEach((name) => {
+        store.registerModule(name, resourceModule({ name, httpClient }));
+    });
 
-Vue.use(CKEditor);
+    return store;
+}
+
+// Setup store
+const store = createStore();
 
 // Define createApp function
-function createApp(options, ...args) {
-    Vue.config.language = getLocale();
-    return new Vue({ store, ...options }, ...args);
+function createApp(options = {}, ...args) {
+    const app = vueCreateApp({ store, ...options }, ...args);
+
+    app.config.compilerOptions.whitespace = 'condense';
+
+    // Define our own global mixin for Vue
+    app.mixin({
+        methods: {
+            globalEmit(...args) {
+                eventBus.emit(...args);
+            },
+            globalOn(...args) {
+                eventBus.on(...args);
+            },
+            globalOff(...args) {
+                eventBus.off(...args);
+            },
+            getStudipConfig: store.getters['studip/getConfig']
+        },
+    });
+
+    app.use(CKEditor);
+    app.use(gettext);
+    app.use(PortalVue);
+    app.use(store);
+
+    // Register global components and directives
+    registerGlobalComponents(app);
+    registerGlobalDirectives(app);
+
+    if (options.el) {
+        app.mount(options.el);
+    }
+    return app;
 }
 
 // Define global registration functions for components and directives
-function registerGlobalComponents() {
+function registerGlobalComponents(app) {
     for (const [name, component] of Object.entries(BaseComponents)) {
-        Vue.component(name, component);
+        app.component(name, component);
     }
 }
 
-function registerGlobalDirectives() {
+function registerGlobalDirectives(app) {
     for (const [name, directive] of Object.entries(BaseDirectives)) {
-        Vue.directive(name, directive);
+        app.directive(name, directive);
     }
 }
 
-export { Vue, createApp, eventBus, store };
+export { createApp, eventBus, store, httpClient };
diff --git a/resources/assets/javascripts/jquery-bundle.js b/resources/assets/javascripts/jquery-bundle.js
index bd1642260eae74610ced75bd88d8b8d1c3b54b13..bfa9eefcd0c7d49877d0641146039d6ab9c0635a 100644
--- a/resources/assets/javascripts/jquery-bundle.js
+++ b/resources/assets/javascripts/jquery-bundle.js
@@ -1,6 +1,6 @@
-import 'expose-loader?exposes[]=$&exposes[]=jQuery!jquery';
+import $ from 'expose-loader?exposes=$,jQuery!jquery';
 
-import { setLocale } from './lib/gettext';
+ import { setLocale } from './lib/gettext';
 
 import 'jquery-ui/ui/widget.js';
 import 'jquery-ui/ui/position.js';
diff --git a/resources/assets/javascripts/lib/admission.js b/resources/assets/javascripts/lib/admission.js
index 4b135118930d467452fbea9e3db376e99a999ce5..992c8be1be85133592234a3079ed16697338118a 100644
--- a/resources/assets/javascripts/lib/admission.js
+++ b/resources/assets/javascripts/lib/admission.js
@@ -23,7 +23,7 @@ const Admission = {
 
     getCourses: function(targetUrl) {
         var courseFilter = $('input[name="course_filter"]').val();
-        if (courseFilter == '') {
+        if (courseFilter === '') {
             courseFilter = '%%%';
         }
         var data = {
diff --git a/resources/assets/javascripts/lib/blubber.js b/resources/assets/javascripts/lib/blubber.js
index 4e724b46cbd0f1cd2c49a691dbb02f0c644420c2..783abe18ae0639298303a7e4530f1a9015319575 100644
--- a/resources/assets/javascripts/lib/blubber.js
+++ b/resources/assets/javascripts/lib/blubber.js
@@ -1,3 +1,5 @@
+import { resolveComponent, h } from "vue";
+
 const Blubber = {
     init() {
         const blubberPage = document.querySelector('#blubber-index, #messenger-course, .blubber_panel.vueinstance');
@@ -18,12 +20,13 @@ const Blubber = {
         function connectBlubber(blubberPanel, componentName) {
             return Promise.all([window.STUDIP.Vue.load(), Blubber.plugin()]).then(
                 ([{ Vue, createApp, store }, BlubberPlugin]) => {
-                    Vue.use(BlubberPlugin, { store });
                     const { initialThreadId, search } = blubberPanel.dataset;
-                    return createApp({
-                        el: blubberPanel,
-                        render: (h) => h(Vue.component(componentName), { props: { initialThreadId, search } }),
+                    const app = createApp({
+                        render: () => h(resolveComponent(componentName), { initialThreadId, search }),
                     });
+                    app.use(BlubberPlugin, { store });
+                    app.mount(blubberPanel);
+                    return app;
                 }
             );
         }
diff --git a/resources/assets/javascripts/lib/datetime.js b/resources/assets/javascripts/lib/datetime.js
index 50f91f22e5b3c19002b74e93ce932016e401407f..04d72605bcbff58d947fcb5f24a01b2044853932 100644
--- a/resources/assets/javascripts/lib/datetime.js
+++ b/resources/assets/javascripts/lib/datetime.js
@@ -1,4 +1,4 @@
-import { $gettext, $gettextInterpolate } from "./gettext.ts";
+import { $gettext } from "./gettext.ts";
 
 
 const DateTime = {
@@ -43,8 +43,8 @@ const DateTime = {
                 return $gettext('Jetzt');
             }
             if (now - date < 2 * 60 * 60 * 1000) {
-                return $gettextInterpolate(
-                    $gettext('Vor %{ minutes } Minuten'),
+                return $gettext(
+                    'Vor %{ minutes } Minuten',
                     {minutes: Math.floor((now - date) / (1000 * 60))}
                 );
             }
diff --git a/resources/assets/javascripts/lib/files.js b/resources/assets/javascripts/lib/files.js
index d05112decd89b1a6d9cd1154e38790a4ad1c7cfa..39bd8934b28568c65431f7f7cdffafe1a4ea3d79 100644
--- a/resources/assets/javascripts/lib/files.js
+++ b/resources/assets/javascripts/lib/files.js
@@ -1,6 +1,7 @@
 import { $gettext } from './gettext';
 import Dialog from './dialog.js';
 import FilesTable from '../../../vue/components/FilesTable.vue';
+import { h } from 'vue';
 
 const Files = {
     init () {
@@ -8,8 +9,7 @@ const Files = {
             && jQuery("#files_table_form").length) {
 
             STUDIP.Vue.load().then(({createApp}) => {
-                this.filesapp = createApp({
-                    el: "#content",
+                const app = createApp({
                     data() {
                         return {
                             files: jQuery("#files_table_form").data("files") || [],
@@ -27,6 +27,9 @@ const Files = {
                             }
                             return false;
                         },
+                        pushFile(file) {
+                            this.files.push(file);
+                        },
                         removeFile(id) {
                             this.files = this.files.filter(file => file.id != id)
                         },
@@ -36,14 +39,15 @@ const Files = {
                             });
                         }
                     },
-                    components: { FilesTable, },
                     updated () {
                         this.onUpdated();
                     },
                     created () {
                         this.onUpdated();
-                    }
+                    },
                 });
+                app.component('files-table', FilesTable);
+                this.filesapp = app.mount('#files_table_form');
             });
         }
 
@@ -258,7 +262,7 @@ const Files = {
                 }
             }
             if (insert) {
-                STUDIP.Files.filesapp.files.push(value);
+                STUDIP.Files.filesapp.pushFile(value);
             }
         });
         $(document).trigger('refresh-handlers');
diff --git a/resources/assets/javascripts/lib/gettext.ts b/resources/assets/javascripts/lib/gettext.ts
index 23daaaa075e371bd14b74cf7d4fb048792539b90..c63831ebab856f484930033d897edbcc2d07eafc 100644
--- a/resources/assets/javascripts/lib/gettext.ts
+++ b/resources/assets/javascripts/lib/gettext.ts
@@ -1,4 +1,4 @@
-import { translate } from 'vue-gettext';
+import { createGettext, LanguageData } from 'vue3-gettext';
 import * as defaultTranslations from '../../../../locale/de/LC_MESSAGES/js-resources.json';
 import eventBus from './event-bus';
 
@@ -15,73 +15,92 @@ interface InstalledLanguages {
     [key: string]: InstalledLanguage;
 }
 
-type TranslationDict = StringDict;
+type Translation = LanguageData;
 
-interface TranslationDicts {
-    [key: string]: TranslationDict | null;
-}
+type Translations = {
+    [language: string]: LanguageData;
+};
 
 const DEFAULT_LANG = 'de_DE';
 const DEFAULT_LANG_NAME = 'Deutsch';
 
 const state = getInitialState();
 
-const $gettext = translate.gettext.bind(translate);
-const $ngettext = translate.ngettext.bind(translate);
-const $gettextInterpolate = translate.gettextInterpolate.bind(translate);
-
-export { $gettext, $ngettext, $gettextInterpolate, translate, getLocale, setLocale, getVueConfig };
+const gettext = createGettext({
+    availableLanguages: getAvailableLanguages(),
+    defaultLanguage: state.locale,
+    silent: false,
+    translations: {
+        [DEFAULT_LANG]: {}
+    },
+    mutedLanguages: [DEFAULT_LANG],
+    setGlobalProperties: true,
+    globalProperties: {
+        language: ['$language'],
+        gettext: ['$gettext'],
+        pgettext: ['$pgettext'],
+        ngettext: ['$ngettext'],
+        npgettext: ['$npgettext'],
+        interpolate: ['$gettextInterpolate'],
+    },
+    provideDirective: true,
+    provideComponent: true,
+});
+
+setLocale(state.locale);
+
+export default gettext;
+
+async function updateTranslations() {
+    let translations: Translations = {};
+
+    for (const [key, value] of Object.entries(getAvailableLanguages())) {
+        if (state.locale === key) {
+            const translation = await getTranslations(key);
+            translations[key] = translation;
+        }
+    }
+    gettext.translations = translations;
+}
 
-function getLocale() {
+export function getLocale() {
     return state.locale;
 }
 
-async function setLocale(locale = getInitialLocale()) {
+export async function setLocale(locale = getInitialLocale()) {
     if (!(locale in getInstalledLanguages())) {
         throw new Error('Invalid locale: ' + locale);
     }
 
     state.locale = locale;
     if (state.translations[state.locale] === null) {
-        const translations: TranslationDict = await getTranslations(state.locale);
+        const translations: Translation = await getTranslations(state.locale);
         state.translations[state.locale] = translations;
     }
 
-    translate.initTranslations(state.translations, {
-        getTextPluginMuteLanguages: [DEFAULT_LANG],
-        getTextPluginSilent: false,
-        language: state.locale,
-        silent: false,
-    });
+    updateTranslations();
 
     eventBus.emit('studip:set-locale', state.locale);
 }
 
-function getVueConfig() {
-    const availableLanguages = Object.entries(getInstalledLanguages()).reduce((memo, [lang, { name }]) => {
+function getAvailableLanguages() {
+    return Object.entries(getInstalledLanguages()).reduce((memo, [lang, { name }]) => {
         memo[lang] = name;
 
         return memo;
     }, {} as StringDict);
-
-    return {
-        availableLanguages,
-        defaultLanguage: DEFAULT_LANG,
-        muteLanguages: [DEFAULT_LANG],
-        silent: false,
-        translations: state.translations,
-    };
 }
 
+
 function getInitialState() {
-    const translations: TranslationDicts = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => {
-        memo[lang] = lang === DEFAULT_LANG ? defaultTranslations : null;
+    const translations: Translations = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => {
+        memo[lang] = lang === DEFAULT_LANG ? defaultTranslations : '';
 
         return memo;
-    }, {} as TranslationDicts);
+    }, {} as Translations);
 
     return {
-        locale: DEFAULT_LANG,
+        locale: getInitialLocale(),
         translations,
     };
 }
@@ -100,7 +119,7 @@ function getInstalledLanguages(): InstalledLanguages {
     return window?.STUDIP?.INSTALLED_LANGUAGES ?? { [DEFAULT_LANG]: { name: DEFAULT_LANG_NAME, selected: true } };
 }
 
-async function getTranslations(locale: string): Promise<TranslationDict> {
+async function getTranslations(locale: string): Promise<Translation> {
     try {
         const language = locale.split(/[_-]/)[0];
         const translation = await import(`../../../../locale/${language}/LC_MESSAGES/js-resources.json`);
@@ -112,3 +131,7 @@ async function getTranslations(locale: string): Promise<TranslationDict> {
         return {};
     }
 }
+
+export const $gettext = gettext.$gettext;
+export const $ngettext = gettext.$ngettext;
+export const $gettextInterpolate = gettext.interpolate;
diff --git a/resources/assets/javascripts/lib/personal_notifications.js b/resources/assets/javascripts/lib/personal_notifications.js
index d05fbaa08bb547ced474b6709bd6bc787ed6b635..34890880ca65df24525783670d78c58bc763f21b 100644
--- a/resources/assets/javascripts/lib/personal_notifications.js
+++ b/resources/assets/javascripts/lib/personal_notifications.js
@@ -1,7 +1,7 @@
 import Favico from 'favico.js';
 import Cache from './cache.js';
 import PageLayout from './page_layout.js';
-import { $gettextInterpolate, $ngettext } from './gettext';
+import { $ngettext } from './gettext';
 
 var stack = {};
 var audio_notification = false;
@@ -189,8 +189,13 @@ const PersonalNotifications = {
         }
         if (old_count !== count) {
             $('#notification_marker .count').text(count);
-            let notification_text = $ngettext('%{ count } Benachrichtigung', '%{ count } Benachrichtigungen', count);
-            $('#notification_marker').attr('title', $gettextInterpolate(notification_text, {count: count}));
+            let notification_text = $ngettext(
+                '%{ count } Benachrichtigung',
+                '%{ count } Benachrichtigungen',
+                count,
+                { count }
+            );
+            $('#notification_marker').attr('title', notification_text);
             updateFavicon(count);
             $('#notification-container .mark-all-as-read').toggleClass('invisible', count < 2);
         }
diff --git a/resources/assets/javascripts/lib/reststate-client.js b/resources/assets/javascripts/lib/reststate-client.js
new file mode 100644
index 0000000000000000000000000000000000000000..5514d1534ab00fee5ed19e4bcda1df9c54a1691e
--- /dev/null
+++ b/resources/assets/javascripts/lib/reststate-client.js
@@ -0,0 +1,141 @@
+function filterQueryString(obj) {
+    return Object.keys(obj)
+        .map(k => `filter[${k}]=${encodeURIComponent(obj[k])}`)
+        .join('&');
+}
+
+const getOptionsQuery = (optionsObject = {}) =>
+    Object.keys(optionsObject)
+        .filter(k => typeof optionsObject[k] !== 'undefined')
+        .map(k => `${k}=${encodeURIComponent(optionsObject[k])}`)
+        .join('&');
+
+const relatedResourceUrl = ({ parent, relationship }) => {
+     const builtUrl = `${parent.type}/${parent.id}/${relationship}`;
+
+    if (
+        parent.relationships
+        && Object.keys(parent.relationships).includes(relationship)
+    ) {
+        return parent.relationships[relationship].links?.related ?? builtUrl;
+    }
+    return builtUrl;
+};
+
+const extractData = response => response.data;
+
+const extractErrorResponse = error => {
+    if (error && error.response) {
+        throw error.response;
+    } else {
+        throw error;
+    }
+};
+
+class Resource {
+    constructor({ name, httpClient }) {
+        this.name = name;
+        this.api = httpClient;
+    }
+
+    all({ options = {} } = {}) {
+        let url;
+
+        if (options.url) {
+            ({ url } = options);
+        } else {
+            url = `${this.name}?${getOptionsQuery(options)}`;
+        }
+
+        return this.api.get(url).then(extractData).catch(extractErrorResponse);
+    }
+
+    find({ id, options } = {}) {
+        const url = `${this.name}/${id}?${getOptionsQuery(options)}`;
+
+        return this.api.get(url).then(extractData).catch(extractErrorResponse);
+    }
+
+    where({ filter, options } = {}) {
+        const queryString = filterQueryString(filter);
+        return this.api
+            .get(`${this.name}?${queryString}&${getOptionsQuery(options)}`)
+            .then(extractData)
+            .catch(extractErrorResponse);
+    }
+
+    related({ parent, relationship = this.name, options }) {
+        const baseUrl = relatedResourceUrl({ parent, relationship });
+        const url = `${baseUrl}?${getOptionsQuery(options)}`;
+        return this.api.get(url).then(extractData).catch(extractErrorResponse);
+    }
+
+    create(partialRecord) {
+        const record = Object.assign({}, partialRecord, { type: this.name });
+        const requestData = { data: record };
+        return this.api
+          .post(`${this.name}`, requestData)
+          .then(extractData)
+          .catch(extractErrorResponse);
+    }
+
+    createRelated(partialRecord) {
+        const record = Object.assign({}, partialRecord, { type: this.name });
+        const requestData = { data: record };
+        return this.api
+            .post(`${this.name}`, requestData)
+            .then(extractData)
+            .catch(extractErrorResponse);
+    }
+
+    updateRelationships(parent, relationship, records) {
+        // https://jsonapi.org/format/#crud-updating-to-many-relationships
+        const requestData = { data: records };
+        return this.api
+            .patch(
+                `${parent.type}/${parent.id}/relationships/${relationship}`,
+                requestData,
+            )
+            .then(extractData)
+            .catch(extractErrorResponse);
+    }
+
+    createRelationships(parent, relationship, records) {
+        // https://jsonapi.org/format/#crud-updating-to-many-relationships
+        const requestData = { data: records };
+        return this.api
+            .post(
+                `${parent.type}/${parent.id}/relationships/${relationship}`,
+                requestData,
+            )
+            .then(extractData)
+            .catch(extractErrorResponse);
+    }
+
+    removeRelationships(parent, relationship, records) {
+        // https://jsonapi.org/format/#crud-updating-to-many-relationships
+        const requestData = { data: records };
+        return this.api
+            .delete(
+                `${parent.type}/${parent.id}/relationships/${relationship}`,
+                requestData,
+            )
+            .then(extractData)
+            .catch(extractErrorResponse);
+    }
+
+    update(record) {
+        // http://jsonapi.org/faq/#wheres-put
+        const requestData = { data: record };
+        return this.api
+            .patch(`${this.name}/${record.id}`, requestData)
+            .then(extractData)
+            .catch(extractErrorResponse);
+    }
+
+    delete({ id }) {
+        return this.api.delete(`${this.name}/${id}`).catch(extractErrorResponse);
+    }
+}
+
+export default Resource;
diff --git a/resources/assets/javascripts/lib/reststate-vuex.js b/resources/assets/javascripts/lib/reststate-vuex.js
new file mode 100644
index 0000000000000000000000000000000000000000..a3dd6ca0e9e6e20d59a57861086f026b1766c743
--- /dev/null
+++ b/resources/assets/javascripts/lib/reststate-vuex.js
@@ -0,0 +1,522 @@
+import ResourceClient from './reststate-client.js';
+import { isEqual } from 'lodash';
+
+const STATUS_INITIAL = 'INITIAL';
+const STATUS_LOADING = 'LOADING';
+const STATUS_ERROR = 'ERROR';
+const STATUS_SUCCESS = 'SUCCESS';
+
+const storeRecord = records => newRecord => {
+    const existingRecord = records.find(r => r.id === newRecord.id);
+    if (existingRecord) {
+        Object.assign(existingRecord, newRecord);
+    } else {
+        records.push(newRecord);
+    }
+};
+
+const getResourceIdentifier = resource => {
+    if (!resource) {
+        return resource;
+    }
+
+    return {
+        type: resource.type,
+        id: resource.id,
+    };
+};
+
+const getRelationshipType = relationship => {
+    const data = Array.isArray(relationship.data)
+        ? relationship.data[0]
+        : relationship.data;
+
+    return data && data.type;
+};
+
+const storeIncluded = ({ commit, dispatch }, result) => {
+    if (result.included) {
+        // store the included records
+        result.included.forEach(relatedRecord => {
+            const action = `${relatedRecord.type}/storeRecord`;
+            dispatch(action, relatedRecord, { root: true });
+        });
+
+        // store the relationship for primary and secondary records
+        let allRecords = [...result.included];
+        if (Array.isArray(result.data)) {
+            allRecords = [...allRecords, ...result.data];
+        } else {
+            allRecords = [...allRecords, result.data];
+        }
+
+        allRecords.forEach(primaryRecord => {
+            if (primaryRecord.relationships) {
+                Object.keys(primaryRecord.relationships).forEach(relationshipName => {
+                    const relationship = primaryRecord.relationships[relationshipName];
+                    if (!relationship.data || relationship.data.length === 0) {
+                        return;
+                    }
+
+                    const type = getRelationshipType(relationship);
+                    let relatedIds;
+                    if (Array.isArray(relationship.data)) {
+                        relatedIds = relationship.data.map(
+                            relatedRecord => relatedRecord.id,
+                        );
+                    } else {
+                        ({ id: relatedIds } = relationship.data);
+                    }
+                    const options = {
+                        relatedIds,
+                        params: {
+                            parent: getResourceIdentifier(primaryRecord),
+                            relationship: relationshipName,
+                        },
+                    };
+                    const action = `${type}/storeRelated`;
+                    dispatch(action, options, { root: true });
+                });
+            }
+        });
+    }
+};
+
+const matches = criteria => test =>
+    Object.keys(criteria).every(key => isEqual(criteria[key], test[key]));
+
+const handleError = commit => errorResponse => {
+    commit('SET_STATUS', STATUS_ERROR);
+    commit('STORE_ERROR', errorResponse);
+    throw errorResponse;
+};
+
+const initialState = () => ({
+    records: [],
+    related: [],
+    filtered: [],
+    page: [],
+    error: null,
+    status: STATUS_INITIAL,
+    links: {},
+    lastCreated: null,
+    lastMeta: null,
+});
+
+const resourceModule = ({ name: resourceName, httpClient }) => {
+    const client = new ResourceClient({ name: resourceName, httpClient });
+
+    const getRelationshipIndex = params => {
+        const { parent, relationship = resourceName } = params;
+        const parentResourceIdentifier = getResourceIdentifier(parent);
+
+        return {
+            parent: parentResourceIdentifier,
+            relationship,
+        };
+    };
+
+    return {
+        namespaced: true,
+
+        state: initialState,
+
+        mutations: {
+            REPLACE_ALL_RECORDS: (state, records) => {
+                state.records = records;
+            },
+
+            REPLACE_ALL_RELATED: (state, related) => {
+                state.related = related;
+            },
+
+            SET_STATUS: (state, status) => {
+                state.status = status;
+            },
+
+            STORE_RECORD: (state, newRecord) => {
+                const { records } = state;
+
+                storeRecord(records)(newRecord);
+            },
+
+            STORE_RECORDS: (state, newRecords) => {
+                const { records } = state;
+
+                newRecords.forEach(storeRecord(records));
+            },
+
+            STORE_PAGE: (state, records) => {
+                state.page = records.map(({ id }) => id);
+            },
+
+            STORE_META: (state, meta) => {
+                state.lastMeta = meta;
+            },
+
+            STORE_ERROR: (state, error) => {
+                state.error = error;
+            },
+
+            STORE_RELATED: (state, { relatedIds, params, resetRelated = true }) => {
+                const { related } = state;
+                const relationshipIndex = getRelationshipIndex(params);
+                const existingRecord = related.find(matches(relationshipIndex));
+                if (existingRecord) {
+                    if (resetRelated) {
+                        existingRecord.relatedIds = relatedIds;
+                    } else {
+                        const ids = new Set([...existingRecord.relatedIds, ...relatedIds])
+                        existingRecord.relatedIds = [...ids];
+                    }
+                } else {
+                    related.push(Object.assign({ relatedIds }, relationshipIndex));
+                }
+            },
+
+            STORE_FILTERED: (state, { matchedIds, params }) => {
+                const { filtered } = state;
+
+                const existingRecord = filtered.find(matches(params));
+                if (existingRecord) {
+                    existingRecord.matchedIds = matchedIds;
+                } else {
+                    filtered.push(Object.assign({ matchedIds }, params));
+                }
+            },
+
+            STORE_LAST_CREATED: (state, record) => {
+                state.lastCreated = record;
+            },
+
+            REMOVE_RECORD: (state, record) => {
+                state.records = state.records.filter(r => r.id !== record.id);
+            },
+
+            SET_LINKS: (state, links) => {
+                state.links = links || {};
+            },
+
+            RESET_STATE: state => {
+                Object.assign(state, initialState());
+            },
+        },
+
+        actions: {
+            loadAll({ commit, dispatch }, { options } = {}) {
+                commit('SET_STATUS', STATUS_LOADING);
+                return client
+                    .all({ options })
+                    .then(result => {
+                        commit('SET_STATUS', STATUS_SUCCESS);
+                        commit('REPLACE_ALL_RECORDS', result.data);
+                        commit('STORE_META', result.meta);
+                        storeIncluded({ commit, dispatch }, result);
+                    })
+                    .catch(handleError(commit));
+            },
+
+            loadById({ commit, dispatch }, { id, options }) {
+                commit('SET_STATUS', STATUS_LOADING);
+                return client
+                    .find({ id, options })
+                    .then(results => {
+                        commit('SET_STATUS', STATUS_SUCCESS);
+                        commit('STORE_RECORD', results.data);
+                        commit('STORE_META', results.meta);
+                        storeIncluded({ commit, dispatch }, results);
+                    })
+                    .catch(handleError(commit));
+            },
+
+            loadWhere({ commit, dispatch }, params) {
+                const { filter, options } = params;
+                commit('SET_STATUS', STATUS_LOADING);
+                return client
+                    .where({ filter, options })
+                    .then(results => {
+                        commit('SET_STATUS', STATUS_SUCCESS);
+                        const matches = results.data;
+                        const matchedIds = matches.map(record => record.id);
+                        commit('STORE_RECORDS', matches);
+                        commit('STORE_FILTERED', { params, matchedIds });
+                        commit('STORE_META', results.meta);
+                        storeIncluded({ commit, dispatch }, results);
+                    })
+                    .catch(handleError(commit));
+            },
+
+            loadPage({ commit, dispatch }, { options }) {
+                commit('SET_STATUS', STATUS_LOADING);
+                return client
+                    .all({ options })
+                    .then(response => {
+                        commit('SET_STATUS', STATUS_SUCCESS);
+                        commit('STORE_RECORDS', response.data);
+                        commit('STORE_PAGE', response.data);
+                        commit('STORE_META', response.meta);
+                        commit('SET_LINKS', response.links);
+                        storeIncluded({ commit, dispatch }, response);
+                    })
+                    .catch(handleError(commit));
+            },
+
+            loadNextPage({ commit, state, dispatch }) {
+                const options = {
+                    url: state.links.next,
+                };
+                return client.all({ options }).then(response => {
+                    commit('STORE_RECORDS', response.data);
+                    commit('STORE_PAGE', response.data);
+                    commit('SET_LINKS', response.links);
+                    commit('STORE_META', response.meta);
+                    storeIncluded({ commit, dispatch }, response);
+                });
+            },
+
+            loadPreviousPage({ commit, state, dispatch }) {
+                const options = {
+                    url: state.links.prev,
+                };
+                return client.all({ options }).then(response => {
+                    commit('STORE_RECORDS', response.data);
+                    commit('STORE_PAGE', response.data);
+                    commit('SET_LINKS', response.links);
+                    commit('STORE_META', response.meta);
+                    storeIncluded({ commit, dispatch }, response);
+                });
+            },
+
+            loadRelated({ commit, dispatch }, params) {
+                const { parent, relationship = resourceName, options, resetRelated = true } = params;
+                commit('SET_STATUS', STATUS_LOADING);
+                const paramsToStore = {
+                    ...params,
+                    relationship,
+                };
+                return client
+                    .related({ parent, relationship, options })
+                    .then(results => {
+                        commit('SET_STATUS', STATUS_SUCCESS);
+                        const { id, type } = parent;
+                        if (Array.isArray(results.data)) {
+                            const relatedRecords = results.data;
+                            const relatedIds = relatedRecords.map(record => record.id);
+                            commit('STORE_RECORDS', relatedRecords);
+                            commit('STORE_RELATED', { params: paramsToStore, relatedIds, resetRelated });
+                        } else {
+                            const record = results.data;
+                            const relatedIds = record.id;
+                            commit('STORE_RECORDS', [record]);
+                            commit('STORE_RELATED', { params: paramsToStore, relatedIds });
+                        }
+                        commit('STORE_META', results.meta);
+                        storeIncluded({ commit, dispatch }, results);
+                    })
+                    .catch(handleError(commit));
+            },
+
+            create({ commit }, recordData) {
+                return client.create(recordData).then(result => {
+                    commit('STORE_RECORD', result.data);
+                    commit('STORE_LAST_CREATED', result.data);
+                });
+            },
+
+            update({ commit, dispatch, getters }, record) {
+                return client.update(record).then(() => {
+                    const oldRecord = getters.byId({ id: record.id });
+
+                    // remove old relationships first
+                    if (oldRecord && oldRecord.relationships) {
+                        for (const entry of Object.entries(oldRecord.relationships)) {
+                            const [relationship, entity] = entry;
+                            const type = getRelationshipType(entity);
+                            const paramsToStore = {
+                                relationship,
+                                parent: getResourceIdentifier(oldRecord),
+                            };
+
+                            // we cannot update the related resource without a type
+                            // this could possibly be very bad as we cannot remove existing
+                            // relationships
+                            if (type === null || type === undefined) {
+                                continue;
+                            }
+
+                            dispatch(
+                                `${type}/storeRelated`,
+                                {
+                                    params: paramsToStore,
+                                    relatedIds: null,
+                                },
+                                { root: true },
+                            );
+                        }
+                    }
+
+                    // save entity
+                    commit('STORE_RECORD', record);
+
+                    // set new relationships
+                    if (record.relationships) {
+                        for (const relationship of Object.keys(record.relationships)) {
+                            const relationshipObject = record.relationships[relationship];
+                            const { data } = relationshipObject;
+                            const isNonEmptyArray = Array.isArray(data) && Boolean(data.length);
+                            const isObject = Boolean(data && data.type && data.id);
+
+                            if (isNonEmptyArray || isObject) {
+                                const paramsToStore = {
+                                    parent: getResourceIdentifier(record),
+                                    relationship,
+                                };
+                                const type = getRelationshipType(relationshipObject);
+                                let relatedIds;
+                                if (Array.isArray(data)) {
+                                    relatedIds = data.map(record => record.id);
+                                } else {
+                                    relatedIds = data.id;
+                                }
+                                dispatch(
+                                    `${type}/storeRelated`,
+                                    {
+                                        params: paramsToStore,
+                                        relatedIds,
+                                    },
+                                    { root: true },
+                                );
+                            }
+                        }
+                    }
+                });
+            },
+
+            delete({ commit }, record) {
+                return client.delete(record).then(() => {
+                    commit('REMOVE_RECORD', record);
+                });
+            },
+
+            storeRecord({ commit }, record) {
+                commit('STORE_RECORD', record);
+            },
+
+            storeRelated({ commit }, { relatedIds, params }) {
+                commit('STORE_RELATED', {
+                    relatedIds,
+                    params,
+                });
+            },
+
+            removeRecord({ commit }, record) {
+                commit('REMOVE_RECORD', record);
+            },
+
+            resetState({ commit }) {
+                commit('RESET_STATE');
+            },
+
+            addRelated({ commit, getters }, params) {
+                const { parent, relationship = resourceName, data } = params;
+                const relatedItems = getters.related(params).map(o => o.id);
+                const difference = data.filter(x => !relatedItems.includes(x));
+                const records = difference.map(id => {
+                    return { type: relationship, id };
+                });
+                return client.createRelationships(parent, relationship, records);
+            },
+
+            setRelated({ commit, dispatch }, params) {
+                const { parent, relationship = resourceName, data } = params;
+                return client.updateRelationships(parent, relationship, data).then(response => {
+                    let relatedIds;
+                    if (Array.isArray(data)) {
+                        relatedIds = data.map(record => record.id);
+                    } else if (data === null) {
+                        relatedIds = null;
+                    } else {
+                        relatedIds = data.id;
+                    }
+                    commit('STORE_RELATED', {
+                        params: { parent, relationship },
+                        relatedIds,
+                    });
+                });
+            },
+
+            removeRelated({ commit, dispatch }, params) {
+                const { parent, relationship = resourceName, data } = params;
+                client.removeRelationships(parent, relationship, data);
+                let relatedIds;
+                if (Array.isArray(data)) {
+                    relatedIds = data.map(record => record.id);
+                } else {
+                    relatedIds = data.id;
+                }
+                commit('REMOVE_RELATED', {
+                    params: { parent, relationship },
+                    relatedIds,
+                });
+            },
+
+            removeAllRelated({ commit, dispatch }, params) {
+                const { parent, relationship = resourceName } = params;
+                client.updateRelationships(parent, relationship, []);
+                commit('REMOVE_RELATED', {
+                    params: { parent, relationship },
+                    relatedIds: [],
+                });
+            },
+        },
+
+        getters: {
+            isLoading: state => state.status === STATUS_LOADING,
+            isError: state => state.status === STATUS_ERROR,
+            error: state => state.error,
+            hasPrevious: state => !!state.links.prev,
+            hasNext: state => !!state.links.next,
+            all: state => state.records,
+            lastCreated: state => state.lastCreated,
+            byId: state => ({ id }) => state.records.find(r => r.id == id),
+            lastMeta: state => state.lastMeta,
+            page: state =>
+                state.page.map(id => state.records.find(record => record.id === id)),
+            where: state => params => {
+                const entry = state.filtered.find(matches(params));
+
+                if (!entry) {
+                    return [];
+                }
+
+                const ids = entry.matchedIds;
+                return ids.map(id => state.records.find(record => record.id === id));
+            },
+            related: state => params => {
+                const relationshipIndex = getRelationshipIndex(params);
+                const related = state.related.find(matches(relationshipIndex));
+
+                if (!related) {
+                    return null;
+                } else if (Array.isArray(related.relatedIds)) {
+                    const ids = related.relatedIds;
+                    return ids
+                        .map(id => state.records.find(record => record.id === id))
+                        .filter(record => record !== undefined);
+                } else {
+                    const id = related.relatedIds;
+                    return state.records.find(record => id === record.id);
+                }
+            },
+        },
+    };
+};
+
+const mapResourceModules = ({ names, httpClient }) =>
+    names.reduce(
+        (acc, name) =>
+        Object.assign({ [name]: resourceModule({ name, httpClient }) }, acc),
+        {},
+    );
+
+export { resourceModule, mapResourceModules };
diff --git a/resources/assets/stylesheets/jquery-ui.structure.css b/resources/assets/stylesheets/jquery-ui.structure.css
index 23f58da575378e950c6a0ed50c6df2a4b766e6c8..76ed1e72abcd681171143ec1a91f9e3466b69e4e 100644
--- a/resources/assets/stylesheets/jquery-ui.structure.css
+++ b/resources/assets/stylesheets/jquery-ui.structure.css
@@ -591,7 +591,8 @@ button.ui-button::-moz-focus-inner {
 .ui-dialog .ui-dialog-content {
 	position: relative;
 	border: 0;
-	padding: .5em 1em;
+	padding: .5em 1em 0;
+    margin-bottom: .5em;
 	background: none;
 	overflow: auto;
 }
diff --git a/resources/assets/stylesheets/scss/blubber.scss b/resources/assets/stylesheets/scss/blubber.scss
index 6e1499967cad67a53cdcff83c9222e62ca48a333..4e7bf34f880fc4c9d1621fd77e81028cafc0cf10 100644
--- a/resources/assets/stylesheets/scss/blubber.scss
+++ b/resources/assets/stylesheets/scss/blubber.scss
@@ -507,10 +507,13 @@ ol.tagcloud {
 }
 
 //Animationen des Widgets:
-.blubberthreadwidget-list-move, .blubberthreadwidget-list-enter-active, .blubberthreadwidget-list-leave-active {
+.blubberthreadwidget-list-move,
+.blubberthreadwidget-list-enter-active,
+.blubberthreadwidget-list-leave-active {
     transition: transform 0.5s;
 }
-.blubberthreadwidget-list-enter, .blubberthreadwidget-list-leave-to {
+.blubberthreadwidget-list-enter-from,
+.blubberthreadwidget-list-leave-to {
     transform: translateY(-70px);
 }
 
diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss
index d9280da526470b747d4829ef5f3183dbfd98d836..dd1d67cd1bdf7a844cf520418c81dd937fdf0ecc 100644
--- a/resources/assets/stylesheets/scss/forms.scss
+++ b/resources/assets/stylesheets/scss/forms.scss
@@ -647,11 +647,12 @@ form.inline {
             footer[data-dialog-button] {
                 background: var(--white);
                 border-top-color: var(--base-color-20);
-                bottom: -0.5em;
+                bottom: 0;
                 margin-top: auto;
                 padding: 0.5em 0;
                 position: sticky;
                 text-align: center;
+                z-index: 2;
             }
         }
     }
diff --git a/resources/vue/avatar-app.js b/resources/vue/avatar-app.js
index 0092ebe5f83e08c19b8340689ce8d0f0ec3ae07e..63db01c5f991a84d1a654636053ad0baf91c398a 100644
--- a/resources/vue/avatar-app.js
+++ b/resources/vue/avatar-app.js
@@ -1,76 +1,45 @@
 import AvatarApp from './components/avatar/AvatarApp.vue';
 import AvatarModule from './store/avatar.module';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
 import axios from 'axios';
+import { h } from 'vue';
 
-const mountApp = async (STUDIP, createApp, element) => {
+const mountApp = async (STUDIP, createApp, store, element) => {
 
     let entry_id = null;
     let entry_type = null;
-    let avatar_url = null;
-    let elem;
+    let elem = document.getElementById(element.substring(1));
 
-    if ((elem = document.getElementById(element.substring(1))) !== undefined) {
-        if (elem.attributes !== undefined) {
-            if (elem.attributes['entry-type'] !== undefined) {
-                entry_type = elem.attributes['entry-type'].value;
-            }
-
-            if (elem.attributes['entry-id'] !== undefined) {
-                entry_id = elem.attributes['entry-id'].value;
-            }
-
-            if (elem.attributes['avatar-url'] !== undefined) {
-                avatar_url = elem.attributes['avatar-url'].value;
-            }
-        }
+    if (elem) {
+        entry_type = elem.attributes?.['entry-type']?.value ?? null;
+        entry_id = elem.attributes?.['entry-id']?.value ?? null;
     }
 
-    const getHttpClient = () =>
-    axios.create({
+    const httpClient = axios.create({
         baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
         headers: {
             'Content-Type': 'application/vnd.api+json',
         },
     });
-    const httpClient = getHttpClient();
 
-    const store = new Vuex.Store({
-        modules: {
-            'avatar-module': AvatarModule,
-            ...mapResourceModules({
-                names: [
-                    'avatar',
-                    'courses',
-                    'institutes',
-                    'stock-images',
-                    'studygroups',
-                    'users',
-                ],
-                httpClient,
-            }),
-        }
-    });
+    store.registerModule('avatar-module', AvatarModule);
 
     const context = {
         type: entry_type,
         id: entry_id
     }
-    store.dispatch('setUserId', STUDIP.USER_ID);
+    await store.dispatch('setUserId', STUDIP.USER_ID);
     await store.dispatch('users/loadById', { id: STUDIP.USER_ID });
-    store.dispatch('setHttpClient', httpClient);
-    store.dispatch('setContext', context);
-    const avatar = await store.dispatch('loadAvatar');
+    await store.dispatch('setHttpClient', httpClient);
+    await store.dispatch('setContext', context);
+    await store.dispatch('loadAvatar');
 
     const app =  createApp({
-        render: (h) => h(AvatarApp),
+        render: () => h(AvatarApp),
         store,
     });
-    app.$mount(element);
+    app.mount(element);
 
     return app;
 }
 
-export default mountApp;
\ No newline at end of file
+export default mountApp;
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index 567e9ff484a510d93a5cc9f323032f09f4e06a13..d9bb1b79aff209edb956dc1ef74239c5a9394b3e 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -1,36 +1,38 @@
+import { defineAsyncComponent } from 'vue';
+
 const BaseComponents = {
-    CaptchaInput: () => import('./components/form_inputs/CaptchaInput.vue'),
-    CalendarPermissionsTable: () => import("./components/form_inputs/CalendarPermissionsTable.vue"),
-    DateListInput: () => import('./components/form_inputs/DateListInput.vue'),
-    Datepicker: () => import('./components/Datepicker.vue'),
-    Datetimepicker: () => import('./components/Datetimepicker.vue'),
-    DayOfWeekSelect: () => import('./components/form_inputs/DayOfWeekSelect.vue'),
-    EditableList: () => import("./components/EditableList.vue"),
-    FileUpload: () => import('./components/form_inputs/FileUpload.vue'),
-    I18nTextarea: () => import("./components/I18nTextarea.vue"),
-    Multiselect: () => import('./components/Multiselect.vue'),
-    MyCoursesColouredTable: () => import('./components/form_inputs/MyCoursesColouredTable.vue'),
-    Quicksearch: () => import('./components/Quicksearch.vue'),
-    QuicksearchListInput: () => import('./components/form_inputs/QuicksearchListInput.vue'),
-    RangeInput: () => import('./components/RangeInput.vue'),
-    RepetitionInput: () => import("./components/form_inputs/RepetitionInput.vue"),
-    SerialTextMarkers: () => import('./components/form_inputs/SerialTextMarkers.vue'),
-    SidebarWidget: () => import('./components/SidebarWidget.vue'),
-    StudipActionMenu: () => import('./components/StudipActionMenu.vue'),
-    StudipAssetImg: () => import('./components/StudipAssetImg.vue'),
-    StudipDateTime: () => import('./components/StudipDateTime.vue'),
-    StudipDialog: () => import('./components/StudipDialog.vue'),
-    StudipFileSize: () => import('./components/StudipFileSize.vue'),
-    StudipFolderSize: () => import('./components/StudipFolderSize.vue'),
-    StudipIcon: () => import('./components/StudipIcon.vue'),
-    StudipMessageBox: () => import('./components/StudipMessageBox.vue'),
-    StudipMultiPersonSearch: () => import('./components/StudipMultiPersonSearch.vue'),
-    StudipProxiedCheckbox: () => import('./components/StudipProxiedCheckbox.vue'),
-    StudipProxyCheckbox: () => import('./components/StudipProxyCheckbox.vue'),
-    StudipSelect: () => import('./components/StudipSelect.vue'),
-    StudipTooltipIcon: () => import('./components/StudipTooltipIcon.vue'),
-    StudipWysiwyg: () => import("./components/StudipWysiwyg.vue"),
-    UserFilterInput: () => import('./components/form_inputs/UserFilterInput.vue')
+    CaptchaInput: defineAsyncComponent(() => import('./components/form_inputs/CaptchaInput.vue')),
+    CalendarPermissionsTable: defineAsyncComponent(() => import('./components/form_inputs/CalendarPermissionsTable.vue')),
+    DateListInput: defineAsyncComponent(() => import('./components/form_inputs/DateListInput.vue')),
+    Datepicker: defineAsyncComponent(() => import('./components/Datepicker.vue')),
+    Datetimepicker: defineAsyncComponent(() => import('./components/Datetimepicker.vue')),
+    DayOfWeekSelect: defineAsyncComponent(() => import('./components/form_inputs/DayOfWeekSelect.vue')),
+    EditableList: defineAsyncComponent(() => import('./components/EditableList.vue')),
+    FileUpload: defineAsyncComponent(() => import('./components/form_inputs/FileUpload.vue')),
+    I18nTextarea: defineAsyncComponent(() => import("./components/I18nTextarea.vue")),
+    Multiselect: defineAsyncComponent(() => import('./components/Multiselect.vue')),
+    MyCoursesColouredTable: defineAsyncComponent(() => import('./components/form_inputs/MyCoursesColouredTable.vue')),
+    Quicksearch: defineAsyncComponent(() => import('./components/Quicksearch.vue')),
+    QuicksearchListInput: defineAsyncComponent(() => import('./components/form_inputs/QuicksearchListInput.vue')),
+    RangeInput: defineAsyncComponent(() => import('./components/RangeInput.vue')),
+    RepetitionInput: defineAsyncComponent(() => import("./components/form_inputs/RepetitionInput.vue")),
+    SerialTextMarkers: defineAsyncComponent(() => import('./components/form_inputs/SerialTextMarkers.vue')),
+    SidebarWidget: defineAsyncComponent(() => import('./components/SidebarWidget.vue')),
+    StudipActionMenu: defineAsyncComponent(() => import('./components/StudipActionMenu.vue')),
+    StudipAssetImg: defineAsyncComponent(() => import('./components/StudipAssetImg.vue')),
+    StudipDateTime: defineAsyncComponent(() => import('./components/StudipDateTime.vue')),
+    StudipDialog: defineAsyncComponent(() => import('./components/StudipDialog.vue')),
+    StudipFileSize: defineAsyncComponent(() => import('./components/StudipFileSize.vue')),
+    StudipFolderSize: defineAsyncComponent(() => import('./components/StudipFolderSize.vue')),
+    StudipIcon: defineAsyncComponent(() => import('./components/StudipIcon.vue')),
+    StudipMessageBox: defineAsyncComponent(() => import('./components/StudipMessageBox.vue')),
+    StudipMultiPersonSearch: defineAsyncComponent(() => import('./components/StudipMultiPersonSearch.vue')),
+    StudipProxiedCheckbox: defineAsyncComponent(() => import('./components/StudipProxiedCheckbox.vue')),
+    StudipProxyCheckbox: defineAsyncComponent(() => import('./components/StudipProxyCheckbox.vue')),
+    StudipSelect: defineAsyncComponent(() => import('./components/StudipSelect.vue')),
+    StudipTooltipIcon: defineAsyncComponent(() => import('./components/StudipTooltipIcon.vue')),
+    StudipWysiwyg: defineAsyncComponent(() => import('./components/StudipWysiwyg.vue')),
+    UserFilterInput: defineAsyncComponent(() => import('./components/form_inputs/UserFilterInput.vue')),
 };
 
 export default BaseComponents;
diff --git a/resources/vue/components/ActiveFilter.vue b/resources/vue/components/ActiveFilter.vue
index 5394bce50a06fc5a9b7a78a99469eca85b88d4e3..050afffdddc63117bd2cfec433d68a8e47bb174d 100644
--- a/resources/vue/components/ActiveFilter.vue
+++ b/resources/vue/components/ActiveFilter.vue
@@ -4,7 +4,7 @@
         <button
             @click="onRemoveActiveFilter"
             type="button"
-            :title="$gettextInterpolate($gettext('Filter \'%{name}\' entfernen'), { name }, true)"
+            :title="$gettext('Filter \'%{name}\' entfernen', { name }, true)"
         >
             <StudipIcon class="text-bottom" shape="decline" role="presentation" alt="" />
         </button>
@@ -12,8 +12,8 @@
 </template>
 
 <script>
-import StudipIcon from './StudipIcon.vue';
 export default {
+    emits: ['remove'],
     props: {
         name: {
             type: String,
diff --git a/resources/vue/components/AdminCourses.vue b/resources/vue/components/AdminCourses.vue
index c0e116051e0762988dd785670602c3fd88cc0e85..386d81d9ab5eed64e6322d322807e318c7326334 100644
--- a/resources/vue/components/AdminCourses.vue
+++ b/resources/vue/components/AdminCourses.vue
@@ -34,7 +34,7 @@
                     <th v-for="activeField in sortedActivatedFields" :key="`field-${activeField}`" :class="sort.by === activeField ? 'sort' + sort.direction.toLowerCase() : ''">
                         <a href="#"
                            @click.prevent="changeSort(activeField)"
-                           :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}, true) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}, true) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}, true))"
+                           :title="sort.by === activeField && sort.direction === 'ASC' ? $gettext('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}, true) : (sort.by === activeField && sort.direction === 'DESC' ? $gettext('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}, true) : $gettext('Sortieren nach %{ field }', { field: fields[activeField]}, true))"
                            v-if="!unsortableFields.includes(activeField)"
                         >
                             {{ fields[activeField] }}
@@ -72,8 +72,8 @@
                            @click.prevent="toggleOpenChildren(course.id)"
                            href="">
                             <studip-icon :shape="open_children.indexOf(course.id) === -1 ? 'add' : 'remove'" class="text-bottom"></studip-icon>
-                            {{ $gettextInterpolate(
-                                $gettext('%{ n } Unterveranstaltungen'),
+                            {{ $gettext(
+                                '%{ n } Unterveranstaltungen',
                                 { n: getChildren(course).length }
                             ) }}
                         </a>
@@ -89,8 +89,8 @@
                 <tr v-if="coursesCount > 0 && sortedCourses.length === 0">
                     <td :colspan="colspan">
                         {{
-                            $gettextInterpolate(
-                                $gettext(`%{ n } Veranstaltungen entsprechen Ihrem Filter. Schränken Sie nach Möglichkeit die Filter weiter ein.`),
+                            $gettext(
+                                '%{ n } Veranstaltungen entsprechen Ihrem Filter. Schränken Sie nach Möglichkeit die Filter weiter ein.',
                                 { n: coursesCount }
                             )
                         }}
@@ -161,7 +161,7 @@ export default {
 
         this.contentWidth = Math.max(...iconCounts) * 26;
     },
-    destroyed() {
+    unmounted() {
         this.globalOff('AdminCourses/changeActionArea', this.changeActionArea.bind(this));
         this.globalOff('AdminCourses/changeFilter', this.changeFilter.bind(this));
         this.globalOff('AdminCourses/loadCourse', this.loadCourse.bind(this));
diff --git a/resources/vue/components/CacheAdministration.vue b/resources/vue/components/CacheAdministration.vue
index c3220753bb8b583810cb61ee2d0dc67a34b31bc2..9b15719adef400ae595988c6137c8b32654a057a 100644
--- a/resources/vue/components/CacheAdministration.vue
+++ b/resources/vue/components/CacheAdministration.vue
@@ -5,10 +5,10 @@
         </StudipMessageBox>
         <fieldset>
             <legend>
-                <translate>Cachetyp</translate>
+                {{ $gettext('Cachetyp') }}
             </legend>
             <label>
-                <translate>Cachetyp auswählen</translate>
+                {{ $gettext('Cachetyp auswählen') }}
 
                 <select name="cachetype" v-model="selectedCacheType" @change="getCacheConfig">
                     <option v-for="(type) in cacheTypes" :key="type.cache_id" :value="type.class_name">
@@ -24,12 +24,12 @@
                 <component :is="configComponent" v-bind="configProps" ref="cacheConfig" @is-valid="setValid"></component>
             </template>
             <template v-else>
-                <translate>Für diesen Cachetyp ist keine Konfiguration erforderlich.</translate>
+                {{ $gettext('Für diesen Cachetyp ist keine Konfiguration erforderlich.') }}
             </template>
         </fieldset>
         <footer data-dialog-button>
             <button class="button accept" @click.prevent="validateConfig" :disabled="!isValid">
-                <translate>Speichern</translate>
+                {{ $gettext('Speichern') }}
             </button>
         </footer>
     </form>
diff --git a/resources/vue/components/ConsultationCreator.vue b/resources/vue/components/ConsultationCreator.vue
index 171b70d5f979f682b5b03493ea584ea2d3fd1307..aa5621cf9afd0505fbab4146e9baa4a882ab3dba 100644
--- a/resources/vue/components/ConsultationCreator.vue
+++ b/resources/vue/components/ConsultationCreator.vue
@@ -41,7 +41,7 @@
                 <span class="required">{{ $gettext('Am Wochentag') }}</span>
 
                 <select required name="day-of-week" @change="evt => dayOfWeek = parseInt(evt.target.value, 10)">
-                    <option v-for="dow in daysOfTheWeek" :value="dow.key" :key="dow.key" :selected="dayOfWeek === dow.key">
+                    <option v-for="dow in daysOfTheWeek" :value="dow.key" :key="dow.key" :selected="dayOfWeek === dow.key ? true : null">
                         {{ dow.label }}
                     </option>
                 </select>
@@ -267,8 +267,8 @@
 
             <label>
                 <input type="checkbox" v-model="confirmed">
-                {{ $gettextInterpolate(
-                    $gettext('Ja, ich möchte wirklich %{ n } Termine erstellen.'),
+                {{ $gettext(
+                    'Ja, ich möchte wirklich %{ n } Termine erstellen.',
                     { n: slotCount }
                 ) }}
             </label>
@@ -352,7 +352,7 @@ export default {
             slotCount: null,
             startDate: moment().add(1, 'weeks').toDate(),
             startTime: '08:00',
-        }
+        };
     },
     computed: {
         csrf() {
diff --git a/resources/vue/components/ContentBar.vue b/resources/vue/components/ContentBar.vue
index 539c5825f8f8af9eaddcbd75d04b48e3ce1aacb8..f12879f092b6338b1ff055700473eb0bf857bb98 100644
--- a/resources/vue/components/ContentBar.vue
+++ b/resources/vue/components/ContentBar.vue
@@ -126,7 +126,7 @@ export default defineComponent({
             }
         });
     },
-    beforeDestroy() {
+    beforeUnmount() {
         if (this.isContentBar) {
             window.STUDIP.eventBus.emit('courseware-contentbar-before-destroy', this);
         }
diff --git a/resources/vue/components/ContentBarTableOfContents.vue b/resources/vue/components/ContentBarTableOfContents.vue
index 525c4d69872b298a5791dc3f3d3980efd7da75c9..dea32549ef05416cf3627e64989a14c8df7d2cb4 100644
--- a/resources/vue/components/ContentBarTableOfContents.vue
+++ b/resources/vue/components/ContentBarTableOfContents.vue
@@ -8,7 +8,7 @@
             <article v-if="tocOpen" id="toc">
                 <header id="toc_header">
                     <h1 id="toc_h1">
-                        {{ $gettextInterpolate('Inhalt (%{count} Elemente)', { count: tocItemsCount }) }}
+                        {{ $gettext('Inhalt (%{count} Elemente)', {count: tocItemsCount.toString()}) }}
                     </h1>
                     <button class="toc-hide-button"
                             :title="$gettext('Inhaltsverzeichnis schließen')"
diff --git a/resources/vue/components/ContentModules.vue b/resources/vue/components/ContentModules.vue
index e6164900787e0032362be1665e393a343989b9f0..d2223a2c84fb2ce0d39810ccc8cc25d26edc9725 100644
--- a/resources/vue/components/ContentModules.vue
+++ b/resources/vue/components/ContentModules.vue
@@ -39,7 +39,9 @@
                    :range-type="rangeType"
         ></component>
 
-        <MountingPortal mount-to="#tool-view-switch .sidebar-widget-content .widget-list" name="sidebar-switch">
+        <Teleport to="#tool-view-switch .sidebar-widget-content .widget-list"
+                  name="sidebar-switch"
+        >
             <ul class="widget-list widget-links sidebar-views">
                 <li :class="{ active: view === 'tiles' }">
                     <a href="#" @click.prevent="changeView('tiles')">
@@ -52,9 +54,11 @@
                     </a>
                 </li>
             </ul>
-        </MountingPortal>
+        </Teleport>
 
-        <MountingPortal mount-to="#tool-filter-category .sidebar-widget-content .widget-list" name="sidebar-filter">
+        <Teleport to="#tool-filter-category .sidebar-widget-content .widget-list"
+                  name="sidebar-filter"
+        >
             <ul class="widget-list widget-options">
                 <li>
                     <a class="options-radio"
@@ -79,7 +83,7 @@
                     </a>
                 </li>
             </ul>
-        </MountingPortal>
+        </Teleport>
     </form>
 </template>
 <script>
@@ -114,6 +118,10 @@ export default {
             'setFilterCategory',
         ]),
     },
+    beforeMount() {
+        document.querySelector('#tool-view-switch .sidebar-widget-content .widget-list').innerHTML = '';
+        document.querySelector('#tool-filter-category .sidebar-widget-content .widget-list').innerHTML = '';
+    }
 };
 </script>
 <style lang="scss">
diff --git a/resources/vue/components/ContentModulesControl.vue b/resources/vue/components/ContentModulesControl.vue
index 34f4125624c811ee1657b7587cae4a2eeaa3fc33..e41f200eb975d289636982e0406c568cbbbc3d46 100644
--- a/resources/vue/components/ContentModulesControl.vue
+++ b/resources/vue/components/ContentModulesControl.vue
@@ -15,7 +15,7 @@
                @click.prevent="toggleModuleVisibility(module)">
                 <studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'"
                              class="text-bottom"
-                             :title="$gettextInterpolate($gettext('Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname}, true)"></studip-icon>
+                             :title="$gettext('Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten', { name: module.displayname}, true)"></studip-icon>
             </a>
         </div>
     </div>
diff --git a/resources/vue/components/ContentModulesEditTiles.vue b/resources/vue/components/ContentModulesEditTiles.vue
index c650f9eb633f3fffdd7dadef5a7b5840c9c8d05a..badfbc88f1bff8f337342a145e697c8f31233e2c 100644
--- a/resources/vue/components/ContentModulesEditTiles.vue
+++ b/resources/vue/components/ContentModulesEditTiles.vue
@@ -1,28 +1,31 @@
 <template>
     <div class="content-modules-wrapper">
-        <draggable v-model="sortedModules" handle=".dragarea">
-            <transition-group
-                name="admin_contentmodules"
-                class="admin_contentmodules studip-grid"
-                tag="div"
-                role="listbox"
-            >
+        <draggable v-model="activeModules"
+                   handle=".dragarea"
+                   :component-data="{
+                        name:'admin_contentmodules',
+                        type: 'transition-group',
+                        tag: 'div',
+                   }"
+                   item-key="id"
+                   class="admin_contentmodules studip-grid"
+                   role="listbox"
+        >
+            <template #item="{element}">
                 <div
-                    v-for="module in activeModules"
-                    :key="module.id"
                     role="option"
                     class="studip-grid-element"
-                    :class="getModuleCSSClasses(module, activated[module.id])"
+                    :class="getModuleCSSClasses(element, activated[element.id])"
                     v-cloak
                 >
                     <div>
-                        <a class="upper_part dragarea" :href="getDescriptionURL(module)" data-dialog>
+                        <a class="upper_part dragarea" :href="getDescriptionURL(element)" data-dialog>
                             <div>
-                                <img :src="module.icon" width="40" height="40" v-if="module.icon" />
+                                <img :src="element.icon" width="40" height="40" v-if="element.icon" />
                             </div>
                             <div>
-                                <h3>{{ module.displayname }}</h3>
-                                {{ module.summary }}
+                                <h3>{{ element.displayname }}</h3>
+                                {{ element.summary }}
                             </div>
                         </a>
                         <div class="down_part">
@@ -31,26 +34,24 @@
                                     class="dragarea"
                                     tabindex="0"
                                     :aria-label="
-                                        $gettextInterpolate(
-                                            $gettext(
-                                                'Sortierelement für Werkzeug %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'
-                                            ),
-                                            { module: module.displayname },
+                                        $gettext(
+                                            'Sortierelement für Werkzeug %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.',
+                                            { module: element.displayname },
                                             true
                                         )
                                     "
-                                    @keydown="keyboardHandler($event, module)"
+                                    @keydown="keyboardHandler($event, element)"
                                     v-if="filterCategory === null"
-                                    :ref="`draghandle-${module.id}`"
+                                    :ref="`draghandle-${element.id}`"
                                 >
                                     <span class="drag-handle"></span>
                                 </a>
-                                <label v-if="!module.mandatory">
+                                <label v-if="!element.mandatory">
                                     <input
                                         type="checkbox"
-                                        :checked="activated[module.id]"
-                                        @click="toggleModule(module)"
-                                        :ref="'checkbox_' + module.id"
+                                        :checked="activated[element.id]"
+                                        @click="toggleModule(element)"
+                                        :ref="'checkbox_' + element.id"
                                     />
                                     {{ $gettext('Werkzeug ist aktiv') }}
                                 </label>
@@ -61,38 +62,34 @@
                                     href="#"
                                     class="toggle_visibility"
                                     role="checkbox"
-                                    v-if="showVisibilityToggle(module)"
-                                    :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'"
-                                    @click.prevent="toggleModuleVisibility(module)"
+                                    v-if="showVisibilityToggle(element)"
+                                    :aria-checked="element.visibility !== 'tutor' ? 'true' : 'false'"
+                                    @click.prevent="toggleModuleVisibility(element)"
                                 >
                                     <studip-icon
                                         :shape="
-                                            module.visibility !== 'tutor'
+                                            element.visibility !== 'tutor'
                                                 ? 'visibility-visible'
                                                 : 'visibility-invisible'
                                         "
                                         class="text-bottom"
                                         :title="
-                                            $gettextInterpolate(
-                                                $gettext(
-                                                    'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'
-                                                ),
-                                                { name: module.displayname },
+                                            $gettext(
+                                                'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten',
+                                                { name: element.displayname },
                                                 true
                                             )
                                         "
                                     ></studip-icon>
                                 </a>
-                                <a :href="getRenameURL(module)" data-dialog="size=medium">
+                                <a :href="getRenameURL(element)" data-dialog="size=medium">
                                     <studip-icon
                                         shape="edit"
                                         class="text-bottom"
                                         :title="
-                                            $gettextInterpolate(
-                                                $gettext(
-                                                    'Umbenennen des Inhaltsmoduls %{ name }'
-                                                ),
-                                                { name: module.displayname },
+                                            $gettext(
+                                                'Umbenennen des Inhaltsmoduls %{ name }',
+                                                { name: element.displayname },
                                                 true
                                             )
                                         "
@@ -102,7 +99,7 @@
                         </div>
                     </div>
                 </div>
-            </transition-group>
+            </template>
         </draggable>
         <transition-group
             name="admin_contentmodules"
@@ -164,15 +161,16 @@ export default {
     },
     methods: {
         toggleModule(module) {
-            Vue.set(this.activated, module.id, !this.activated[module.id]);
+            this.activated[module.id] = !this.activated[module.id];
             this.toggleModuleActivation(module);
         },
     },
     watch: {
         modules: {
             immediate: true,
+            deep: true,
             handler(current) {
-                current.forEach((module) => Vue.set(this.activated, module.id, module.active));
+                current.forEach((module) => this.activated[module.id] = module.active);
             },
         },
     },
diff --git a/resources/vue/components/ContentmodulesEditTable.vue b/resources/vue/components/ContentmodulesEditTable.vue
index f8d563897941f615579208c4808609df7649075d..992de76bea428f3e20d01973ab56f554a6d8bfa7 100644
--- a/resources/vue/components/ContentmodulesEditTable.vue
+++ b/resources/vue/components/ContentmodulesEditTable.vue
@@ -14,83 +14,87 @@
                 <th class="actions">{{ $gettext('Aktionen') }}</th>
             </tr>
         </thead>
-        <draggable v-model="sortedModules" handle=".dragarea" tag="tbody">
-            <tr v-for="module in activeModules" :key="module.id" :class="getModuleCSSClasses(module)" v-cloak>
-                <td v-if="filterCategory === null">
-                    <a
-                        class="dragarea"
-                        tabindex="0"
-                        :aria-label="
-                            $gettextInterpolate(
+        <draggable v-model="activeModules"
+                   handle=".dragarea"
+                   tag="tbody"
+                   item-key="id"
+        >
+            <template #item="{element}">
+                <tr :class="getModuleCSSClasses(element)" v-cloak>
+                    <td v-if="filterCategory === null">
+                        <a
+                            class="dragarea"
+                            tabindex="0"
+                            :aria-label="
                                 $gettext(
-                                    'Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'
-                                ),
-                                { module: module.displayname },
-                                true
-                            )
-                        "
-                        @keydown="keyboardHandler($event, module)"
-                        v-if="module.active"
-                        :ref="`draghandle-${module.id}`"
-                    >
-                        <span class="drag-handle"></span>
-                    </a>
-                </td>
-                <td>
-                    <input
-                        type="checkbox"
-                        v-model="module.active"
-                        @click="toggleModuleActivation(module)"
-                        v-if="!module.mandatory"
-                        :ref="'checkbox_' + module.id"
-                    />
-                </td>
-                <td>
-                    <a
-                        class="upper_part"
-                        :class="{ dragrea: module.active }"
-                        :href="getDescriptionURL(module)"
-                        data-dialog
-                    >
-                        <img :src="module.icon" width="20" height="20" v-if="module.icon" class="text-bottom" />
-                        {{ module.displayname }}
-                    </a>
-                </td>
-                <td class="actions">
-                    <a
-                        href="#"
-                        v-if="showVisibilityToggle(module)"
-                        role="checkbox"
-                        :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'"
-                        @click.prevent="toggleModuleVisibility(module)"
-                    >
-                        <studip-icon
-                            :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'"
-                            class="text-bottom"
-                            :title="
-                                $gettextInterpolate(
-                                    $gettext(
-                                        'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'
-                                    ),
-                                    { name: module.displayname },
+                                    'Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.',
+                                    { module: element.displayname },
                                     true
                                 )
                             "
-                        ></studip-icon>
-                    </a>
-                    <a :href="getRenameURL(module)" data-dialog="size=auto" v-if="module.active">
-                        <studip-icon
-                            shape="edit"
-                            class="text-bottom"
-                            :title="
-                                $gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), {
-                                    name: module.displayname,
-                                }, true)
-                            "
-                        ></studip-icon>
-                    </a>
-                </td>
-            </tr>
+                            @keydown="keyboardHandler($event, element)"
+                            v-if="element.active"
+                            :ref="`draghandle-${element.id}`"
+                        >
+                            <span class="drag-handle"></span>
+                        </a>
+                    </td>
+                    <td>
+                        <input
+                            type="checkbox"
+                            v-model="element.active"
+                            @click="toggleModuleActivation(element)"
+                            v-if="!element.mandatory"
+                            :ref="'checkbox_' + element.id"
+                        />
+                    </td>
+                    <td>
+                        <a
+                            class="upper_part"
+                            :class="{ dragrea: element.active }"
+                            :href="getDescriptionURL(element)"
+                            data-dialog
+                        >
+                            <img :src="element.icon" width="20" height="20" v-if="element.icon" class="text-bottom" />
+                            {{ element.displayname }}
+                        </a>
+                    </td>
+                    <td class="actions">
+                        <a
+                            href="#"
+                            v-if="showVisibilityToggle(element)"
+                            role="checkbox"
+                            :aria-checked="element.visibility !== 'tutor' ? 'true' : 'false'"
+                            @click.prevent="toggleModuleVisibility(element)"
+                        >
+                            <studip-icon
+                                :shape="element.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'"
+                                class="text-bottom"
+                                :title="
+                                    $gettext(
+                                        'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten',
+                                        { name: element.displayname },
+                                        true
+                                    )
+                                "
+                            ></studip-icon>
+                        </a>
+                        <a :href="getRenameURL(element)" data-dialog="size=auto" v-if="element.active">
+                            <studip-icon
+                                shape="edit"
+                                class="text-bottom"
+                                :title="
+                                    $gettext(
+                                        'Umbenennen des Inhaltsmoduls %{ name }',
+                                        { name: element.displayname },
+                                        true
+                                    )
+                                "
+                            ></studip-icon>
+                        </a>
+                    </td>
+                </tr>
+            </template>
         </draggable>
         <tbody>
             <tr v-for="module in inactiveModules" :key="module.id" :class="getModuleCSSClasses(module)" v-cloak>
diff --git a/resources/vue/components/Datepicker.vue b/resources/vue/components/Datepicker.vue
index c1886a2997765228c1fabfe914985331810b7d88..eaedc83feffae9fae3c10469819c350c46162ba4 100644
--- a/resources/vue/components/Datepicker.vue
+++ b/resources/vue/components/Datepicker.vue
@@ -5,7 +5,6 @@
                ref="visibleInput"
                class="visible_input"
                v-bind="$attrs"
-               v-on="$listeners"
                :placeholder="placeholder">
     </span>
 </template>
@@ -16,12 +15,13 @@ import RestrictedDatesHelper from '../../assets/javascripts/lib/RestrictedDatesH
 export default {
     name: 'Datepicker',
     inheritAttrs: false,
+    emits: ['update:modelValue'],
     props: {
+        modelValue: [Date, String, Number],
         name: {
             type: String,
             required: false
         },
-        value: [Date, String, Number],
         mindate: [Date, Number, String],
         maxdate: [Date, Number, String],
         placeholder: String,
@@ -69,14 +69,14 @@ export default {
         },
         returnValue() {
             if (this.returnAs === 'unix') {
-                return this.convertInputToUnixTimestamp(this.value);
+                return this.convertInputToUnixTimestamp(this.modelValue);
             }
 
             if (this.returnAs === 'iso') {
-                return this.convertInputToNativeDate(this.value).toISOString();
+                return this.convertInputToNativeDate(this.modelValue).toISOString();
             }
 
-            return this.convertInputToNativeDate(this.value).toLocaleDateString(String.locale);
+            return this.convertInputToNativeDate(this.modelValue).toLocaleDateString(String.locale);
         }
     },
     methods: {
@@ -104,11 +104,11 @@ export default {
         },
         setUnixTimestamp () {
             let date = this.input.datepicker('getDate');
-            this.$emit('input', this.emitDate ? date : Math.floor(date.getTime() / 1000));
+            this.$emit('update:modelValue', this.emitDate ? date : Math.floor(date.getTime() / 1000));
         }
     },
     mounted () {
-        let value = this.convertInputToUnixTimestamp(this.value);
+        let value = this.convertInputToUnixTimestamp(this.modelValue);
 
         if (Number.isInteger(value)) {
             let date = new Date(value * 1000);
diff --git a/resources/vue/components/Datetimepicker.vue b/resources/vue/components/Datetimepicker.vue
index 6b12ad78abc2896cf607f85601f1033058ddc4bd..204aacc62b10080fae1a6d567f24c4f2ecb7aa3f 100644
--- a/resources/vue/components/Datetimepicker.vue
+++ b/resources/vue/components/Datetimepicker.vue
@@ -1,25 +1,25 @@
 <template>
     <span>
-        <input type="hidden" :name="name" :value="value">
-        <input type="text"
+        <input type="hidden" :name="name" :value="modelValue">
+        <input v-bind="$attrs"
+               type="text"
                ref="visibleInput"
                class="visible_input"
-               @change="setUnixTimestamp"
-               v-bind="$attrs"
-               v-on="$listeners">
+               @change="setUnixTimestamp">
     </span>
 </template>
 
 <script>
 export default {
     name: 'datetimepicker',
+    emits: ['update:modelValue'],
     inheritAttrs: false,
     props: {
         name: {
             type: String,
             required: false
         },
-        value: {
+        modelValue: {
             required: false
         },
         mindate: {
@@ -35,14 +35,14 @@ export default {
             let date = formatted_date.match(/(\d+)/g);
             if (date) {
                 date = new Date(`${date[2]}-${date[1]}-${date[0]} ${date[3]}:${date[4]}`);
-                this.$emit('input', Math.floor(date / 1000));
+                this.$emit('update:modelValue', Math.floor(date / 1000));
             } else {
-                this.$emit('input', null);
+                this.$emit('update:modelValue', null);
             }
         }
     },
     mounted () {
-        let value = !isNaN(parseInt(this.value, 10)) ? parseInt(this.value, 10) : this.value;
+        let value = !isNaN(parseInt(this.modelValue, 10)) ? parseInt(this.modelValue, 10) : this.modelValue;
         if (Number.isInteger(value)) {
             let date = new Date(value * 1000);
             let formatted_date =
@@ -59,10 +59,9 @@ export default {
         } else {
             this.$refs.visibleInput.value = value;
         }
-        let v = this;
         let params = {
-            onSelect () {
-                v.setUnixTimestamp();
+            onSelect: () => {
+                this.setUnixTimestamp();
             }
         };
         if (this.mindate) {
@@ -74,10 +73,10 @@ export default {
         $(this.$refs.visibleInput).datetimepicker(params);
     },
     watch: {
-        mindat (new_data, old_data) {
+        mindat (new_data) {
             $(this.$refs.visibleInput).datetimepicker('option', 'minDate', new Date(new_data * 1000));
         },
-        maxdate (new_data, old_data) {
+        maxdate (new_data) {
             $(this.$refs.visibleInput).datetimepicker('option', 'maxDate', new Date(new_data * 1000));
         }
     }
diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/EditableList.vue
index e7aa83865be61ec82a43e8b34a48777da38e3f62..057ffe3b72995315ace8094761d0d9cf840e18ef 100644
--- a/resources/vue/components/EditableList.vue
+++ b/resources/vue/components/EditableList.vue
@@ -12,7 +12,7 @@
                     <studip-icon v-if="item.icon" :shape="item.icon" role="info" class="text-bottom" alt=""></studip-icon>
                     <input v-if="name" type="hidden" :name="name + '[]'" :value="item.value">
                     <span>{{item.name}}</span>
-                    <button v-if="item.deletable" @click.prevent="deleteItem(item)" :title="$gettextInterpolate($gettext('%{ name } löschen'), {name: item.name}, true)" class="undecorated">
+                    <button v-if="item.deletable" @click.prevent="deleteItem(item)" :title="$gettext('%{ name } löschen', {name: item.name}, true)" class="undecorated">
                         <studip-icon shape="trash" class="text-bottom"></studip-icon>
                     </button>
                 </li>
@@ -21,17 +21,17 @@
         </div>
 
         <label v-if="selectable">
-            <translate>Oder aus Liste auswählen:</translate>
+            {{ $gettext('Oder aus Liste auswählen:') }}
             <select @change="quickselect" @keydown="navigate_or_select">
-                <option value=""><translate>Direkt auswählen ...</translate></option>
+                <option value="">{{ $gettext('Direkt auswählen ...') }}</option>
                 <template v-for="(opt, idx) in selectable">
                     <optgroup v-if="opt.label && opt.options" :label="opt.label" :key="idx">
-                        <option v-for="(option, index) in opt.options" :disabled="isSelected(option.value)" :value="JSON.stringify({value: option.value, name: option.name})" :key="index">
+                        <option v-for="(option, index) in opt.options" :disabled="isSelected(option.value)" :value="JSON.stringify({value: option.value, name: option.name})" :key="`group-${index}`">
                             {{ option.name + (isSelected(option.value) ? ' ✓' : '') }}
                         </option>
                     </optgroup>
-                    <option v-else :disabled="isSelected(opt.value)" @click="quicksearch" :value="JSON.stringify({value: opt.value, name: opt.name})" :key="idx">
-                        {{ opt.name + (isSelected(option.value) ? ' ✓' : '') }}
+                    <option v-else :disabled="isSelected(opt.value)" @click="quicksearch" :value="JSON.stringify({value: opt.value, name: opt.name})" :key="`opt-${idx}`">
+                        {{ opt.name + (isSelected(opt.value) ? ' ✓' : '') }}
                     </option>
                 </template>
             </select>
@@ -43,6 +43,7 @@
 <script>
 export default {
     name: 'editable-list',
+    emits: ['input', 'items'],
     props: {
         name: {
             type: String,
diff --git a/resources/vue/components/FileCacheConfig.vue b/resources/vue/components/FileCacheConfig.vue
index 82569ba798653dee367bba853102a81f715b3332..3644c5c6949b78d361cc9e1b3cfcc75ff7c1b36a 100644
--- a/resources/vue/components/FileCacheConfig.vue
+++ b/resources/vue/components/FileCacheConfig.vue
@@ -1,7 +1,7 @@
 <template>
     <label class="col-4">
         <span class="required">
-            <translate>Dateipfad</translate>
+            {{ $gettexdt('Dateipfad') }}
         </span>
         <input required type="text" name="path" v-model="thePath">
     </label>
@@ -10,6 +10,7 @@
 <script>
 export default {
     name: 'FileCacheConfig',
+    emits: ['is-valid'],
     props: {
         path: {
             type: String,
diff --git a/resources/vue/components/FilesTable.vue b/resources/vue/components/FilesTable.vue
index 71db77ff8d2f22af5f4a6bf97bc530fffe10f4a6..e6480ef27a33fb860889278e22aa980adf47592e 100644
--- a/resources/vue/components/FilesTable.vue
+++ b/resources/vue/components/FilesTable.vue
@@ -46,7 +46,7 @@
                         data-sort="false"
                         :aria-label="$gettext('Ordner und Dateien auswählen')">
                         <studip-proxy-checkbox
-                            v-model="selectedIds"
+                            v-model:selected="selectedIds"
                             :total="allIds"
                             :title="$gettext('Alle Ordner und Dateien auswählen')"
                         ></studip-proxy-checkbox>
@@ -132,7 +132,7 @@
                     >
                         <a href="#"
                            @click.prevent
-                           :title="$gettextInterpolate($gettext('Nach %{ colName } sortieren'), {colName: name}, true)">
+                           :title="$gettext('Nach %{ colName } sortieren', {colName: name}, true)">
                             {{name}}
                         </a>
                     </th>
@@ -149,7 +149,7 @@
             <tbody v-else-if="displayedFolders.length + displayedFiles.length === 0">
                 <tr class="empty">
                     <td :colspan="numberOfColumns">
-                        <translate>Keine Ordner oder Dateien entsprechen Ihrem Filter.</translate>
+                        {{ $gettext('Keine Ordner oder Dateien entsprechen Ihrem Filter.') }}
                     </td>
                 </tr>
             </tbody>
@@ -162,22 +162,22 @@
                         <studip-proxied-checkbox
                             name="ids[]"
                             :value="folder.id"
-                            v-model="selectedIds"
+                            v-model:selected="selectedIds"
                             :aria-label="getAriaLabelForFolder(folder)"
                         ></studip-proxied-checkbox>
                     </td>
                     <td class="document-icon">
                         <a :href="folder.url"
                            :id="`folder-${folder.id}`"
-                           :title="$gettextInterpolate($gettext('Ordner %{foldername} öffnen'),
-                           { foldername: folder.name}, true)">
+                           :title="$gettext('Ordner %{foldername} öffnen', { foldername: folder.name}, true)"
+                        >
                             <studip-icon :shape="folder.icon" :size="26" class="text-bottom" alt=""></studip-icon>
                         </a>
                     </td>
                     <td :class="{'filter-match': valueMatchesFilter(folder.name)}">
                         <a :href="folder.url"
-                           :title="$gettextInterpolate($gettext('Ordner %{foldername} öffnen'),
-                           { foldername: folder.name}, true)">
+                           :title="$gettext('Ordner %{foldername} öffnen', { foldername: folder.name}, true)"
+                        >
                             <span v-html="highlightString(folder.name)"></span>
                         </a>
                     </td>
@@ -202,7 +202,7 @@
                             class="responsive-hidden"
                             v-html="folder.additionalColumns[index].html"
                             :key="index"></td>
-                        <td v-else class="responsive-hidden" :key="index"></td>
+                        <td v-else class="responsive-hidden" :key="`empty-${index}`"></td>
                     </template>
                     <td class="actions" v-html="folder.actions">
                     </td>
@@ -219,7 +219,7 @@
                         <studip-proxied-checkbox
                             name="ids[]"
                             :value="file.id"
-                            v-model="selectedIds"
+                            v-model:selected="selectedIds"
                             :aria-label="getAriaLabelForFile(file)"
                         ></studip-proxied-checkbox>
                     </td>
@@ -227,8 +227,8 @@
                         <a v-if="file.download_url"
                            :href="file.download_url"
                            target="_blank" rel="noopener noreferrer"
-                           :title="$gettextInterpolate($gettext('Datei %{filename} herunterladen'),
-                            { filename: file.name }, true)">
+                           :title="$gettext('Datei %{filename} herunterladen', { filename: file.name }, true)"
+                        >
                             <studip-icon :shape="file.icon" :size="24" class="text-bottom"></studip-icon>
                         </a>
                         <studip-icon v-else :shape="file.icon" :size="24"></studip-icon>
@@ -237,15 +237,15 @@
                            v-if="file.download_url && file.mime_type.indexOf('image/') === 0"
                            class="lightbox-image"
                            data-lightbox="gallery"
-                           :title="$gettextInterpolate($gettext('Datei %{filename} anzeigen'),
-                            { filename: file.name }, true)"></a>
+                           :title="$gettext('Datei %{filename} anzeigen', { filename: file.name }, true)"
+                        ></a>
                     </td>
                     <td :class="{'filter-match': valueMatchesFilter(file.name)}">
                         <a :href="file.details_url"
                            data-dialog
                            :id="`file-${file.id}`"
-                           :title="$gettextInterpolate($gettext('Details zur Datei %{filename} anzeigen'),
-                            { filename: file.name }, true)">
+                           :title="$gettext('Details zur Datei %{filename} anzeigen', { filename: file.name }, true)"
+                        >
                             <span v-html="highlightString(file.name)"></span>
                             <studip-icon v-if="file.isAccessible"
                                          shape="accessibility"
@@ -283,7 +283,7 @@
                             class="responsive-hidden"
                             v-html="file.additionalColumns[index].html"
                             :key="index"></td>
-                        <td v-else class="responsive-hidden" :key="index"></td>
+                        <td v-else class="responsive-hidden" :key="`empty-${index}`"></td>
                     </template>
                     <td class="actions" v-html="file.actions">
                     </td>
@@ -307,9 +307,9 @@
             </tfoot>
         </table>
 
-        <MountingPortal v-if="allow_filter" mount-to="#table-view-filter .sidebar-widget-content div" name="sidebar-content-toggle">
+        <Teleport v-if="allow_filter" to="#table-view-filter .sidebar-widget-content div" name="sidebar-content-toggle">
             <input :placeholder="$gettext('Name oder Autor/in')" type="search" v-model="filter" :disabled="!hasData" />
-        </MountingPortal>
+        </Teleport>
     </div>
 </template>
 <script>
@@ -459,15 +459,15 @@ export default {
             return highlighted;
         },
         getAriaLabelForFolder(folder) {
-            return this.$gettextInterpolate(
-                this.$gettext('Ordner %{name} auswählen'),
-                {name: folder.name}
+            return this.$gettext(
+                'Ordner %{name} auswählen',
+                { name: folder.name }
             );
         },
         getAriaLabelForFile(file) {
-            return this.$gettextInterpolate(
-                this.$gettext('Datei %{name} auswählen'),
-                {name: file.name}
+            return this.$gettext(
+                'Datei %{name} auswählen',
+                { name: file.name }
             );
         },
         getAriaSortString(column) {
@@ -479,10 +479,9 @@ export default {
             if (column !== this.sortedBy) {
                 return null;
             }
-            const template = this.sortDirection === 'asc'
-                ? this.$gettext('Es wird aufsteigend nach der Spalte %{ label } sortiert.')
-                : this.$gettext('Es wird absteigend nach der Spalte %{ label } sortiert.');
-            return this.$gettextInterpolate(template, { label });
+            return this.sortDirection === 'asc'
+                ? this.$gettext('Es wird aufsteigend nach der Spalte %{ label } sortiert.', { label })
+                : this.$gettext('Es wird absteigend nach der Spalte %{ label } sortiert.', { label });
         }
     },
     computed: {
@@ -535,23 +534,26 @@ export default {
         this.selectedIds = [];
     },
     watch: {
-        selectedIds (current) {
-            const activated = current.length > 0;
-            if (this.$refs.buttons) {
-                this.$nextTick(() => { // needs to be wrapped since we check the dom
-                    this.$refs.buttons.querySelectorAll('.multibuttons .button').forEach(element => {
-                        let condition = element.dataset.activatesCondition;
-                        if (!condition || !activated) {
-                            element.disabled = !activated;
-                        } else {
-                            condition = condition.replace(/:has\((.*?)\)/g, ' $1');
-                            condition = condition.replace(':checkbox', 'input[type="checkbox"]');
+        selectedIds: {
+            handler(current) {
+                const activated = current.length > 0;
+                if (this.$refs.buttons) {
+                    this.$nextTick(() => { // needs to be wrapped since we check the dom
+                        this.$refs.buttons.querySelectorAll('.multibuttons .button').forEach(element => {
+                            let condition = element.dataset.activatesCondition;
+                            if (!condition || !activated) {
+                                element.disabled = !activated;
+                            } else {
+                                condition = condition.replace(/:has\((.*?)\)/g, ' $1');
+                                condition = condition.replace(':checkbox', 'input[type="checkbox"]');
 
-                            element.disabled = this.$el.querySelector(condition) === null;
-                        }
+                                element.disabled = this.$el.querySelector(condition) === null;
+                            }
+                        });
                     });
-                });
-            }
+                }
+            },
+            deep: true,
         },
     }
 }
diff --git a/resources/vue/components/I18nTextarea.vue b/resources/vue/components/I18nTextarea.vue
index 5071d38fc57b3e6986d8d781c2fb658380fd62f4..c2c8c4518bc7f914ee9f2a9c71ecb6886fb32199 100644
--- a/resources/vue/components/I18nTextarea.vue
+++ b/resources/vue/components/I18nTextarea.vue
@@ -10,12 +10,10 @@
                    v-model="currentText"
                    :required="required && defaultLanguage === selectedLanguage.id"
                    v-bind="$attrs"
-                   v-on="$listeners"
                    v-if="type === 'text'">
             <textarea :name="nameOfInput(selectedLanguage.id)"
                       ref="inputfield"
                       v-bind="$attrs"
-                      v-on="$listeners"
                       v-model="currentText"
                       :required="required && defaultLanguage === selectedLanguage.id"
                       v-else-if="type === 'textarea'"></textarea>
@@ -23,7 +21,6 @@
                             ref="inputfield"
                             v-model="currentText"
                             v-bind="$attrs"
-                            v-on="$listeners"
                             :required="required && defaultLanguage === selectedLanguage.id"
                             v-else></studip-wysiwyg>
         </div>
@@ -49,21 +46,18 @@
                :name="name"
                v-model="currentText"
                v-bind="$attrs"
-               v-on="$listeners"
                :required="required"
                v-if="type === 'text'">
         <textarea :name="name"
                   ref="inputfield"
                   v-model="currentText"
                   v-bind="$attrs"
-                  v-on="$listeners"
                   :required="required"
                   v-else-if="type === 'textarea'"></textarea>
         <studip-wysiwyg :name="name"
                         ref="inputfield"
                         v-model="currentText"
                         v-bind="$attrs"
-                        v-on="$listeners"
                         :required="required"
                         v-else></studip-wysiwyg>
     </div>
@@ -76,6 +70,7 @@ export default {
     components: {
         StudipWysiwyg
     },
+    emits: ['allinputs', 'input', 'selectlanguage'],
     props: {
         name: {
             type: String,
diff --git a/resources/vue/components/MemcachedCacheConfig.vue b/resources/vue/components/MemcachedCacheConfig.vue
index ba06c7c0625633fc6f153bf304957238a703ecb1..9c3ac0c84821e8236bbf45139457c81346387aa6 100644
--- a/resources/vue/components/MemcachedCacheConfig.vue
+++ b/resources/vue/components/MemcachedCacheConfig.vue
@@ -37,6 +37,7 @@
 <script>
 export default {
     name: 'MemcachedCacheConfig',
+    emits: ['is-valid'],
     props: {
         servers: {
             type: Array,
diff --git a/resources/vue/components/Multiselect.vue b/resources/vue/components/Multiselect.vue
index e1f180347313544098308297efde518183bdd5c2..ee13e8cd7a44f62ca9cb5921e221213d908e39df 100644
--- a/resources/vue/components/Multiselect.vue
+++ b/resources/vue/components/Multiselect.vue
@@ -5,9 +5,10 @@
         :options="transformed_options"
         :reduce="(option) => option.id"
         v-bind="$attrs"
-        v-on="$listeners"
     >
-        <div slot="no-options"><translate>Keine Auswahlmöglichkeiten</translate></div>
+        <template v-slot:no-options>
+            {{ $gettext('Keine Auswahlmöglichkeiten') }}
+        </template>
     </v-select>
 </template>
 
@@ -19,12 +20,16 @@ export default {
     components: {
         vSelect,
     },
+    emits: ['update:model-value'],
     inheritAttrs: false,
     props: {
         name: {
             type: String,
             required: false
         },
+        modelValue: {
+            required: false,
+        },
         value: {
             required: false
         },
@@ -56,7 +61,7 @@ export default {
     watch: {
         selected: {
             handler(newValue, oldValue) {
-                this.$emit('input', newValue);
+                this.$emit('update:model-value', newValue);
             },
             deep: true
         }
diff --git a/resources/vue/components/MyCourses.vue b/resources/vue/components/MyCourses.vue
index 40480251c4b2867d43430721446c5a27dde9382d..1c244cb018350b9a581d751793d6b1b921e66cc3 100644
--- a/resources/vue/components/MyCourses.vue
+++ b/resources/vue/components/MyCourses.vue
@@ -21,18 +21,17 @@
         </studip-message-box>
         <component v-else :is="displayComponent" :icon-size="iconSize"></component>
 
-        <MountingPortal mount-to="#tiled-courses-sidebar-switch .sidebar-widget-content .widget-list" name="sidebar-switch">
-            <my-courses-sidebar-switch></my-courses-sidebar-switch>
-        </MountingPortal>
+        <Teleport to="#tiled-courses-sidebar-switch .sidebar-widget-content .widget-list" name="sidebar-switch">
+            <MyCoursesSidebarSwitch />
+        </Teleport>
 
-        <MountingPortal mount-to="#tiled-courses-new-contents-toggle .sidebar-widget-content .widget-list" name="sidebar-content-toggle">
-            <my-courses-new-content-toggle></my-courses-new-content-toggle>
-        </MountingPortal>
+        <Teleport to="#tiled-courses-new-contents-toggle .sidebar-widget-content .widget-list" name="sidebar-content-toggle">
+            <MyCoursesNewContentToggle />
+        </Teleport>
     </div>
 </template>
 
 <script>
-import { sprintf } from 'sprintf-js';
 import MyCoursesTables from './MyCoursesTables.vue';
 import MyCoursesTiles from './MyCoursesTiles.vue';
 import MyCoursesMixin from '../mixins/MyCoursesMixin.js';
@@ -69,6 +68,10 @@ export default {
         isEmpty () {
             return this.groups.length === 0;
         }
+    },
+    beforeMount() {
+        document.querySelector('#tiled-courses-sidebar-switch .widget-list').innerHTML = '';
+        document.querySelector('#tiled-courses-new-contents-toggle .widget-list').innerHTML = '';
     }
 }
 </script>
diff --git a/resources/vue/components/MyCoursesColorPicker.vue b/resources/vue/components/MyCoursesColorPicker.vue
index cfcb380ac81e4883221e0068831a60c7a50689ab..68eae5d481246505d9ec68188583630445e2368c 100644
--- a/resources/vue/components/MyCoursesColorPicker.vue
+++ b/resources/vue/components/MyCoursesColorPicker.vue
@@ -11,6 +11,7 @@
 <script>
 export default {
     name: "my-courses-color-picker",
+    emits: ['color-picked'],
     props: {
         course: {
             type: Object,
@@ -33,9 +34,9 @@ export default {
             return classes;
         },
         getTitle (i, index) {
-            let title = this.$gettextInterpolate(
-                this.$gettext('Gruppe %{ group }'),
-                {group: i}
+            let title = this.$gettext(
+                'Gruppe %{ group }',
+                { group: i }
             );
             if (this.course.group === index) {
                 title += ' ('  + this.$gettext('ausgewählt') + ')';
diff --git a/resources/vue/components/MyCoursesTables.vue b/resources/vue/components/MyCoursesTables.vue
index b8b0ceb0e9af044db7d0b769942a1aec37549aff..08d57da3c259adb49b507eddb5668435179d1c25 100644
--- a/resources/vue/components/MyCoursesTables.vue
+++ b/resources/vue/components/MyCoursesTables.vue
@@ -43,8 +43,8 @@
                 <tr v-for="course in getOrderedCourses(subgroup.ids)" :data-course-id="course.id" :class="getCourseClasses(course)" :key="course.id">
                     <td :class="`gruppe${course.group}`">
                         <span class="sr-only">
-                            {{ $gettextInterpolate(
-                                $gettext('Diese Veranstaltung gehört zur Farbgruppe %{group}'),
+                            {{ $gettext(
+                                'Diese Veranstaltung gehört zur Farbgruppe %{group}',
                                 course
                             ) }}
                         </span>
diff --git a/resources/vue/components/MyCoursesTiles.vue b/resources/vue/components/MyCoursesTiles.vue
index 12b2627258ab00e501846ba0d2add16a44bd3fcd..c530fd15f79d487e71bb71fa2443a762420d5afa 100644
--- a/resources/vue/components/MyCoursesTiles.vue
+++ b/resources/vue/components/MyCoursesTiles.vue
@@ -1,7 +1,7 @@
 <template>
     <div class="my-courses my-courses--tiles">
-        <template v-for="(group, index) in groups">
-            <div class="group-label" :key="index">{{ group.name }}</div>
+        <template v-for="(group, index) in groups" :key="index">
+            <div class="group-label">{{ group.name }}</div>
             <article class="studip" v-for="subgroup in group.data" :key="subgroup.id" :class="getGroupCssClasses(subgroup)">
                 <header v-if="subgroup.label">
                     <h1>
@@ -9,12 +9,12 @@
                     </h1>
                 </header>
                 <section class="studip-grid">
-                    <template v-for="course in getOrderedCourses(subgroup.ids)">
-                        <div class="course-group-label" v-if="isParent(course)" :key="course.id">
+                    <template v-for="course in getOrderedCourses(subgroup.ids)" :key="course.id">
+                        <div class="course-group-label" v-if="isParent(course)">
                             {{ getCourseName(course, getConfig('sem_number')) }}
                         </div>
 
-                        <article class="studip-grid-element" :data-course-id="course.id" :class="getCourseCssClasses(course)" :key="course.id">
+                        <article class="studip-grid-element" :data-course-id="course.id" :class="getCourseCssClasses(course)">
                             <header class="tiles-grid-element-header">
                                 <span class="tiles-grid-element-options">
                                     <studip-action-menu :items="getActionMenuForCourse(course, true)"
diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/Quicksearch.vue
index 575b464b3305d6b44dd9cab4f143ea7aa1b6d5fb..94c1d518f480374eb5715b4d8901b87f558648a2 100644
--- a/resources/vue/components/Quicksearch.vue
+++ b/resources/vue/components/Quicksearch.vue
@@ -31,6 +31,7 @@
 <script>
 export default {
     name: 'quicksearch',
+    emits: ['update:modelValue'],
     props: {
         searchtype: {
             type: String,
@@ -40,7 +41,7 @@ export default {
             type: String,
             required: false
         },
-        value: {
+        modelValue: {
             type: String,
             required: false,
             default: ''
@@ -59,6 +60,10 @@ export default {
             type: String,
             required: false,
             default: ''
+        },
+        keepValue: {
+            type: Boolean,
+            default: false
         }
     },
     inheritAttrs: false,
@@ -118,7 +123,11 @@ export default {
             }
             this.results = [];
 
-            this.$emit('input', this.returnValue, this.inputValue);
+            this.$emit('update:modelValue', this.returnValue, this.inputValue);
+
+            if (!this.keepValue) {
+                this.inputValue = '';
+            }
         },
         selectUp () {
             if (this.selected > 0) {
@@ -158,8 +167,8 @@ export default {
     },
     created () {
         this.initialize(
-            this.value,
-            this.autocomplete ? this.value : this.needle
+            this.modelValue,
+            this.autocomplete ? this.modelValue : this.needle
         );
     },
     computed: {
@@ -177,7 +186,7 @@ export default {
                 this.search(needle);
             }
             if (this.autocomplete) {
-                this.$emit('input', this.inputValue, this.inputValue);
+                this.$emit('update:modelValue', this.inputValue, this.inputValue);
             }
         }
     }
diff --git a/resources/vue/components/RangeInput.vue b/resources/vue/components/RangeInput.vue
index 650c750d52af9cb2469039500b94af8f144594cc..f28b091f2300798bf7de0c0685f6b41b83772f9d 100644
--- a/resources/vue/components/RangeInput.vue
+++ b/resources/vue/components/RangeInput.vue
@@ -9,21 +9,23 @@
                :aria-valuemax="max"
                :aria-valuenow="myValue"
                v-bind="$attrs"
-               v-on="$listeners"
                v-model="myValue">
-        <output for="fader"><translate :translate-params="{myValue: myValue || '1', max: max}">%{myValue} von %{max}</translate></output>
+        <output for="fader">
+            {{ $gettext('%{myValue} von %{max}', {myValue: myValue || '1', max: max}) }}
+        </output>
     </div>
 </template>
 
 <script>
 export default {
     name: 'range-input',
+    emits: ['update:modelValue'],
     props: {
         name: {
             type: String,
             required: true
         },
-        value: {
+        modelValue: {
             required: false,
             default: 1
         },
@@ -49,7 +51,7 @@ export default {
         };
     },
     mounted () {
-        this.myValue = this.value > this.min ? this.value : this.min;
+        this.myValue = this.modelValue > this.min ? this.modelValue : this.min;
         if (this.myValue > this.max) {
             this.myValue = this.max;
         }
@@ -57,8 +59,8 @@ export default {
     inheritAttrs: false,
     watch: {
         myValue: {
-            handler(newValue, oldValue) {
-                this.$emit('input', newValue);
+            handler(newValue) {
+                this.$emit('update:modelValue', newValue);
             },
             deep: true
         }
diff --git a/resources/vue/components/RedisCacheConfig.vue b/resources/vue/components/RedisCacheConfig.vue
index ad035b37f0393df8cf145f10c9f0787969af2491..8ad4b529e8eded19ebd2b534f8a1bd03b5a32dec 100644
--- a/resources/vue/components/RedisCacheConfig.vue
+++ b/resources/vue/components/RedisCacheConfig.vue
@@ -22,6 +22,7 @@
 <script>
 export default {
     name: 'RedisCacheConfig',
+    emits: ['is-valid'],
     props: {
         hostname: {
             type: String,
diff --git a/resources/vue/components/SearchWithFilter.vue b/resources/vue/components/SearchWithFilter.vue
index 45ca4377fe2aa7f4bf014625ea20a36110818a97..3c45ea369249e5bf8858a8b66bc36d77c535a0fa 100644
--- a/resources/vue/components/SearchWithFilter.vue
+++ b/resources/vue/components/SearchWithFilter.vue
@@ -61,6 +61,7 @@ export default {
             required: true,
         },
     },
+    emits: ['search'],
     components: {
         StudipIcon,
     },
diff --git a/resources/vue/components/SidebarWidget.vue b/resources/vue/components/SidebarWidget.vue
index 692f4f904b245bcf5968cd4e3643bcaab22ab72f..9de82251424fb9a8acc1704b9d04bd1a000ea83c 100644
--- a/resources/vue/components/SidebarWidget.vue
+++ b/resources/vue/components/SidebarWidget.vue
@@ -15,6 +15,7 @@
 <script>
 export default {
     name: 'sidebar-widget',
+    emits: ['scroll'],
     props: {
         title: String,
     },
@@ -27,7 +28,7 @@ export default {
         this.handleDebouncedScroll = _.debounce(this.handleScroll, 100);
         this.$refs.scrollable.addEventListener('scroll', this.handleDebouncedScroll);
     },
-    beforeDestroy() {
+    beforeUnmount() {
         this.$refs.scrollable.removeEventListener('scroll', this.handleDebouncedScroll);
     },
 };
diff --git a/resources/vue/components/StudipActionMenu.vue b/resources/vue/components/StudipActionMenu.vue
index 9bce949d3b7ca3d0b233c315bd964142b7e09b13..a301ac3fa856fd5043fa6d62e0c5fab39dcb3281 100644
--- a/resources/vue/components/StudipActionMenu.vue
+++ b/resources/vue/components/StudipActionMenu.vue
@@ -72,6 +72,8 @@
 </template>
 
 <script>
+import { $gettext } from '../../assets/javascripts/lib/gettext';
+
 export default {
     name: 'studip-action-menu',
     props: {
@@ -86,7 +88,7 @@ export default {
         title: {
             type: String,
             default() {
-                return this.$gettext('Aktionen');
+                return $gettext('Aktionen');
             }
         }
     },
@@ -144,7 +146,7 @@ export default {
             return Number.parseInt(collapseAt) <= this.items.filter((item) => item.type !== 'separator').length;
         },
         tooltip () {
-            return this.context ? this.$gettextInterpolate(this.$gettext('%{title} für %{context}'), {title: this.title, context: this.context}) : this.title;
+            return this.context ? this.$gettext('%{title} für %{context}', {title: this.title, context: this.context}) : this.title;
         }
     }
 }
diff --git a/resources/vue/components/StudipDateTime.vue b/resources/vue/components/StudipDateTime.vue
index dfdc0c3f16a13d545f5a5d629a362ce23d8cbbc0..61f498ba0445fc81a2360acfd5a8ef85ca084580 100644
--- a/resources/vue/components/StudipDateTime.vue
+++ b/resources/vue/components/StudipDateTime.vue
@@ -30,7 +30,7 @@
                 return date.toISOString();
             },
             title () {
-                return this.display_relative() ? this.formatted_date(true) : false;
+                return this.display_relative() ? this.formatted_date(true) : null;
             }
         },
         methods: {
diff --git a/resources/vue/components/StudipDialog.vue b/resources/vue/components/StudipDialog.vue
index 5de97ebdfeadaf1f750957cc2684b7f1be44bf4a..0861d1995f9660e3a0eb5634e6058092749d990a 100644
--- a/resources/vue/components/StudipDialog.vue
+++ b/resources/vue/components/StudipDialog.vue
@@ -1,9 +1,9 @@
 <template>
-    <MountingPortal mountTo="body" append>
+    <Teleport to="body">
         <focus-trap v-model="trap">
             <div class="studip-dialog" @keydown.esc="closeDialog">
                 <transition name="dialog-fade">
-                    <div class="studip-dialog-backdrop">
+                    <div class="studip-dialog-backdrop" v-if="true">
                         <vue-resizeable
                             class="resizable"
                             style="position: absolute"
@@ -15,8 +15,8 @@
                             :top="top"
                             :width="currentWidth"
                             :height="currentHeight"
-                            :min-width="minW | checkEmpty"
-                            :min-height="minH | checkEmpty"
+                            :min-width="minW"
+                            :min-height="minH"
                             @mount="initSize"
                             @resize:move="resizeHandler"
                             @resize:start="resizeHandler"
@@ -103,12 +103,12 @@
                 </transition>
             </div>
         </focus-trap>
-    </MountingPortal>
+    </Teleport>
 </template>
 
 <script>
 import { FocusTrap } from 'focus-trap-vue';
-import VueResizeable from 'vrp-vue-resizable';
+import VueResizeable from 'vue-resizable';
 let uuid = 0;
 const dialogPadding = 3;
 
@@ -118,6 +118,7 @@ export default {
         FocusTrap,
         VueResizeable,
     },
+    emits: ['close', 'confirm'],
     props: {
         height: {
             type: [String, Number],
@@ -268,11 +269,6 @@ export default {
             }
         }
     },
-    filters: {
-        checkEmpty(value) {
-            return typeof value !== "number" ? 0 : value;
-        }
-    },
     mounted() {
         if (this.defaultFocus) {
             this.$nextTick()
diff --git a/resources/vue/components/StudipFileChooser.vue b/resources/vue/components/StudipFileChooser.vue
index deb8aabe9e734ae2c18183ab4ac74e5b39c94578..d7ba0a07561fd1fa57f932e4cc020f8699c08ed2 100644
--- a/resources/vue/components/StudipFileChooser.vue
+++ b/resources/vue/components/StudipFileChooser.vue
@@ -14,7 +14,7 @@ export default {
     components: {
         FileChooserDialog,
     },
-
+    emits: ['select'],
     props: {
         selectable: {
             type: String,
@@ -76,17 +76,20 @@ export default {
                 if (this.selectedId === '') {
                     return this.$gettext('Kein Ordner ausgewählt');
                 }
-                return this.$gettextInterpolate(this.$gettext('Ordner "%{folderName}" ausgewählt'), {
-                    folderName: this.folderById({ id: this.selectedId })?.attributes?.name ?? '-',
-                });
+                return this.$gettext(
+                    'Ordner "%{folderName}" ausgewählt'
+                    ,
+                    { folderName: this.folderById({ id: this.selectedId })?.attributes?.name ?? '-' }
+                );
             }
 
             if (this.selectedId === '') {
                 return this.$gettext('Keine Datei ausgewählt');
             }
-            return this.$gettextInterpolate(this.$gettext('Datei "%{fileName}" ausgewählt'), {
-                fileName: this.fileById({ id: this.selectedId })?.attributes?.name ?? '-',
-            });
+            return this.$gettext(
+                'Datei "%{fileName}" ausgewählt',
+                { fileName: this.fileById({ id: this.selectedId })?.attributes?.name ?? '-' }
+            );
         },
     },
     methods: {
diff --git a/resources/vue/components/StudipFolderSize.vue b/resources/vue/components/StudipFolderSize.vue
index 0c6e0bc6d1221871666154358bfed3e6ced228f4..75f2acb956e7ee99ff13869f93e6f8323988b5fe 100644
--- a/resources/vue/components/StudipFolderSize.vue
+++ b/resources/vue/components/StudipFolderSize.vue
@@ -14,9 +14,11 @@ export default {
                 return '';
             }
 
-            return this.$gettextInterpolate(
-                this.$ngettext('%{count} Objekt', '%{count} Objekte', this.object_count),
-                {count: this.object_count}
+            return this.$ngettext(
+                '%{count} Objekt',
+                '%{count} Objekte',
+                this.object_count,
+                { count: this.object_count }
             );
         }
     }
diff --git a/resources/vue/components/StudipIcon.vue b/resources/vue/components/StudipIcon.vue
index b20aba01833d6a5c94ccab26ebd4d88b787e39b2..b64dfb0802da4eb31effd77a9de145be4fe3a8a5 100644
--- a/resources/vue/components/StudipIcon.vue
+++ b/resources/vue/components/StudipIcon.vue
@@ -1,29 +1,27 @@
 <template>
     <input
         v-if="name"
+        v-bind="$attrs"
         type="image"
         :name="name"
         :src="url"
         :style="{ width: realSize + 'px', height: realSize + 'px' }"
         :role="ariaRole"
         :class="cssClass"
-        v-bind="$attrs"
-        v-on="$listeners"
         :alt="$attrs.alt ?? ''"
     />
     <img v-else
+         v-bind="$attrs"
          :src="url"
          :style="{ width: realSize + 'px', height: realSize + 'px' }"
          :role="ariaRole"
          :class="cssClass"
-         v-bind="$attrs"
-         v-on="$listeners"
          :alt="$attrs.alt ?? ''"
     />
 </template>
 
 <script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
 
 function getCSSVariableValue(property: string): Number {
     const value = getComputedStyle(document.body).getPropertyValue(property);
@@ -33,7 +31,7 @@ function getCSSVariableValue(property: string): Number {
 const defaultIconSize: Number = getCSSVariableValue('--icon-size-default');
 const inlineIconSize: Number = getCSSVariableValue('--icon-size-inline');
 
-export default Vue.extend({
+export default defineComponent({
     name: 'studip-icon',
     props: {
         ariaRole: {
@@ -49,7 +47,10 @@ export default Vue.extend({
             required: false,
             default: 'clickable',
         },
-        shape: String,
+        shape: {
+            type: String,
+            required: true,
+        },
         size: {
             type: Number,
             required: false,
diff --git a/resources/vue/components/StudipIdentImage.vue b/resources/vue/components/StudipIdentImage.vue
index 8879d1171f1f1d61b564616f03e7da234f92d363..f8df91f0b3c187612e0c9fcf78267e218443c2e8 100644
--- a/resources/vue/components/StudipIdentImage.vue
+++ b/resources/vue/components/StudipIdentImage.vue
@@ -5,8 +5,9 @@
 <script>
 export default {
     name: 'studip-ident-image',
+    emits: ['update:modelValue'],
     props: {
-        value: {
+        modelValue: {
             type: String,
         },
         showCanvas: {
@@ -109,7 +110,7 @@ export default {
                 ctx.fill();
             });
 
-            this.$emit('input', canvas.toDataURL());
+            this.$emit('update:modelValue', canvas.toDataURL());
         },
         createPointInEllipse(ctx) {
             const x = this.random();
diff --git a/resources/vue/components/StudipMessageBox.vue b/resources/vue/components/StudipMessageBox.vue
index b2cf48605f29555f90dba440f5d0d26f52162020..ad557a55ac0f7037423f4e15d0bfe583043f8476 100644
--- a/resources/vue/components/StudipMessageBox.vue
+++ b/resources/vue/components/StudipMessageBox.vue
@@ -22,6 +22,7 @@
 <script>
 export default {
     name: 'studip-message-box',
+    emits: ['close'],
     props: {
         type: {
             type: String, // exception, error, success, info, warning
diff --git a/resources/vue/components/StudipMultiPersonSearch.vue b/resources/vue/components/StudipMultiPersonSearch.vue
index 5218a76e8c4ec7d1424fbf6427b1f208b8bcd9a9..c87dd4f977138252adea9431c82046292b025bc8 100644
--- a/resources/vue/components/StudipMultiPersonSearch.vue
+++ b/resources/vue/components/StudipMultiPersonSearch.vue
@@ -18,6 +18,7 @@
 <script>
 export default {
     name: 'studip-multi-person-search',
+    emits: ['update:model-value'],
     props: {
         name: String,
         withDetail: {
@@ -42,7 +43,7 @@ export default {
     },
     computed: {
         id() {
-            return this._uid;
+            return this._.uid;
         },
         count_text_id() {
             return this.id + '_count';
@@ -71,9 +72,9 @@ export default {
             });
             let selection_header = document.createElement('div');
             selection_header.setAttribute('id', this.count_text_id);
-            selection_header.innerText = this.$gettextInterpolate(
-                this.$gettext('Sie haben %{ count } Personen ausgewählt'),
-                {count: this.count}
+            selection_header.innerText = this.$gettext(
+                'Sie haben %{ count } Personen ausgewählt',
+                { count: this.count }
             );
 
             $('#' + this.select_box_id).multiSelect({
@@ -108,9 +109,9 @@ export default {
                     if (searchcount === 0) {
                         view.append(
                             '--',
-                            view.$gettextInterpolate(
-                                view.$gettext('Es wurden keine neuen Ergebnisse für "%{ needle }" gefunden.'),
-                                {needle: view.searchTerm}
+                            view.$gettext(
+                                'Es wurden keine neuen Ergebnisse für "%{ needle }" gefunden.',
+                                { needle: view.searchTerm }
                             ),
                             true
                         );
@@ -164,9 +165,9 @@ export default {
 
         updateCount(){
             this.count = $('#' + this.select_box_id + ' option:enabled:selected').length;
-            $('#' + this.count_text_id).text(this.$gettextInterpolate(
-                this.$gettext('Sie haben %{ count } Personen ausgewählt'),
-                {count: this.count}
+            $('#' + this.count_text_id).text(this.$gettext(
+                'Sie haben %{ count } Personen ausgewählt',
+                { count: this.count }
             ));
         },
 
@@ -192,7 +193,7 @@ export default {
             } else {
                 return_value = user_ids;
             }
-            this.$emit('input', return_value);
+            this.$emit('update:model-value', return_value);
         }
     },
 }
diff --git a/resources/vue/components/StudipPagination.vue b/resources/vue/components/StudipPagination.vue
index 8b82f989460b82149c1c4a20ba12925a607421ba..5e6893bbf61a3a80b186b2abc67149e1622ca720 100644
--- a/resources/vue/components/StudipPagination.vue
+++ b/resources/vue/components/StudipPagination.vue
@@ -10,18 +10,18 @@
                     {{ $gettext('zurück') }}
                 </button>
             </li>
-            <template v-for="offset of offsets">
-                <li :key="'end-dots-' + offset" class="divider"
+            <template v-for="offset of offsets" :key="offset">
+                <li class="divider"
                     v-if="offset === (total_offsets - 1) && currentOffset < (total_offsets - 1) - (range + 1)">
                     &hellip;
                 </li>
-                <li :key="'offset-' + offset" :class="{'current': offset === currentOffset, 'no-divider': offset === 0}">
+                <li :class="{'current': offset === currentOffset, 'no-divider': offset === 0}">
                     <button class="pagination--link" @click.prevent="goTo(offset)">
                         <span class="audible">{{ $gettext('Seite') }}</span>
                         {{ offset + 1 }}
                     </button>
                 </li>
-                <li :key="'start-dots' + offset" class="divider"
+                <li class="divider"
                     v-if="offset === 0 && currentOffset > range + 1">
                     &hellip;
                 </li>
@@ -39,6 +39,7 @@
 <script>
 export default {
     name: 'studip-pagination',
+    emits: ['updateOffset'],
     props: {
         currentOffset: {
             type: Number,
@@ -60,11 +61,10 @@ export default {
     },
     computed: {
         pagination_id() {
-            return 'pagination-label-' + this._uid;
+            return 'pagination-label-' + this._.uid;
         },
         total_offsets() {
-            let total = Math.ceil(this.totalItems / this.itemsPerPage);
-            return total;
+            return Math.ceil(this.totalItems / this.itemsPerPage);
         },
         offsets() {
             let offsets = [0, this.currentOffset, (this.total_offsets - 1)];
diff --git a/resources/vue/components/StudipProgressIndicator.vue b/resources/vue/components/StudipProgressIndicator.vue
index 62a33a8840fa4c476571da1d25bc903d126b150c..bdc8af426aba5d39c707fc0983196c00322cf57b 100644
--- a/resources/vue/components/StudipProgressIndicator.vue
+++ b/resources/vue/components/StudipProgressIndicator.vue
@@ -5,7 +5,7 @@
             {{ description }}
         </p>
         <p v-else class="progress-indicator-description-default">
-            <translate>Lade...</translate>
+            {{ $gettext('Lade...') }}
         </p>
     </div>
 </template>
diff --git a/resources/vue/components/StudipProxiedCheckbox.vue b/resources/vue/components/StudipProxiedCheckbox.vue
index 76d07852b8de1f11f03e3d282f45b025180e3bce..332815338be2a0967aa9d2089fd702595cb78126 100644
--- a/resources/vue/components/StudipProxiedCheckbox.vue
+++ b/resources/vue/components/StudipProxiedCheckbox.vue
@@ -1,11 +1,10 @@
 <script>
+import { h } from 'vue';
+
 let uuid = 0;
 export default {
     name: 'studip-proxied-checkbox',
-    model: {
-        prop: 'selected',
-        event: 'change',
-    },
+    emits: ['update:selected'],
     props: {
         name: String,
         id: String,
@@ -27,7 +26,7 @@ export default {
                 selected.add(this.value);
             }
 
-            this.$emit('change', [...selected.values()]);
+            this.$emit('update:selected', [...selected.values()]);
         }
     },
     computed: {
@@ -38,23 +37,15 @@ export default {
             return this.selected.includes(this.value);
         },
     },
-    render (createElement) {
-        const checkbox = createElement('input', {
-            attrs: {
-                type: 'checkbox',
-                name: this.name,
-                id: this.proxiedId,
-                value: this.value,
-            },
-            domProps: {
-                checked: this.checked,
-            },
-            on: {
-                change: this.changeCollection,
-            }
+    render () {
+        return h('input', {
+            type: 'checkbox',
+            name: this.name,
+            id: this.proxiedId,
+            value: this.value,
+            checked: this.checked ? true : null,
+            onChange: this.changeCollection,
         });
-
-        return checkbox;
     }
 };
 </script>
diff --git a/resources/vue/components/StudipProxyCheckbox.vue b/resources/vue/components/StudipProxyCheckbox.vue
index b9cd2187429f167dfb489032b8a4c1001e9fa8b9..e19351da3906c58a2e543f834e2c28b9ff9824fb 100644
--- a/resources/vue/components/StudipProxyCheckbox.vue
+++ b/resources/vue/components/StudipProxyCheckbox.vue
@@ -1,12 +1,12 @@
 <script>
+import { h } from 'vue';
+
 let uuid = 0;
 export default {
     name: 'studip-proxy-checkbox',
-    model: {
-        prop: 'selected',
-        event: 'change',
-    },
+    emits: ['update:selected'],
     props: {
+        name: String,
         id: String,
         total: {
             type: Array,
@@ -15,11 +15,11 @@ export default {
        selected: {
             type: Array,
             required: true,
-        }
+        },
     },
     methods: {
         changeProxy () {
-            this.$emit('change', this.checked ? [] : [...this.total] );
+            this.$emit('update:selected', this.checked ? [] : [...this.total] );
         }
     },
     computed: {
@@ -33,23 +33,15 @@ export default {
             return this.selected.length > 0 && this.selected.length !== this.total.length;
         }
     },
-    render (createElement) {
-        const checkbox = createElement('input', {
-            attrs: {
-                type: 'checkbox',
-                name: this.name,
-                id: this.proxyId
-            },
-            domProps: {
-                checked: this.checked,
-                indeterminate: this.indeterminate,
-            },
-            on: {
-                change: this.changeProxy,
-            }
+    render () {
+        return h('input', {
+            type: 'checkbox',
+            name: this.name,
+            id: this.proxyId,
+            checked: this.checked ? true : null,
+            indeterminate: this.indeterminate ? true : null,
+            onChange: this.changeProxy,
         });
-
-        return checkbox;
     }
 };
 </script>
diff --git a/resources/vue/components/StudipSelect.vue b/resources/vue/components/StudipSelect.vue
index 196a91bd5fd4338b417d7870657acf870a13faf2..2bb209f4af21d2e533ccbca77d749706bbce5817 100644
--- a/resources/vue/components/StudipSelect.vue
+++ b/resources/vue/components/StudipSelect.vue
@@ -1,13 +1,13 @@
 <template>
     <v-select ref="select"
-        @change="updateValue"
-        v-bind="{...$props, ...$attrs}"
-        v-on="$listeners"
-        :calculate-position="withPopper"
-        class="studip-v-select"
-        append-to-body
+              v-bind="{...$props, ...$attrs}"
+              :model-value="modelValue"
+              @update:modelValue="value => $emit('update:modelValue', value)"
+              :calculate-position="withPopper"
+              class="studip-v-select"
+              append-to-body
     >
-        <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
+        <template v-for="(index, name) in $slots" v-slot:[name]="data">
             <slot :name="name" v-bind="data"></slot>
         </template>
     </v-select>
@@ -17,13 +17,16 @@
 import vSelect from 'vue-select';
 import { createPopper } from '@popperjs/core'
 import 'vue-select/dist/vue-select.css'
+
 export default {
     name: 'studip-select',
+    emits: ['update:modelValue'],
     inheritAttrs: false,
     components: {
         vSelect,
     },
     props: {
+        modelValue: [String, Number, Object, Array],
         maxHeight: {
             type: String,
             default: '12em'
@@ -31,7 +34,8 @@ export default {
     },
     methods: {
         updateValue(val) {
-            this.$emit('input', val)
+            console.log('Updating value', val);
+            this.$emit('update:modelValue', val)
         },
         withPopper(dropdownList, component, { width }) {
             if (component.$el?.offsetParent.classList.contains('studip-dialog-content')) {
diff --git a/resources/vue/components/StudipSquareButton.vue b/resources/vue/components/StudipSquareButton.vue
index fe89e8d6315d850d70134d6d515eea468321351b..42434dd02ebadd28e5a5125f256998b0e9c377ce 100644
--- a/resources/vue/components/StudipSquareButton.vue
+++ b/resources/vue/components/StudipSquareButton.vue
@@ -8,6 +8,7 @@
 <script>
 export default {
     name: 'studip-square-button',
+    emits: ['click'],
     props: {
         icon: {
             type: String,
diff --git a/resources/vue/components/StudipUserFilter.vue b/resources/vue/components/StudipUserFilter.vue
index 20ca16201f7775a0731c31e2e0b67db23667ab4c..ea48383b88c8c7b9817276895dbd3b5a3d4837d3 100644
--- a/resources/vue/components/StudipUserFilter.vue
+++ b/resources/vue/components/StudipUserFilter.vue
@@ -61,6 +61,7 @@
 <script>
 export default {
     name: 'StudipUserFilter',
+    emits: ['close', 'submit'],
     props: {
         filter: {
             type: Array,
diff --git a/resources/vue/components/StudipWizardDialog.vue b/resources/vue/components/StudipWizardDialog.vue
index 073c25f9fd391ef25816b5125338c92698ba8b16..5c60a27fac77d72c9831bcd9a647536bde3625f1 100644
--- a/resources/vue/components/StudipWizardDialog.vue
+++ b/resources/vue/components/StudipWizardDialog.vue
@@ -19,7 +19,7 @@
                     <p class="wizard-description">
                         {{ activeSlot.description }}
                     </p>
-                    <p v-if="requirements.length > 0" class="wizard-requirements">
+                    <div v-if="requirements.length > 0" class="wizard-requirements">
                         <span>{{ $gettext('Bitte geben Sie die folgenden Informationen an:') }}</span>
                         <ul>
                             <li v-for="(requirement, index) in requirements" :key="requirement.slot.name + '_' + index">
@@ -32,7 +32,7 @@
                                 </button>
                             </li>
                         </ul>
-                    </p>
+                    </div>
                 </div>
                 <div class="wizard-content-wrapper">
                     <h2>
@@ -102,6 +102,7 @@ import StudipDialog from './StudipDialog.vue'
 import StudipIcon from './StudipIcon.vue';
 export default {
     name: 'studip-wizard-dialog',
+    emits: ['close', 'confirm'],
     components: {
         StudipDialog,
         StudipIcon
diff --git a/resources/vue/components/StudipWysiwyg.vue b/resources/vue/components/StudipWysiwyg.vue
index 3b36cc8b11192795e3705527e18d9a87078d09ff..c7de9c7f6e55f37d5c1845ab41f54113998a29be 100644
--- a/resources/vue/components/StudipWysiwyg.vue
+++ b/resources/vue/components/StudipWysiwyg.vue
@@ -1,30 +1,18 @@
-<template>
-    <ckeditor
-        :editor="editor"
-        :config="editorConfig"
-        @ready="onReady"
-        v-model="currentText"
-        @input="onInput"
-    />
-</template>
-
 <script>
 import { ClassicEditor, BalloonEditor } from '../../assets/javascripts/chunks/wysiwyg.js';
+import {h, resolveComponent} from "vue";
 
 export default {
     name: 'studip-wysiwyg',
-    model: {
-        prop: 'text',
-        event: 'input',
-    },
+    emits: ['update:modelValue'],
     props: {
-        text: {
+        modelValue: {
             type: String,
             required: true,
         },
         editorType: {
             type: String,
-            validator: function (value) {
+            validator(value) {
                 return ['classic', 'balloon'].includes(value);
             },
             default: 'classic',
@@ -54,7 +42,7 @@ export default {
     methods: {
         onReady(editor) {
             this.createdEditor = editor;
-            this.currentText = this.text;
+            this.currentText = this.modelValue;
 
             if (this.shouldFocus) {
                 this.focus();
@@ -64,7 +52,7 @@ export default {
         },
         onInput(value) {
             this.currentText = value;
-            this.$emit('input', value);
+            this.$emit('update:modelValue', value);
         },
         focus() {
             if (this.createdEditor) {
@@ -77,5 +65,14 @@ export default {
     created() {
         STUDIP.loadChunk('mathjax');
     },
+    render() {
+        return h(resolveComponent('ckeditor'), {
+            editor: this.editor,
+            config: this.editorConfig,
+            modelValue: this.modelValue,
+            onInput: this.onInput,
+            onReady: this.onReady,
+        })
+    }
 };
 </script>
diff --git a/resources/vue/components/SystemNotification.vue b/resources/vue/components/SystemNotification.vue
index 82324a155d602ea3cd74025f549be70f417f9afa..d8c9384587b65c5eb8bdc03985014a05a667125f 100644
--- a/resources/vue/components/SystemNotification.vue
+++ b/resources/vue/components/SystemNotification.vue
@@ -59,6 +59,7 @@
 <script>
 export default {
     name: 'SystemNotification',
+    emits: ['destroyMe'],
     props: {
         allowClosing: {
             type: Boolean,
@@ -174,7 +175,7 @@ export default {
             }, timing);
         }
     },
-    destroyed() {
+    unmounted() {
         this.globalOff('disrupt-system-notifications', this.disruptTimeout);
         this.globalOff('resume-system-notifications', this.initTimeout);
     }
diff --git a/resources/vue/components/Timepicker.vue b/resources/vue/components/Timepicker.vue
index d3a1ec6b9cc28dffdb011ceaf28b781942b5074a..0bef84e4157c73d1bc8140c5b70b7fc2380d0612 100644
--- a/resources/vue/components/Timepicker.vue
+++ b/resources/vue/components/Timepicker.vue
@@ -12,13 +12,14 @@
 <script>
 export default {
     name: 'Timepicker',
+    emits: ['update:modelValue'],
     inheritAttrs: false,
     props: {
         name: {
             type: String,
             required: false
         },
-        value: String,
+        modelValue: String,
         mintime: String,
         maxtime: String,
         placeholder: String,
@@ -26,10 +27,10 @@ export default {
     computed: {
         timeValue: {
             get() {
-                return this.value;
+                return this.modelValue;
             },
             set(value) {
-                this.$emit('input', value);
+                this.$emit('update:modelValue', value);
             }
         }
     }
diff --git a/resources/vue/components/WikiEditor.vue b/resources/vue/components/WikiEditor.vue
index 3fad25b017b6cb5461e0ee189cb4da967a26f5c5..8fc2e41292f8d993d4cc33d38c23372fe8ba8be5 100644
--- a/resources/vue/components/WikiEditor.vue
+++ b/resources/vue/components/WikiEditor.vue
@@ -46,7 +46,7 @@
                         @click.prevent="delegateEditMode(user.user_id)"
                         class="button"
                 >
-                    {{ $gettextInterpolate($gettext('Schreibmodus an %{name} übergeben'), { name: user.fullname }, true) }}
+                    {{ $gettext('Schreibmodus an %{name} übergeben', { name: user.fullname }, true) }}
                 </button>
             </footer>
         </form>
@@ -75,7 +75,6 @@
         </div>
 
         <wiki-editor-online-users :users="onlineUsers"></wiki-editor-online-users>
-
     </div>
 </template>
 <script>
@@ -84,6 +83,7 @@ import StudipDateTime from "./StudipDateTime.vue";
 import JSUpdater from "@/assets/javascripts/lib/jsupdater";
 import ContentBar from "./ContentBar.vue";
 import ContentBarBreadcrumbs from "./ContentBarBreadcrumbs.vue";
+import {markRaw} from "vue";
 
 export default {
     name: 'wiki-editor',
@@ -227,7 +227,7 @@ export default {
                 this.focusEditor();
             }
 
-            this.editor = editor;
+            this.editor = markRaw(editor);
         });
 
         JSUpdater.register(
diff --git a/resources/vue/components/WikiEditorOnlineUsers.vue b/resources/vue/components/WikiEditorOnlineUsers.vue
index 71c7c1c9929662ec9a54fe38407f6c55ca97aee7..7bee689b83f834a7bcf4e01cd1d4048224117a86 100644
--- a/resources/vue/components/WikiEditorOnlineUsers.vue
+++ b/resources/vue/components/WikiEditorOnlineUsers.vue
@@ -1,5 +1,5 @@
 <template>
-    <MountingPortal mountTo="#sidebar" append name="wiki_online_editing_users">
+    <Teleport to="#sidebar" append name="wiki_online_editing_users">
         <SidebarWidget :title="$gettext('Anwesende Personen')">
              <template #content>
                 <ol class="clean">
@@ -17,7 +17,7 @@
                 </ol>
              </template>
         </SidebarWidget>
-    </MountingPortal>
+    </Teleport>
 </template>
 <script>
 import SidebarWidget from "./SidebarWidget.vue";
diff --git a/resources/vue/components/admission/AdmissionRuleConfig.vue b/resources/vue/components/admission/AdmissionRuleConfig.vue
index b9691a832ba056196277af78556c85991eeea8a4..921a6c2914c13cbd951e4ac955e336480106433b 100644
--- a/resources/vue/components/admission/AdmissionRuleConfig.vue
+++ b/resources/vue/components/admission/AdmissionRuleConfig.vue
@@ -28,8 +28,11 @@
 </template>
 
 <script>
+import {shallowRef} from "vue";
+
 export default {
     name: 'AdmissionRuleConfig',
+    emits: ['cancel', 'submit'],
     props: {
         type: {
             type: String,
@@ -75,9 +78,8 @@ export default {
     },
     created() {
         const file = STUDIP.Admission.availableRules[this.type];
-        let components = {};
         import(`@/vue/components/admission/${file}`).then((module) => {
-            this.component = module.default;
+            this.component = shallowRef(module.default);
             this.props = {
                 id: this.theRule?.id,
                 ruleData: this.theRule,
diff --git a/resources/vue/components/admission/AdmissionRuleTypeSelector.vue b/resources/vue/components/admission/AdmissionRuleTypeSelector.vue
index 498497aa5a184bb712a015cfab7e796778bea808..e8bbe8c9fe69adee5f770494dbf07ff6ff122c36 100644
--- a/resources/vue/components/admission/AdmissionRuleTypeSelector.vue
+++ b/resources/vue/components/admission/AdmissionRuleTypeSelector.vue
@@ -70,6 +70,7 @@ import StudipDialog from '../StudipDialog.vue';
 
 export default {
     name: 'AdmissionRuleTypeSelector',
+    emits: ['close', 'configureRule'],
     components: { StudipProgressIndicator, StudipDialog },
     props: {
         assignedRuleTypes: {
diff --git a/resources/vue/components/admission/ConfigureCourseSet.vue b/resources/vue/components/admission/ConfigureCourseSet.vue
index f23f17250e0f7df9bf140d8f6bb3e7c4e766c6f1..190c149de53564ffac1e0ab0a6a8a2ffa03adde9 100644
--- a/resources/vue/components/admission/ConfigureCourseSet.vue
+++ b/resources/vue/components/admission/ConfigureCourseSet.vue
@@ -44,7 +44,6 @@
                     <quicksearch v-if="instituteSearch"
                                  :searchtype="instituteSearch"
                                  name="institute"
-                                 :key="NaN"
                                  id="isearch"
                                  @input="addInstitute"
                                  :aria-label="$gettext('Geben Sie einen Suchbegriff mit mehr als 3 Zeichen ein, um nach Einrichtungen zu suchen')"
@@ -79,14 +78,14 @@
                                 </td>
                                 <td class="actions">
                                     <button v-if="myInstitutes?.length !== 1"
-                                            :title="$gettextInterpolate(
-                                                $gettext('Zuordnung der Einrichtung %{name} entfernen'),
+                                            :title="$gettext(
+                                                'Zuordnung der Einrichtung %{name} entfernen',
                                                 { name: institute.name }
-                                                )"
-                                            :aria-label="$gettextInterpolate(
-                                                $gettext('Zuordnung der Einrichtung %{name} entfernen'),
+                                            )"
+                                            :aria-label="$gettext(
+                                                'Zuordnung der Einrichtung %{name} entfernen',
                                                 { name: institute.name }
-                                                )"
+                                            )"
                                             class="as-link delete-assignment"
                                             tabindex="0"
                                             @click.prevent="removeInstitute(index)">
@@ -139,8 +138,10 @@
                     <table v-if="availableCourses?.length > 0"
                            class="default">
                         <caption>
-                            {{ $gettextInterpolate($gettext('Veranstaltungen im %{semester}'),
-                                { semester: allSemesters[selectedSemester].name }) }}
+                            {{ $gettext(
+                                'Veranstaltungen im %{semester}',
+                                { semester: allSemesters[selectedSemester].name }
+                            ) }}
                         </caption>
                         <colgroup>
                             <col style="width: 15px">
@@ -160,8 +161,10 @@
                                         <input type="checkbox"
                                                :value="course.id"
                                                v-model="checkedCourses"
-                                               :title="$gettextInterpolate($gettext('Veranstaltung %{coursename} dem Anmeldeset zuordnen'),
-                                                { coursename: course.attributes.title })">
+                                               :title="$gettext(
+                                                   'Veranstaltung %{coursename} dem Anmeldeset zuordnen',
+                                                   { coursename: course.attributes.title }
+                                               )">
                                         <template v-if="course.attributes['course-number']">
                                             {{ course.attributes['course-number'] }}
                                         </template>
@@ -195,12 +198,14 @@
                                 {{ course.attributes.title }}
                             </td>
                             <td class="actions">
-                                <button :title="$gettextInterpolate(
-                                            $gettext('Zuordnung der Veranstaltung %{name} entfernen'),
-                                            { name: course.attributes.title })"
-                                        :aria-label="$gettextInterpolate(
-                                            $gettext('Zuordnung der Veranstaltung %{name} entfernen'),
-                                            { name: course.attributes.title })"
+                                <button :title="$gettext(
+                                            'Zuordnung der Veranstaltung %{name} entfernen',
+                                            { name: course.attributes.title }
+                                        )"
+                                        :aria-label="$gettext(
+                                            'Zuordnung der Veranstaltung %{name} entfernen',
+                                            { name: course.attributes.title }
+                                        )"
                                         class="as-link delete-assignment"
                                         tabindex="0"
                                         @click.prevent="removeCourse(index)">
@@ -218,9 +223,10 @@
                 <button v-if="numApplicants > 0"
                         class="button"
                         @click.prevent="getApplicants">
-                    {{ $gettextInterpolate(
-                        $gettext('Liste der Anmeldungen (%{number} Personen)'),
-                        { number: numApplicants }) }}
+                    {{ $gettext(
+                        'Liste der Anmeldungen (%{number} Personen)',
+                        { number: numApplicants }
+                    ) }}
                 </button>
                 <button v-if="numApplicants > 0"
                         class="button"
@@ -244,27 +250,27 @@
                             >
                                 <td v-html="rule.attributes.ruletext"></td>
                                 <td class="actions">
-                                    <button :title="$gettextInterpolate(
-                                                $gettext('Regel %{name} bearbeiten'),
+                                    <button :title="$gettext(
+                                                'Regel %{name} bearbeiten',
                                                 { name: rule.attributes.name }
-                                                )"
-                                            :aria-label="$gettextInterpolate(
-                                                $gettext('Regel %{name} bearbeiten'),
+                                            )"
+                                            :aria-label="$gettext(
+                                                'Regel %{name} bearbeiten',
                                                 { name: rule.attributes.name }
-                                                )"
+                                            )"
                                             class="as-link edit-assignment"
                                             tabindex="0"
                                             @click.prevent="configureRule(rule.attributes.type, rule, index)">
                                         <studip-icon shape="edit" :size="16"></studip-icon>
                                     </button>
-                                    <button :title="$gettextInterpolate(
-                                                $gettext('Regel %{name} entfernen'),
+                                    <button :title="$gettext(
+                                                'Regel %{name} entfernen',
                                                 { name: rule.attributes.name }
-                                                )"
-                                            :aria-label="$gettextInterpolate(
-                                                $gettext('Regel %{name} entfernen'),
+                                            )"
+                                            :aria-label="$gettext(
+                                                'Regel %{name} entfernen',
                                                 { name: rule.attributes.name }
-                                                )"
+                                            )"
                                             class="as-link delete-assignment"
                                             tabindex="0"
                                             data-confirm="$gettext('Soll die Regel wirklich entfernt werden?')"
@@ -630,12 +636,9 @@ export default {
                 && (rule?.length > 0 ? rule[0].attributes.payload['distribution-time'] > 0 : false);
         },
         userListText(factor, count) {
-            return this.$gettextInterpolate(
-                factor < 1
-                ? this.$gettext('%{number} Personen werden nachrangig eingetragen')
-                    : this.$gettext('%{number} Personen werden bevorzugt'),
-                { number: count }
-            );
+            return factor < 1
+                ? this.$gettext('%{number} Personen werden nachrangig eingetragen', { number: count })
+                : this.$gettext('%{number} Personen werden bevorzugt', { number: count });
         },
         openUserListUsers() {
             STUDIP.Dialog.fromURL(
diff --git a/resources/vue/components/blubber/Comment.vue b/resources/vue/components/blubber/Comment.vue
index 2d4c897933a72f74ad07422ddc3974d22b78c63a..a91648d46d707e96f3a3235c750bcd9d31efa165 100644
--- a/resources/vue/components/blubber/Comment.vue
+++ b/resources/vue/components/blubber/Comment.vue
@@ -52,6 +52,7 @@
 <script>
 export default {
     name: 'BlubberComment',
+    emits: ['answer-comment', 'change-comment', 'edit-comment', 'remove-comment'],
     data: () => ({
         localText: '',
         commentWidth: 0,
diff --git a/resources/vue/components/blubber/CommunityPage.vue b/resources/vue/components/blubber/CommunityPage.vue
index 8831978d45b8c70950f092d38b9eca37ce83871b..bd937e08e1a173c471518c68e7a55ea2d25b6f66 100644
--- a/resources/vue/components/blubber/CommunityPage.vue
+++ b/resources/vue/components/blubber/CommunityPage.vue
@@ -1,21 +1,19 @@
 <template>
-    <div>
-        <BlubberPanel :threadId="threadId" :search="search" v-if="threadId" />
+    <BlubberPanel :threadId="threadId" :search="search" v-if="threadId" />
 
-        <MountingPortal mountTo="#blubber-search-widget" name="sidebar-blubber-search">
-            <BlubberSearchWidget :search="search" />
-        </MountingPortal>
-        <MountingPortal mountTo="#blubber-threads-widget" name="sidebar-blubber-threads">
-            <BlubberThreadsWidget
-                :hasMoreThreads="hasMoreThreads"
-                :threadId="threadId"
-                :threads="threads"
-                @load-more-threads="onLoadMoreThreads"
-                @select-thread="onSelectThread"
-                class="blubber_threads_widget"
-            />
-        </MountingPortal>
-    </div>
+    <Teleport to="#blubber-search-widget" name="sidebar-blubber-search">
+        <BlubberSearchWidget :search="search" />
+    </Teleport>
+    <Teleport to="#blubber-threads-widget" name="sidebar-blubber-threads">
+        <BlubberThreadsWidget
+            :hasMoreThreads="hasMoreThreads"
+            :threadId="threadId"
+            :threads="threads"
+            @load-more-threads="onLoadMoreThreads"
+            @select-thread="onSelectThread"
+            class="blubber_threads_widget"
+        />
+    </Teleport>
 </template>
 
 <script>
@@ -82,7 +80,7 @@ export default {
             }
         });
     },
-    beforeDestroy() {
+    beforeUnmount() {
         this.globalOff('studip:select-blubber-thread', this.handleSelectBlubberThread);
     },
 };
diff --git a/resources/vue/components/blubber/Composer.vue b/resources/vue/components/blubber/Composer.vue
index 72a5d1c4c56c0b83fe55eb7f647c872219dc6e6c..4132a10293130e390c29eed1f30147f3a9a965ab 100644
--- a/resources/vue/components/blubber/Composer.vue
+++ b/resources/vue/components/blubber/Composer.vue
@@ -27,10 +27,7 @@
 <script>
 export default {
     name: 'blubber-composer',
-    model: {
-        prop: 'text',
-        event: 'change',
-    },
+    emits: ['update:modelValue', 'add-posting', 'pick-files', 'edit-previous'],
     props: {
         placeholder: {
             type: String,
@@ -40,7 +37,7 @@ export default {
             type: Number,
             default: 0,
         },
-        text: {
+        modelValue: {
             type: String,
             default: '',
         },
@@ -109,11 +106,11 @@ export default {
         },
         saveCommentToSession() {
             this.resizeTextarea();
-            this.$emit('change', this.localText);
+            this.$emit('update:modelValue', this.localText);
         },
     },
     mounted() {
-        this.localText = this.text;
+        this.localText = this.modelValue;
         this.$nextTick(() => {
             this.resizeTextarea();
         });
diff --git a/resources/vue/components/blubber/Panel.vue b/resources/vue/components/blubber/Panel.vue
index f70d1f637a2fd7cf565828b51f6709b551a24cb7..d38b5232a44852caa9b391c11f6ef9527833fec4 100644
--- a/resources/vue/components/blubber/Panel.vue
+++ b/resources/vue/components/blubber/Panel.vue
@@ -5,7 +5,7 @@
         v-if="!doneFetching"
     />
 
-    <div class="blubber_panel" v-else-if="thread">
+    <template v-else-if="thread">
         <div id="blubber_stream_container">
             <BlubberThread
                 ref="thread"
@@ -24,7 +24,7 @@
             ></BlubberThread>
         </div>
         <BlubberSideInfo :thread="thread" />
-    </div>
+    </template>
 </template>
 
 <script>
diff --git a/resources/vue/components/blubber/Thread.vue b/resources/vue/components/blubber/Thread.vue
index 676ff35e6b649161be1406e780bd9f5ffcd5dd4f..e7a4c4188fa1cea80a0d9c5d53c022150c82ecbd 100644
--- a/resources/vue/components/blubber/Thread.vue
+++ b/resources/vue/components/blubber/Thread.vue
@@ -67,6 +67,15 @@ export default {
         BlubberComposer,
         ThreadSubscriber,
     },
+    emits: [
+        'add-posting',
+        'change-comment',
+        'load-newer',
+        'load-older',
+        'pick-files',
+        'remove-comment',
+        'subscribe-thread',
+    ],
     props: {
         comments: {
             type: Array,
@@ -218,7 +227,7 @@ export default {
             }
         });
     },
-    beforeDestroy() {
+    beforeUnmount() {
         this.$refs.scrollable.removeEventListener('scroll', this.handleDebouncedScroll);
     },
     beforeUpdate() {
diff --git a/resources/vue/components/blubber/ThreadSubscriber.vue b/resources/vue/components/blubber/ThreadSubscriber.vue
index c259a7ad5393de26b62d46ab594469a59cbcebe6..28ad7e00059a7fc81defaf5a119e3e6850e68188 100644
--- a/resources/vue/components/blubber/ThreadSubscriber.vue
+++ b/resources/vue/components/blubber/ThreadSubscriber.vue
@@ -14,9 +14,10 @@
     </div>
 </template>
 <script lang="ts">
-import Vue from 'vue';
+import { defineComponent } from 'vue';
 
-export default Vue.extend({
+export default defineComponent({
+    emits: ['subscribe-thread'],
     props: {
         followed: {
             type: Boolean,
diff --git a/resources/vue/components/blubber/ThreadsWidget.vue b/resources/vue/components/blubber/ThreadsWidget.vue
index c19efb437e433dfee9247294f13af214c7624dfe..4eed89f54d3ad3124f8b89bb5660d3cb5744b221 100644
--- a/resources/vue/components/blubber/ThreadsWidget.vue
+++ b/resources/vue/components/blubber/ThreadsWidget.vue
@@ -43,6 +43,7 @@
 import SidebarWidget from '../SidebarWidget.vue';
 
 export default {
+    emits: ['load-more-threads', 'select-thread'],
     props: {
         hasMoreThreads: {
             type: Boolean,
diff --git a/resources/vue/components/courseware/ActivitiesApp.vue b/resources/vue/components/courseware/ActivitiesApp.vue
index 10c605655afe20d877d8e40c7bebd494bbd67437..af23647da0978efa085ca48bb15cf45ab6c92a4a 100644
--- a/resources/vue/components/courseware/ActivitiesApp.vue
+++ b/resources/vue/components/courseware/ActivitiesApp.vue
@@ -1,12 +1,12 @@
 <template>
     <div class="cw-activities-wrapper">
         <courseware-activities />
-        <MountingPortal mountTo="#courseware-activities-widget-filter-type" name="sidebar-filter-type">
+        <Teleport to="#courseware-activities-widget-filter-type" name="sidebar-filter-type">
             <courseware-activities-widget-filter-type />
-        </MountingPortal>
-        <MountingPortal mountTo="#courseware-activities-widget-filter-unit" name="sidebar-filter-unit">
+        </Teleport>
+        <Teleport to="#courseware-activities-widget-filter-unit" name="sidebar-filter-unit">
             <courseware-activities-widget-filter-unit />
-        </MountingPortal>
+        </Teleport>
     </div>
 </template>
 
diff --git a/resources/vue/components/courseware/AdminApp.vue b/resources/vue/components/courseware/AdminApp.vue
index 4f2df7bbbcabfe2ff5aabbde5dd47bde4d4e3ae3..b0df1e16b3da7ecbbd69b76740ce04b0182adaa8 100644
--- a/resources/vue/components/courseware/AdminApp.vue
+++ b/resources/vue/components/courseware/AdminApp.vue
@@ -1,12 +1,12 @@
 <template>
     <div class="cw-admin">
         <courseware-admin-templates v-if="templatesView" />
-        <MountingPortal mountTo="#courseware-admin-view-widget" name="sidebar-views">
+        <Teleport to="#courseware-admin-view-widget" name="sidebar-views">
             <courseware-admin-view-widget />
-        </MountingPortal>
-        <MountingPortal mountTo="#courseware-admin-action-widget" name="sidebar-views">
+        </Teleport>
+        <Teleport to="#courseware-admin-action-widget" name="sidebar-views">
             <courseware-admin-action-widget />
-        </MountingPortal>
+        </Teleport>
 
     </div>
 </template>
diff --git a/resources/vue/components/courseware/CommentsApp.vue b/resources/vue/components/courseware/CommentsApp.vue
index 417423b28a0f04ca040ae1363357b8685a3f45bc..52829ab81865fc7ab9180164c3bf3a07fed7c696 100644
--- a/resources/vue/components/courseware/CommentsApp.vue
+++ b/resources/vue/components/courseware/CommentsApp.vue
@@ -2,15 +2,15 @@
     <div class="cw-comments-overview-wrapper">
         <courseware-block-comments-overview v-if="showBlocks"/>
         <courseware-structural-element-comments-overview v-if="showElements"/>
-        <MountingPortal mountTo="#courseware-comments-overview-widget-filter-type" name="sidebar-views">
+        <Teleport to="#courseware-comments-overview-widget-filter-type" name="sidebar-views">
             <courseware-comments-overview-widget-filter-type />
-        </MountingPortal>
-        <MountingPortal mountTo="#courseware-comments-overview-widget-filter-created" name="sidebar-views">
+        </Teleport>
+        <Teleport to="#courseware-comments-overview-widget-filter-created" name="sidebar-views">
             <courseware-comments-overview-widget-filter-created />
-        </MountingPortal>
-        <MountingPortal mountTo="#courseware-comments-overview-widget-filter-unit" name="sidebar-views">
+        </Teleport>
+        <Teleport to="#courseware-comments-overview-widget-filter-unit" name="sidebar-views">
             <courseware-comments-overview-widget-filter-unit />
-        </MountingPortal>
+        </Teleport>
     </div>
 </template>
 
diff --git a/resources/vue/components/courseware/ContentBookmarkApp.vue b/resources/vue/components/courseware/ContentBookmarkApp.vue
index b0c9fbc51d2998ed6e7a29f2535d117ae9702c65..faf4db6b4065c3d74e7d7544a7a17407383c75eb 100644
--- a/resources/vue/components/courseware/ContentBookmarkApp.vue
+++ b/resources/vue/components/courseware/ContentBookmarkApp.vue
@@ -1,9 +1,9 @@
 <template>
   <div class="cw-content-bookmark">
         <courseware-content-bookmarks />
-        <MountingPortal mountTo="#courseware-content-bookmark-filter-widget" name="sidebar-views">
+        <Teleport to="#courseware-content-bookmark-filter-widget" name="sidebar-views">
             <courseware-content-bookmark-filter-widget />
-        </MountingPortal>
+        </Teleport>
   </div>
 </template>
 
diff --git a/resources/vue/components/courseware/CoursewareAdminTemplates.vue b/resources/vue/components/courseware/CoursewareAdminTemplates.vue
index a0b17e6a60c583bef4f5247dfaba98519cf3942a..b82c8446d5c1ec8dc4f7fde06dddbf7b50f15b79 100644
--- a/resources/vue/components/courseware/CoursewareAdminTemplates.vue
+++ b/resources/vue/components/courseware/CoursewareAdminTemplates.vue
@@ -2,13 +2,13 @@
     <div class="cw-admin-templates">
         <table class="default">
             <caption>
-                <translate>Vorlagen</translate>
+                {{ $gettext('Vorlagen') }}
             </caption>
             <thead>
                 <tr>
-                    <th><translate>Art des Lernmaterials</translate></th>
-                    <th><translate>Name</translate></th>
-                    <th class="actions"><translate>Aktionen</translate></th>
+                    <th>{{ $gettext('Art des Lernmaterials') }}</th>
+                    <th>{{ $gettext('Name') }}</th>
+                    <th class="actions">{{ $gettext('Aktionen') }}</th>
                 </tr>
             </thead>
             <tbody>
@@ -40,27 +40,27 @@
             <template v-slot:dialogContent>
                 <form class="default" @submit.prevent="">
                     <label>
-                        <translate>Name der neuen Vorlage</translate>
+                        {{ $gettext('Name der neuen Vorlage') }}}
                         <input v-model="newTemplateName" type="text" />
                     </label>
                     <label>
-                        <translate>Art des Lernmaterials</translate>
+                        {{ $gettext('Art des Lernmaterials') }}
                         <select v-model="newElementPurpose">
-                            <option value="content"><translate>Inhalt</translate></option>
-                            <option value="template"><translate>Aufgabenvorlage</translate></option>
-                            <option value="oer"><translate>OER-Material</translate></option>
-                            <option value="portfolio"><translate>ePortfolio</translate></option>
-                            <option value="draft"><translate>Entwurf</translate></option>
-                            <option value="other"><translate>Sonstiges</translate></option>
+                            <option value="content">{{ $gettext('Inhalt') }}</option>
+                            <option value="template">{{ $gettext('Aufgabenvorlage') }}</option>
+                            <option value="oer">{{ $gettext('OER-Material') }}</option>
+                            <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
+                            <option value="draft">{{ $gettext('Entwurf') }}</option>
+                            <option value="other">{{ $gettext('Sonstiges') }}</option>
                         </select>
                     </label>
                     <label>
-                        <translate>Vorlage</translate><br>
+                        {{ $gettext('Vorlage') }}<br>
                         <button
                             class="button"
                             @click.prevent="chooseFile"
                         >
-                            <translate>Vorlage-Archiv auswählen</translate>
+                            {{ $gettext('Vorlage-Archiv auswählen') }}
                         </button>
                         <div v-if="importZip" class="cw-import-zip">
                             <header>{{ importZip.name }}</header>
@@ -84,18 +84,18 @@
             <template v-slot:dialogContent>
                 <form class="default" @submit.prevent="">
                     <label>
-                        <translate>Name der neuen Vorlage</translate>
+                        {{ $gettext('Name der neuen Vorlage') }}
                         <input v-model="currentTemplate.attributes.name" type="text" />
                     </label>
                     <label>
-                        <translate>Art des Lernmaterials</translate>
+                        {{ $gettext('Art des Lernmaterials') }}
                         <select v-model="currentTemplate.attributes.purpose">
-                            <option value="content"><translate>Inhalt</translate></option>
-                            <option value="template"><translate>Aufgabenvorlage</translate></option>
-                            <option value="oer"><translate>OER-Material</translate></option>
-                            <option value="portfolio"><translate>ePortfolio</translate></option>
-                            <option value="draft"><translate>Entwurf</translate></option>
-                            <option value="other"><translate>Sonstiges</translate></option>
+                            <option value="content">{{ $gettext('Inhalt') }}</option>
+                            <option value="template">{{ $gettext('Aufgabenvorlage') }}</option>
+                            <option value="oer">{{ $gettext('OER-Material') }}</option>
+                            <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
+                            <option value="draft">{{ $gettext('Entwurf') }}</option>
+                            <option value="other">{{ $gettext('Sonstiges') }}</option>
                         </select>
                     </label>
                 </form>
@@ -235,4 +235,4 @@ export default {
     }
 
 }
-</script>
\ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/CoursewareBlockCommentsOverview.vue b/resources/vue/components/courseware/CoursewareBlockCommentsOverview.vue
index d3fd69c376002214aadb5d25e791ecbcab184bf3..6d108d41cb94a91cd28d17bacf46af1d4319285c 100644
--- a/resources/vue/components/courseware/CoursewareBlockCommentsOverview.vue
+++ b/resources/vue/components/courseware/CoursewareBlockCommentsOverview.vue
@@ -47,21 +47,25 @@
                         <a
                             href="#"
                             @click.prevent="enableCommentsDialog(block)">
-                            {{ $gettextInterpolate(
-                                $ngettext('%{length} Kommentar', '%{length} Kommentare', block.comments.length),
-                                {length: block.comments.length}
+                            {{ $ngettext(
+                                '%{length} Kommentar',
+                                '%{length} Kommentare',
+                                block.comments.length,
+                                { length: block.comments.length }
                             ) }}
                         </a>
                     </td>
                     <td class="responsive-hidden">
-                        <a 
+                        <a
                             v-if="block.element.attributes['can-edit']"
                             href="#"
                             @click.prevent="enableFeedbackDialog(block)"
                             >
-                            {{ $gettextInterpolate(
-                                $ngettext('%{length} Anmerkung', '%{length} Anmerkungen', block.feedbacks.length),
-                                {length: block.feedbacks.length}
+                            {{ $ngettext(
+                                '%{length} Anmerkung',
+                                '%{length} Anmerkungen',
+                                block.feedbacks.length,
+                                { length: block.feedbacks.length }
                             ) }}
                         </a>
                         <template v-else>
diff --git a/resources/vue/components/courseware/CoursewareCommentsOverviewDialog.vue b/resources/vue/components/courseware/CoursewareCommentsOverviewDialog.vue
index d157f5b42b96b68e94cddfb7bdb5675c2b92c2a1..99b8fc6ae78e9fdf12adcaddedd6e4d4aef8bd4f 100644
--- a/resources/vue/components/courseware/CoursewareCommentsOverviewDialog.vue
+++ b/resources/vue/components/courseware/CoursewareCommentsOverviewDialog.vue
@@ -12,7 +12,7 @@
             </h2>
             <courseware-block-comments
                 v-if="isBlock && isComment"
-                :block="item" 
+                :block="item"
             />
             <courseware-structural-element-comments
                 v-if="isStructuralElement && isComment"
@@ -20,7 +20,7 @@
             />
             <courseware-block-feedback
                 v-if="isBlock && isFeedback"
-                :block="item" 
+                :block="item"
             />
             <courseware-structural-element-feedback
                 v-if="isStructuralElement && isFeedback"
@@ -44,6 +44,7 @@ export default {
         CoursewareStructuralElementComments,
         CoursewareStructuralElementFeedback
     },
+    emits: ['close'],
     props: {
         itemType: String,
         item: Object,
diff --git a/resources/vue/components/courseware/CoursewareContentLinks.vue b/resources/vue/components/courseware/CoursewareContentLinks.vue
index 7260fd6401014ca4bfab05e345de8d6c8012a50d..407543940dfd7e4bc8a4e7a8ee62ea5c24a14ce9 100644
--- a/resources/vue/components/courseware/CoursewareContentLinks.vue
+++ b/resources/vue/components/courseware/CoursewareContentLinks.vue
@@ -2,15 +2,15 @@
     <div>
         <table class="default">
             <caption>
-                <translate>Öffentlich verlinkte Seiten</translate>
+                {{ $gettext('Öffentlich verlinkte Seiten') }}
             </caption>
             <thead>
                 <tr>
-                    <th><translate>Seite</translate></th>
-                    <th><translate>Link</translate></th>
-                    <th><translate>Passwort</translate></th>
-                    <th><translate>Ablaufdatum</translate></th>
-                    <th class="actions"><translate>Aktionen</translate></th>
+                    <th>{{ $gettext('Seite') }}</th>
+                    <th>{{ $gettext('Link') }}</th>
+                    <th>{{ $gettext('Passwort') }}</th>
+                    <th>{{ $gettext('Ablaufdatum') }}</th>
+                    <th class="actions">{{ $gettext('Aktionen') }}</th>
                 </tr>
             </thead>
             <tbody>
@@ -56,11 +56,11 @@
             <template v-slot:dialogContent>
                 <form class="default" @submit.prevent="">
                     <label>
-                        <translate>Passwort</translate>
+                        {{ $gettext('Passwort') }}
                         <input type="text" v-model="currentLink.attributes.password" />
                     </label>
                     <label>
-                        <translate>Ablaufdatum</translate>
+                        {{ $gettext('Ablaufdatum') }}
                         <input v-model="currentLink.attributes['expire-date']" type="date" class="size-l" />
                     </label>
                 </form>
@@ -83,7 +83,7 @@ export default {
     data() {
         return {
             menuItems: [
-                { id: 1, label: this.$gettext('Link in Zwischenablage kopieren'), icon: 'clipboard', emit: 'copyLinkToClipboard'}, 
+                { id: 1, label: this.$gettext('Link in Zwischenablage kopieren'), icon: 'clipboard', emit: 'copyLinkToClipboard'},
                 { id: 2, label: this.$gettext('Link bearbeiten'), icon: 'edit', emit: 'editLink' },
                 { id: 3, label: this.$gettext('Link löschen'), icon: 'trash', emit: 'deleteLink' }
             ],
@@ -162,7 +162,7 @@ export default {
             if (!date) {
                 return '-';
             }
-            return new Date(date).toLocaleDateString(navigator.language, { 
+            return new Date(date).toLocaleDateString(navigator.language, {
                 year: 'numeric',
                 month: '2-digit',
                 day: '2-digit'
diff --git a/resources/vue/components/courseware/CoursewareContentPermissions.vue b/resources/vue/components/courseware/CoursewareContentPermissions.vue
index 841fc8ffd8a5228859208afeaf22f1abf6591736..56c52d6d2af18ac4fc108ca4f4833c84848734ed 100644
--- a/resources/vue/components/courseware/CoursewareContentPermissions.vue
+++ b/resources/vue/components/courseware/CoursewareContentPermissions.vue
@@ -42,7 +42,7 @@
                     <td class="perm">
                         <input
                             class="right"
-                            :title="$gettextInterpolate($gettext('Leserechte für %{ userName }'), { userName: user_perm.username }, true)"
+                            :title="$gettext('Leserechte für %{ userName }', { userName: user_perm.username }, true)"
                             type="radio"
                             :name="`${user_perm.id}_right`"
                             value="read"
@@ -53,7 +53,7 @@
                     <td class="perm">
                         <input
                             class="right"
-                            :title="$gettextInterpolate($gettext('Lese- und Schreibrechte für %{ userName }'), { userName: user_perm.username }, true)"
+                            :title="$gettext('Lese- und Schreibrechte für %{ userName }', { userName: user_perm.username }, true)"
                             type="radio"
                             :name="`${user_perm.id}_right`"
                             value="write"
@@ -75,7 +75,7 @@
                     <td class="actions">
                         <button
                             class="cw-permission-delete"
-                            :title="$gettextInterpolate($gettext('Entfernen der Rechte von %{ userName }'), { userName: user_perm.username }, true)"
+                            :title="$gettext('Entfernen der Rechte von %{ userName }', { userName: user_perm.username }, true)"
                             @click.prevent="confirmDeleteUserPerm(index)"
                         >
                         </button>
@@ -141,6 +141,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-content-permissions',
+    emits: ['updateContentApproval'],
     props: {
         element: Object,
     },
@@ -188,13 +189,13 @@ export default {
 
         getExpiryTitle(userName, date) {
             if (date) {
-                return this.$gettextInterpolate(
-                    this.$gettext('Die Berechtigungen für %{ userName } laufen am folgendem Datum ab: %{ dateStr }'),
+                return this.$this.$gettext(
+                    'Die Berechtigungen für %{ userName } laufen am folgendem Datum ab: %{ dateStr }',
                     { userName: userName, dateStr: new Date(date).toLocaleDateString() }
                 );
             } else {
-                return this.$gettextInterpolate(
-                    this.$gettext('Das Ablaufdatum der Berechtigungen für %{ userName }'),
+                return this.$this.$gettext(
+                    'Das Ablaufdatum der Berechtigungen für %{ userName }',
                     { userName: userName }
                 );
             }
diff --git a/resources/vue/components/courseware/CoursewareContentShared.vue b/resources/vue/components/courseware/CoursewareContentShared.vue
index 0bdc4b5cfd2228596a3cae3af521f18c3a3a1f58..993a08bc3a945f15b12953810f95f3c185cf0ae8 100644
--- a/resources/vue/components/courseware/CoursewareContentShared.vue
+++ b/resources/vue/components/courseware/CoursewareContentShared.vue
@@ -73,7 +73,7 @@
         <studip-dialog
             v-if="showClearReleases"
             :title="$gettext('Löschen der Freigabe')"
-            :question="$gettextInterpolate($gettext('Möchten Sie die Freigabe für %{ pageTitle} wirklich löschen?'), {pageTitle: this.selectedElement.attributes.title})"
+            :question="$gettext('Möchten Sie die Freigabe für %{ pageTitle} wirklich löschen?', { pageTitle: this.selectedElement.attributes.title })"
             height="220"
             @confirm="clearReleases"
             @close="closeClearReleases"
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementCommentsOverview.vue b/resources/vue/components/courseware/CoursewareStructuralElementCommentsOverview.vue
index f9b6f1a4f89b8b379a97857305859a058fd2e4c3..e0b264c211baa4d9bc26f03d475107905f262321 100644
--- a/resources/vue/components/courseware/CoursewareStructuralElementCommentsOverview.vue
+++ b/resources/vue/components/courseware/CoursewareStructuralElementCommentsOverview.vue
@@ -47,9 +47,11 @@
                             :title="$gettext('Kommentare anzeigen')"
                             @click.prevent="enableCommentsDialog(element)"
                         >
-                            {{ $gettextInterpolate(
-                                $ngettext('%{length} Kommentar', '%{length} Kommentare', element.comments.length),
-                                {length: element.comments.length}
+                            {{ $ngettext(
+                                '%{length} Kommentar',
+                                '%{length} Kommentare',
+                                element.comments.length,
+                                { length: element.comments.length }
                             ) }}
                         </a>
                     </td>
@@ -60,8 +62,10 @@
                             :title="$gettext('Anmerkungen anzeigen')"
                             @click.prevent="enableFeedbackDialog(element)"
                         >
-                            {{ $gettextInterpolate(
-                                $ngettext('%{length} Anmerkung', '%{length} Anmerkungen', element.feedbacks.length),
+                            {{ $ngettext(
+                                '%{length} Anmerkung',
+                                '%{length} Anmerkungen',
+                                element.feedbacks.length,
                                 {length: element.feedbacks.length}
                             ) }}
                         </a>
diff --git a/resources/vue/components/courseware/IndexApp.vue b/resources/vue/components/courseware/IndexApp.vue
index 2b1ad72a5e6db79a62d6117e014bafd03728138e..62fbf6759dda80bd62c4653e8c202f53c13a6de4 100644
--- a/resources/vue/components/courseware/IndexApp.vue
+++ b/resources/vue/components/courseware/IndexApp.vue
@@ -7,11 +7,11 @@
                 :canVisit="canVisit"
                 :structural-element="selected"
                 :ordered-structural-elements="orderedStructuralElements"
-                @select="selectStructuralElement"
+                @select-element="selectStructuralElement"
             ></courseware-structural-element>
-            <MountingPortal mountTo="#courseware-search-widget" name="sidebar-search">
+            <Teleport to="#courseware-search-widget" name="sidebar-search">
                 <courseware-search-widget v-if="selected !== null"></courseware-search-widget>
-            </MountingPortal>
+            </Teleport>
         </div>
         <studip-progress-indicator
             v-if="structureLoadingState === 'loading'"
@@ -128,12 +128,15 @@ export default {
             this.selectStructuralElement(selectedId);
             window.scrollTo({ top: 0 });
         },
-        async structuralElements(newElements, oldElements) {
-            // compute order of structural elements once more
-            await this.buildStructure();
+        structuralElements: {
+            async handler(newElements, oldElements) {
+                // compute order of structural elements once more
+                await this.buildStructure();
 
-            // throw away stale cache
-            this.invalidateStructureCache();
+                // throw away stale cache
+                this.invalidateStructureCache();
+            },
+            deep: true
         },
     },
 };
diff --git a/resources/vue/components/courseware/PublicApp.vue b/resources/vue/components/courseware/PublicApp.vue
index 0a082f0dc196e78a90429dba8460b5e76adbae12..ffaf83457d50ef8664ff6720c29cf3780e6ace91 100644
--- a/resources/vue/components/courseware/PublicApp.vue
+++ b/resources/vue/components/courseware/PublicApp.vue
@@ -25,11 +25,11 @@
         />
         <form v-if="!isAuthenticated" class="default" @submit.prevent="">
             <label>
-                <translate>Passwort</translate>
+                {{ $gettext('Passwort') }}
                 <input type="password" v-model="password">
             </label>
             <button class="button" @click="submitPassword">
-                <translate>Absenden</translate>
+                {{ $gettext('Absenden') }}
             </button>
         </form>
      </div>
diff --git a/resources/vue/components/courseware/ShelfApp.vue b/resources/vue/components/courseware/ShelfApp.vue
index 173be71bce4430e7a93f342d314a92d8fd3a9d5e..98e6f2a7c7d69ebdb301ea216a33d7c8a568bebe 100644
--- a/resources/vue/components/courseware/ShelfApp.vue
+++ b/resources/vue/components/courseware/ShelfApp.vue
@@ -9,9 +9,9 @@
         <courseware-shelf-dialog-copy v-if="showUnitCopyDialog" />
         <courseware-shelf-dialog-import v-if="showUnitImportDialog" />
         <courseware-shelf-dialog-topics v-if="showUnitTopicsDialog" />
-        <MountingPortal v-if="userIsTeacher || !inCourseContext" mountTo="#courseware-action-widget" name="sidebar-actions">
+        <Teleport v-if="userIsTeacher || !inCourseContext" to="#courseware-action-widget" name="sidebar-actions">
             <courseware-shelf-action-widget></courseware-shelf-action-widget>
-        </MountingPortal>
+        </Teleport>
         <courseware-companion-overlay />
     </div>
 </template>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
index c0d5d0882efcdafa4279337654ce25664cda2f7c..564a8542cbeccdfa2f91dc1fb1fce259cbda6ee0 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
@@ -10,7 +10,7 @@
             @closeEdit="initCurrentData"
         >
             <template #content>
-                <TwentyTwenty v-if="!isEmpty" :before="currentBeforeUrl" :after="currentAfterUrl" />
+                <ImageCompare v-if="!isEmpty" :image1="currentBeforeUrl" :image2="currentAfterUrl" />
                 <courseware-companion-box
                     v-if="isEmpty && canEdit"
                     :msgCompanion="$gettext('Bitte wählen Sie ein Vorher- und ein Nachher-Bild aus.')"
@@ -19,7 +19,7 @@
             </template>
             <template v-if="canEdit" #edit>
                 <courseware-tabs>
-                    <courseware-tab :index="0" :name="$gettext('Vorher')" :selected="true">
+                    <courseware-tab :name="$gettext('Vorher')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Quelle') }}
@@ -34,12 +34,19 @@
                             </label>
                             <label v-if="currentBeforeSource === 'studip'">
                                 {{ $gettext('Bilddatei') }}
-                                <studip-file-chooser v-model="currentBeforeFileId" selectable="file" :courseId="context.id" :userId="userId" :isImage="true" :excludedCourseFolderTypes="excludedCourseFolderTypes"/>
+                                <studip-file-chooser
+                                    v-model="currentBeforeFileId"
+                                    selectable="file"
+                                    :courseId="context.id"
+                                    :userId="userId"
+                                    :isImage="true"
+                                    :excludedCourseFolderTypes="excludedCourseFolderTypes"
+                                />
                             </label>
                         </form>
                     </courseware-tab>
-                    <courseware-tab :index="1" :name="$gettext('Nachher')">
-                        <form class="default" @submit.prevent="">
+                    <courseware-tab :name="$gettext('Nachher')">
+                        <form class="default" @submit.prevent="onSubmit">
                             <label>
                                 {{ $gettext('Quelle') }}
                                 <select v-model="currentAfterSource">
@@ -53,7 +60,13 @@
                             </label>
                             <label v-if="currentAfterSource === 'studip'">
                                 {{ $gettext('Bilddatei') }}
-                                <studip-file-chooser v-model="currentAfterFileId" selectable="file" :courseId="context.id" :userId="userId" :isImage="true"/>
+                                <studip-file-chooser
+                                    v-model="currentAfterFileId"
+                                    selectable="file"
+                                    :courseId="context.id"
+                                    :userId="userId"
+                                    :isImage="true"
+                                />
                             </label>
                         </form>
                     </courseware-tab>
@@ -67,15 +80,14 @@
 <script>
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
-import TwentyTwenty from 'vue-twentytwenty';
-import 'vue-twentytwenty/dist/vue-twentytwenty.css';
 import { mapActions, mapGetters } from 'vuex';
+import ImageCompare from './ImageCompare.vue';
 
 export default {
     name: 'courseware-before-after-block',
     mixins: [blockMixin],
     components: Object.assign(BlockComponents, {
-        TwentyTwenty,
+        ImageCompare,
     }),
     props: {
         block: Object,
@@ -154,12 +166,12 @@ export default {
                 }
 
                 this.currentBeforeFile = this.beforeFile;
-                this.currentAfterFile  = this.afterFile;
+                this.currentAfterFile = this.afterFile;
             });
 
             this.loadImages();
         }
-        
+
         this.initCurrentData();
     },
     methods: {
@@ -260,25 +272,7 @@ export default {
             if (newId) {
                 this.currentAfterFile = this.fileRefById({ id: newId });
             }
-        }
-    }
+        },
+    },
 };
 </script>
-<style scoped lang="scss">
-.cw-block-before-after {
-    .twentytwenty-container {
-        width: 100% !important;
-        z-index: 19;
-        .twentytwenty-handle {
-            z-index: 18;
-        }
-        .twentytwenty-overlay {
-            z-index: 17;
-        }
-        img {
-            width: 100%;
-            z-index: 16;
-        }
-    }
-}
-</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
index eea76fefd257faf5c71578333d5c5ee3024868da..468809074f87b74b2c323ec160d902ed2a27a1ab 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
@@ -26,6 +26,17 @@ export default {
     components: {
         StudipActionMenu,
     },
+    emits: [
+        'activateComments',
+        'copyToClipboard',
+        'deactivateComments',
+        'deleteBlock',
+        'editBlock',
+        'removeLock',
+        'showExportOptions',
+        'showFeedback',
+        'showInfo',
+    ],
     props: {
         canEdit: Boolean,
         deleteOnly: {
@@ -98,9 +109,9 @@ export default {
                     if (!this.blocked || this.blockedByThisUser) {
                         menuItems.push({
                             id: 9,
-                            label: this.$gettext('Block löschen'), 
+                            label: this.$gettext('Block löschen'),
                             icon: 'trash',
-                            emit: 'deleteBlock' 
+                            emit: 'deleteBlock'
                         });
                     }
                     menuItems.push({
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue b/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue
index 9b1cc44623c30ead5d09986d92ab2c063db85b76..dc03f2ad00e58cc3aa30f72f1d2354c01b2e490e 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue
@@ -12,7 +12,7 @@
             </div>
             <div class="cw-block-comment-create">
                 <textarea v-model="createComment" :placeholder="placeHolder" spellcheck="true"></textarea>
-                <button class="button" @click="postComment"><translate>Senden</translate></button>
+                <button class="button" @click="postComment">{{ $gettext('Senden') }}</button>
             </div>
         </div>
     </section>
@@ -24,6 +24,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-block-comments',
+    emits: ['hasComments'],
     components: {
         CoursewareTalkBubble,
     },
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue b/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue
index e29e15a14079f391dfb3d54883e7ec35239489d7..59cf406e246f81423ec0380024e0ec31a01c0e0c 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue
@@ -88,13 +88,12 @@ export default {
             return ((this.canEdit || this.userIsTeacher) && this.hasFeedback) || this.displayFeedback;
         },
         callToActionTitleFeedback() {
-            return this.$gettextInterpolate(
-                this.$ngettext(
-                    '%{length} Anmerkung (Nur für Nutzende mit Schreibrechten sichtbar)',
-                    '%{length} Anmerkungen (Nur für Nutzende mit Schreibrechten sichtbar)',
-                    this.feedbackCounter
-                ),
-            { length: this.feedbackCounter });
+            return this.$ngettext(
+                '%{length} Anmerkung (Nur für Nutzende mit Schreibrechten sichtbar)',
+                '%{length} Anmerkungen (Nur für Nutzende mit Schreibrechten sichtbar)',
+                this.feedbackCounter,
+                { length: this.feedbackCounter }
+            );
         },
         comments() {
             const { id, type } = this.block;
@@ -105,13 +104,12 @@ export default {
             return this.comments?.length ?? 0;
         },
         callToActionTitleComments() {
-            return this.$gettextInterpolate(
-                this.$ngettext(
-                    '%{length} Kommentar',
-                    '%{length} Kommentare',
-                    this.commentsCounter
-                ),
-            { length: this.commentsCounter });
+            return this.$ngettext(
+                '%{length} Kommentar',
+                '%{length} Kommentare',
+                this.commentsCounter,
+                { length: this.commentsCounter }
+            );
         },
     },
 };
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
index 82d1abcbf1358aa73471e7f721543b7781796316..1c8b08c1a0fe6598dc5244cab7c7f039266478fc 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
@@ -18,6 +18,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-block-edit',
+    emits: ['close', 'store'],
     props: {
         block: Object,
         preview: Boolean
@@ -31,7 +32,7 @@ export default {
     beforeMount() {
         this.originalBlock = this.block;
     },
-    beforeDestroy() {
+    beforeUnmount() {
         if (this.exitHandler) {
             this.$emit('store');
         }
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockExportOptions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockExportOptions.vue
index 71f6adca9eb78a836b76b6ef5ee7a5d8eb5462f3..aeb66a5c6c284dd440e643431a6eeb7deb2cb9aa 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockExportOptions.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockExportOptions.vue
@@ -1,8 +1,8 @@
 <template>
     <section class="cw-block-export-options">
-        <header><translate>Export Optionen</translate></header>
+        <header>{{ $gettext('Export Optionen') }}</header>
         <div class="cw-block-features-content">
-            <button class="button" @click="$emit('close')"><translate>Schließen</translate></button>
+            <button class="button" @click="$emit('close')">{{ $gettext('Schließen') }}</button>
         </div>
     </section>
 </template>
@@ -10,7 +10,7 @@
 <script>
 export default {
     name: 'courseware-block-export-options',
-    components: {},
+    emits: ['close'],
     props: {
         block: Object,
     },
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockInfo.vue b/resources/vue/components/courseware/blocks/CoursewareBlockInfo.vue
index 705328ecac94dda902caaf06b839acc787ea67eb..e955899629e6788daf1268db0c4104deac2239da 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockInfo.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockInfo.vue
@@ -3,26 +3,28 @@
         <header>{{ $gettext('Informationen') }}</header>
         <div class="cw-block-features-content cw-block-info-content">
             <table class="cw-block-info-table">
-                <tr>
-                    <td>{{ $gettext('Blockbeschreibung') }}</td>
-                    <td><slot name="info" /></td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Block wurde erstellt von') }}</td>
-                    <td>{{ owner }}</td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Block wurde erstellt am') }}:</td>
-                    <td><iso-date :date="block.attributes.mkdate" /></td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Zuletzt bearbeitet von') }}:</td>
-                    <td>{{ editor }}</td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Zuletzt bearbeitet am') }}:</td>
-                    <td><iso-date :date="block.attributes.chdate" /></td>
-                </tr>
+                <tbody>
+                    <tr>
+                        <td>{{ $gettext('Blockbeschreibung') }}</td>
+                        <td><slot name="info" /></td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Block wurde erstellt von') }}</td>
+                        <td>{{ owner }}</td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Block wurde erstellt am') }}:</td>
+                        <td><iso-date :date="block.attributes.mkdate" /></td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Zuletzt bearbeitet von') }}:</td>
+                        <td>{{ editor }}</td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Zuletzt bearbeitet am') }}:</td>
+                        <td><iso-date :date="block.attributes.chdate" /></td>
+                    </tr>
+                </tbody>
             </table>
             <button class="button" @click="$emit('close')">{{ $gettext('Schließen') }}</button>
         </div>
@@ -36,6 +38,7 @@ import { mapActions, mapGetters } from 'vuex';
 export default {
     name: 'courseware-block-info',
     components: { IsoDate },
+    emits: ['close'],
     props: {
         block: Object,
     },
diff --git a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
index 21a52006940df26b983009c3a72c11b4431310cd..49c9cfc1f0f5ae0d3b6b7c3a7a73f3ba6460f7ce 100644
--- a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
@@ -106,9 +106,7 @@
             <template v-if="canEdit" #edit>
                 <courseware-tabs>
                     <courseware-tab
-                        :index="0"
                         :name="$gettext('Grunddaten')"
-                        :selected="true"
                     >
                         <form class="default" @submit.prevent="">
                             <label>
@@ -136,7 +134,6 @@
                         </form>
                     </courseware-tab>
                     <courseware-tab
-                        :index="1"
                         :name="$gettext('Einstellungen')"
                     >
                         <form class="default" @submit.prevent="">
diff --git a/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue b/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue
index fa85a4119e3d3bcae7363bef4f933208574bf0c0..33ef473c97c0a055470a391113cb1e298a08c63a 100644
--- a/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue
@@ -27,7 +27,7 @@
                             :clearable="false"
                             @option:selected="buildChart"
                         >
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
                             </template>
                             <template #selected-option="{name}">
@@ -68,7 +68,7 @@
                                     v-model="item.color"
                                     @option:selected="buildChart"
                                 >
-                                    <template #open-indicator="selectAttributes">
+                                    <template #open-indicator="{ selectAttributes }">
                                         <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
                                     </template>
                                     <template #no-options>
@@ -95,7 +95,7 @@
                             </button>
                         </form>
                     </courseware-tab>
-                </courseware-tabs>                
+                </courseware-tabs>
             </template>
             <template #info>
                 <p>{{ $gettext('Informationen zum Diagramm-Block') }}</p>
diff --git a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
index 6bac6ff6e66b470f20b62ed03a6df282135b3a4c..b788a5217c5ae51d98ec81e817261f9d4e217b98 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
@@ -7,7 +7,7 @@
                     <span>{{ blockTitle }}</span>
                     <studip-icon v-if="blockedByAnotherUser" shape="lock-locked" />
                     <span v-if="blockedByAnotherUser" class="cw-default-block-blocker-warning">
-                        {{ $gettextInterpolate($gettext('Wird im Moment von %{ userName } bearbeitet'), { userName: this.blockingUserName }) }}
+                        {{ $gettext('Wird im Moment von %{ userName } bearbeitet', { userName: this.blockingUserName }) }}
                     </span>
                     <studip-icon v-if="!block.attributes.visible" shape="visibility-invisible" />
                     <span v-if="!block.attributes.visible" class="cw-default-block-invisible-info">
@@ -101,6 +101,7 @@ import { mapActions, mapGetters } from 'vuex';
 export default {
     name: 'courseware-default-block',
     mixins: [blockMixin],
+    emits: ['closeEdit', 'showEdit', 'storeEdit'],
     components: {
         CoursewareBlockActions,
         CoursewareBlockDiscussion,
@@ -109,7 +110,7 @@ export default {
         CoursewareBlockInfo,
         StudipDialog,
         StudipIcon,
-        
+
     },
     props: {
         block: Object,
@@ -275,9 +276,9 @@ export default {
 
             if (this.blockedByAnotherUser) {
                 this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
+                    info: this.$gettext(
+                        'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.',
+                        { blockingUserName: this.blockingUserName }
                     )
                 });
                 this.displayFeature(false);
@@ -307,9 +308,9 @@ export default {
                     this.showDeleteDialog = true;
                 } else {
                     this.companionInfo({
-                        info: this.$gettextInterpolate(
-                            this.$gettext('Löschen nicht möglich, da %{blockingUserName} den Block bearbeitet.'),
-                            {blockingUserName: this.blockingUserName}
+                        info: this.$gettext(
+                            'Löschen nicht möglich, da %{blockingUserName} den Block bearbeitet.',
+                            { blockingUserName: this.blockingUserName }
                         )
                     });
                 }
@@ -329,9 +330,9 @@ export default {
             await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
             if (this.blockedByAnotherUser) {
                 this.companionInfo({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
+                    info: this.$gettext(
+                        'Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.',
+                        { blockingUserName: this.blockingUserName }
                     )
                 });
                 return false;
@@ -419,4 +420,4 @@ export default {
         }
     }
 };
-</script>
\ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue
index ff01d78a2638252e0206f149c6ba250880f4c48c..0207acab4d0dc54caddb84df9664996bf2da1fa8 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue
@@ -43,7 +43,7 @@
             </template>
             <template v-if="canEdit" #edit>
                 <courseware-tabs>
-                    <courseware-tab :index="0" :name="$gettext('Datei')" :selected="true">
+                    <courseware-tab :name="$gettext('Datei')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Überschrift') }}
@@ -55,7 +55,7 @@
                             </label>
                         </form>
                     </courseware-tab>
-                    <courseware-tab :index="1" :name="$gettext('Infobox')">
+                    <courseware-tab :name="$gettext('Infobox')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Infobox vor Download') }}
@@ -67,7 +67,7 @@
                             </label>
                         </form>
                     </courseware-tab>
-                    <courseware-tab :index="2" :name="$gettext('Fortschritt')">
+                    <courseware-tab :name="$gettext('Fortschritt')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Fortschritt erst beim Herunterladen') }}
diff --git a/resources/vue/components/courseware/blocks/CoursewareEmbedBlock.vue b/resources/vue/components/courseware/blocks/CoursewareEmbedBlock.vue
index 9ace7bf4774857c575d5ea5f6abbb71e873739a9..de1f833830ed399acc51d23a2fd41af409478fe4 100644
--- a/resources/vue/components/courseware/blocks/CoursewareEmbedBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareEmbedBlock.vue
@@ -165,7 +165,7 @@ export default {
             this.recalculateContentHeight(data);
         });
     },
-    destroyed() {
+    unmounted() {
         window.removeEventListener('resize', this.calcContentHeight);
     },
     methods: {
@@ -185,7 +185,7 @@ export default {
             }
         },
         recalculateContentHeight(data) {
-            if (this.$parent._uid === data.uid) {
+            if (this.$parent._.uid === data.uid) {
                 if (this.oembedData) {
                     this.calcContentHeight();
                 }
diff --git a/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue b/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue
index 9d64859f4830dfc4d79ee405cc831e4107fee788..239556c2fd2805f7893ce9d5997811352ed8f1b2 100644
--- a/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue
@@ -104,7 +104,7 @@
                                     }}
                                 </div>
                                 <fieldset class="select_terms_of_use">
-                                    <template v-for="term in termsOfUse">
+                                    <template v-for="term in termsOfUse" :key="term.id">
                                         <input
                                             type="radio"
                                             name="content_terms_of_use_id"
@@ -113,9 +113,8 @@
                                             :id="'content_terms_of_use-' + term.id"
                                             :checked="selectedTerm === term.id"
                                             :aria-description="term.description"
-                                            :key="term.id + '_input'"
                                         />
-                                        <label @click="selectedTerm = term.id" :key="term.id + 'label'">
+                                        <label @click="selectedTerm = term.id">
                                             <div class="icon">
                                                 <studip-icon :shape="term.attributes.icon" :size="32" />
                                             </div>
@@ -125,7 +124,7 @@
                                             <studip-icon shape="arr_1down" :size="24" class="arrow" />
                                             <studip-icon shape="check-circle" :size="24" class="check" />
                                         </label>
-                                        <div class="terms_of_use_description" :key="term.id + '_description'">
+                                        <div class="terms_of_use_description">
                                             <div class="description">
                                                 {{ term.attributes.description }}
                                             </div>
diff --git a/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue b/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue
index 1d078329bf3a194e23a6f46b58eb9413ac467f4c..9a32689998b0813629e60daae75dbd531561460a 100644
--- a/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue
@@ -75,7 +75,7 @@
             </template>
             <template v-if="canEdit" #edit>
                 <courseware-tabs>
-                    <courseware-tab :index="0" :name="$gettext('Einstellungen')" :selected="true">
+                    <courseware-tab :name="$gettext('Einstellungen')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Layout') }}
@@ -104,7 +104,7 @@
                             </label>
                         </form>
                     </courseware-tab>
-                    <courseware-tab :index="1" :name="$gettext('Beschreibung')">
+                    <courseware-tab :name="$gettext('Beschreibung')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Dateinamen anzeigen') }}
@@ -136,7 +136,7 @@
                             </label>
                         </form>
                     </courseware-tab>
-                    <courseware-tab v-if="currentLayout === 'carousel'" :index="2" :name="$gettext('Autoplay')">
+                    <courseware-tab v-if="currentLayout === 'carousel'" :name="$gettext('Autoplay')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Autoplay') }}
diff --git a/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue b/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue
index c9c2ce0ac7fa0f66b94040efaf471944d5b657ee..3d992f68ac775a8c26d49cf353e5afeedb8fdb03 100644
--- a/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue
@@ -49,9 +49,7 @@
             <template v-if="canEdit" #edit>
                 <courseware-tabs>
                     <courseware-tab
-                        :index="0"
                         :name="$gettext('Layout')"
-                        :selected="true"
                     >
                         <form class="default" @submit.prevent="">
                             <label>
@@ -77,7 +75,6 @@
                         </form>
                     </courseware-tab>
                     <courseware-tab
-                        :index="1"
                         :name="$gettext('Inhalt')"
                     >
                         <form class="default" @submit.prevent="">
@@ -102,7 +99,7 @@
                                     :clearable="false"
                                     v-model="currentTextColor"
                                 >
-                                    <template #open-indicator="selectAttributes">
+                                    <template #open-indicator="{ selectAttributes }">
                                         <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
                                     </template>
                                     <template #no-options>
@@ -125,7 +122,7 @@
                                     :clearable="false"
                                     v-model="currentTextBackgroundColor"
                                 >
-                                    <template #open-indicator="selectAttributes">
+                                    <template #open-indicator="{ selectAttributes }">
                                         <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
                                     </template>
                                     <template #no-options>
@@ -143,7 +140,7 @@
                                 <label>
                                     {{ $gettext('Icon') }}
                                     <studip-select :clearable="false" :options="icons" v-model="currentIcon">
-                                        <template #open-indicator="selectAttributes">
+                                        <template #open-indicator="{ selectAttributes }">
                                             <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
                                         </template>
                                         <template #no-options>
@@ -166,7 +163,7 @@
                                         :clearable="false"
                                         v-model="currentIconColor"
                                     >
-                                        <template #open-indicator="selectAttributes">
+                                        <template #open-indicator="{ selectAttributes }">
                                             <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
                                         </template>
                                         <template #no-options>
@@ -184,7 +181,6 @@
                         </form>
                     </courseware-tab>
                     <courseware-tab
-                        :index="2"
                         :name="$gettext('Hintergrund')"
                     >
                         <form class="default" @submit.prevent="">
@@ -206,7 +202,7 @@
                                     v-model="currentBackgroundColor"
                                     :clearable="false"
                                 >
-                                    <template #open-indicator="selectAttributes">
+                                    <template #open-indicator="{ selectAttributes }">
                                         <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
                                     </template>
                                     <template #no-options>
@@ -634,4 +630,4 @@ export default {
 </script>
 <style scoped lang="scss">
     @import "../../../../assets/stylesheets/scss/courseware/blocks/headline.scss";
-</style>
\ No newline at end of file
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
index 4ff7e45b8e02d8ef1b69cac0f8f37ce51266e3d7..49ea664e4b4a1fcdfbb8396f2035667fe92a974d 100644
--- a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
@@ -31,7 +31,7 @@
             </template>
             <template v-if="canEdit" #edit>
                 <courseware-tabs>
-                    <courseware-tab :index="0" :name="$gettext('Grunddaten')" :selected="true">
+                    <courseware-tab :name="$gettext('Grunddaten')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Titel') }}
@@ -47,7 +47,7 @@
                             </label>
                         </form>
                     </courseware-tab>
-                    <courseware-tab :index="1" :name="$gettext('Nutzerspezifische ID')">
+                    <courseware-tab :name="$gettext('Nutzerspezifische ID')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Nutzerspezifische ID übergeben') }}
@@ -67,7 +67,7 @@
                             </label>
                         </form>
                     </courseware-tab>
-                    <courseware-tab :index="2" :name="$gettext('Creative Commons')">
+                    <courseware-tab :name="$gettext('Creative Commons')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Creative Commons Lizenz') }}
diff --git a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
index 4f52d4b57d739f39e64dbc7586e5a26ffdd499b0..4fd312870a07020df4822df2b882c0b78e66dc50 100644
--- a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
@@ -111,7 +111,7 @@
                                     v-model="shape.data.color"
                                     @input="drawScreen"
                                 >
-                                    <template #open-indicator="selectAttributes">
+                                    <template #open-indicator="{ selectAttributes }">
                                         <span v-bind="selectAttributes"
                                             ><studip-icon shape="arr_1down" :size="10"
                                         /></span>
@@ -139,7 +139,7 @@
                                     v-model="shape.data.textcolor"
                                     @input="drawScreen"
                                 >
-                                    <template #open-indicator="selectAttributes">
+                                    <template #open-indicator="{ selectAttributes }">
                                         <span v-bind="selectAttributes"
                                             ><studip-icon shape="arr_1down" :size="10"
                                         /></span>
@@ -263,7 +263,7 @@
 <script>
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
-import VueResizeable from 'vrp-vue-resizable';
+import VueResizeable from 'vue-resizable';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
index 0931182927a89bbec379dc7d0bbda72689954379..55bf2a9e71156f3e6c3ef3229562e87916c30a9d 100644
--- a/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
@@ -39,7 +39,7 @@
                             :reduce="(option) => option.icon"
                             v-model="currentColor"
                         >
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
                             </template>
                             <template #no-options>
@@ -58,7 +58,7 @@
                     <label class="col-2">
                         {{ $gettext('Icon') }}
                         <studip-select :options="icons" :clearable="false" v-model="currentIcon">
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
                             </template>
                             <template #no-options>
@@ -115,7 +115,7 @@ export default {
         },
         colors() {
              let colors = this.mixinColors.filter(
-                (color) => (color.icon && color.class !== 'white' && color.class !== 'studip-lightblue') 
+                (color) => (color.icon && color.class !== 'white' && color.class !== 'studip-lightblue')
                 || color.class === 'royal-purple' || color.class === 'apple-green' || color.class === 'pumpkin' || color.class === 'verdigris' || color.class === 'mulberry'
             );
 
diff --git a/resources/vue/components/courseware/blocks/CoursewareLtiBlock.vue b/resources/vue/components/courseware/blocks/CoursewareLtiBlock.vue
index 626a1a020313db5bc2072396f17a8760e0050c16..91965015964c4d459f1ec8cb6a787460c0efa477 100644
--- a/resources/vue/components/courseware/blocks/CoursewareLtiBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareLtiBlock.vue
@@ -28,7 +28,7 @@
             </template>
             <template v-if="canEdit" #edit>
                 <courseware-tabs>
-                    <courseware-tab :index="0" :name="$gettext('Grunddaten')" :selected="true">
+                    <courseware-tab :name="$gettext('Grunddaten')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Titel') }}
@@ -110,7 +110,7 @@
                             </div>
                         </form>
                     </courseware-tab>
-                    <courseware-tab :index="1" :name="$gettext('Zusätzliche Einstellungen')">
+                    <courseware-tab :name="$gettext('Zusätzliche Einstellungen')">
                         <form class="default" @submit.prevent="">
                             <label>
                                 {{ $gettext('Höhe') }}
diff --git a/resources/vue/components/courseware/blocks/CoursewarePDFTableOfContent.vue b/resources/vue/components/courseware/blocks/CoursewarePDFTableOfContent.vue
index 989e2edabfa90e3a17c53e24681c8c8033ba399a..2a258a2394c75bbd201c1359af92f3805f80842d 100644
--- a/resources/vue/components/courseware/blocks/CoursewarePDFTableOfContent.vue
+++ b/resources/vue/components/courseware/blocks/CoursewarePDFTableOfContent.vue
@@ -15,6 +15,7 @@
 <script>
 export default {
     name: 'courseware-pdf-toc-item',
+    emits: ['tocPageNav'],
     props: {
         item: {
             type: Object,
diff --git a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue
index 2f09edeadb2fc0c8acfef0c787bcf9bfc21e5029..4330f13acd489bdb978e7ddacb2ff95273c35bcd 100644
--- a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue
@@ -37,15 +37,12 @@
                                         {{ child.attributes.payload.description }}
                                     </template>
                                     <template #footer>
-                                        {{
-                                            $gettextInterpolate(
-                                                $ngettext(
-                                                    '%{length} Seite',
-                                                    '%{length} Seiten',
-                                                    countChildChildren(child)
-                                                ),
-                                                { length: countChildChildren(child) })
-                                        }}
+                                        {{ $ngettext(
+                                            '%{length} Seite',
+                                            '%{length} Seiten',
+                                            countChildChildren(child),
+                                            { length: countChildChildren(child) }
+                                        ) }}
                                     </template>
                                 </courseware-tile>
                             </router-link>
diff --git a/resources/vue/components/courseware/blocks/CoursewareTimelineBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTimelineBlock.vue
index 4e93e70afbe8956552f9f33d876e3d2ff8407dda..1f305f8f0668ce1994ca574b87708058ffb15b6c 100644
--- a/resources/vue/components/courseware/blocks/CoursewareTimelineBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareTimelineBlock.vue
@@ -74,7 +74,7 @@
                                 <input type="text" v-model="item.title" />
                             </label>
                             <label class="col-4">
-                            
+
                                 {{ $gettext('Beschreibung') }}
                                 <textarea v-model="item.description" />
                             </label>
@@ -88,7 +88,7 @@
                                     :reduce="option => option.class"
                                     v-model="item.color"
                                 >
-                                    <template #open-indicator="selectAttributes">
+                                    <template #open-indicator="{ selectAttributes }">
                                         <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                                     </template>
                                     <template #no-options>
@@ -105,7 +105,7 @@
                             <label class="col-2">
                                 {{ $gettext('Icon') }}
                                 <studip-select :options="icons" :clearable="false" v-model="item.icon">
-                                    <template #open-indicator="selectAttributes">
+                                    <template #open-indicator="{ selectAttributes }">
                                         <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10"/></span>
                                     </template>
                                     <template #no-options>
@@ -304,4 +304,4 @@ export default {
 </script>
 <style scoped lang="scss">
 @import '../../../../assets/stylesheets/scss/courseware/blocks/timeline.scss';
-</style>
\ No newline at end of file
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue b/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue
index 7febfbc0a6160c3321c73c0eb79b76aee2496846..147c07f00d45642624cd5cefdbf053e990902ca9 100644
--- a/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue
@@ -23,9 +23,7 @@
             <template v-if="canEdit" #edit>
                 <courseware-tabs>
                     <courseware-tab
-                        :index="0"
                         :name="$gettext('Grunddaten')"
-                        :selected="true"
                     >
                         <form class="default" @submit.prevent="">
                             <label>
@@ -58,7 +56,6 @@
                         </form>
                     </courseware-tab>
                     <courseware-tab
-                        :index="1"
                         :name="$gettext('Video Einstellungen')"
                     >
                         <form class="default" @submit.prevent="">
diff --git a/resources/vue/components/courseware/blocks/ImageCompare.vue b/resources/vue/components/courseware/blocks/ImageCompare.vue
new file mode 100644
index 0000000000000000000000000000000000000000..40ad6e40f0be0f81c7a6c9a4a4264772ced2b3a2
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/ImageCompare.vue
@@ -0,0 +1,156 @@
+<template>
+    <div class="image-comparator" :style="{ height: imageHeight + 'px' }" ref="comparator">
+        <div class="images">
+            <!-- Erstes Bild, linke Seite -->
+            <img
+                :src="image1"
+                class="image"
+                :style="{ clipPath: 'inset(0 ' + (100 - sliderValue) + '% 0 0)' }"
+                @load="setImageHeight"
+            />
+
+            <!-- Zweites Bild, rechte Seite -->
+            <img :src="image2" class="image" :style="{ clipPath: 'inset(0 0 0 ' + sliderValue + '%)' }" />
+        </div>
+
+        <!-- Slider zur Steuerung des Vergleichs -->
+        <div class="slider-container" @mousedown="startDragging" @touchstart="startDragging">
+            <div class="thumb" :class="{ dragging: isDragging}" :style="{ left: `${sliderValue}%`, height: imageHeight + 'px' }">
+                <StudipIcon class="arrow-left" shape="arr_1left" :size="32" role="info_alt" draggable="false" />
+                <StudipIcon class="arrow-right" shape="arr_1right" :size="32" role="info_alt" draggable="false" />
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'ImageComparator',
+    data() {
+        return {
+            sliderValue: 50, // Anfangswert für den Slider (zeigt 50% von beiden Bildern)
+            imageHeight: 0, // Höhe der Komponente
+            isDragging: false, // Dragging-Zustand
+        };
+    },
+    props: {
+        image1: {
+            type: String,
+            required: true,
+        },
+        image2: {
+            type: String,
+            required: true,
+        },
+    },
+    computed: {},
+    methods: {
+        setImageHeight(event) {
+            const img = event.target;
+            const ratio = img.naturalHeight / img.naturalWidth;
+            this.imageHeight = this.$refs.comparator.clientWidth * ratio;
+        },
+        startDragging(event) {
+            this.isDragging = true;
+            this.updateSliderValue(event);
+            window.addEventListener('mousemove', this.updateSliderValue);
+            window.addEventListener('touchmove', this.updateSliderValue);
+            window.addEventListener('mouseup', this.stopDragging);
+            window.addEventListener('touchend', this.stopDragging);
+        },
+        stopDragging() {
+            this.isDragging = false;
+            window.removeEventListener('mousemove', this.updateSliderValue);
+            window.removeEventListener('touchmove', this.updateSliderValue);
+            window.removeEventListener('mouseup', this.stopDragging);
+            window.removeEventListener('touchend', this.stopDragging);
+        },
+        updateSliderValue(event) {
+            if (!this.isDragging) return;
+
+            const rect = this.$el.getBoundingClientRect();
+            const x = event.clientX - rect.left; // Position innerhalb des Containers
+            const width = rect.width;
+            const newValue = Math.min(Math.max((x / width) * 100, 0), 100); // Wert zwischen 0 und 100
+            this.sliderValue = newValue; // Sliderwert aktualisieren
+        },
+    },
+};
+</script>
+
+<style lang="scss">
+.image-comparator {
+    position: relative;
+    width: 100%;
+    margin: 0 auto;
+    overflow: hidden;
+
+    img {
+        -webkit-touch-callout: none; /* iOS Safari */
+        -webkit-user-select: none; /* Safari */
+        -khtml-user-select: none; /* Konqueror HTML */
+        -moz-user-select: none; /* Old versions of Firefox */
+        -ms-user-select: none; /* Internet Explorer/Edge */
+        user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */
+    }
+
+    .images {
+        position: relative;
+        display: flex;
+        width: 100%;
+        height: 100%;
+        overflow: hidden;
+
+        .image {
+            position: absolute;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: auto;
+            object-fit: cover;
+        }
+    }
+}
+
+.slider-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    transform: translateY(-50%);
+    display: flex;
+    align-items: center;
+
+    .thumb {
+        position: absolute;
+        top: 0;
+        width: 4px;
+        background: rgba(0, 0, 0, 0.2);
+        cursor: grab;
+
+        &.dragging {
+            cursor: grabbing;
+        }
+
+        .arrow-left,
+        .arrow-right {
+            position: absolute;
+            top: 50%;
+            background-color: rgba(0, 0, 0, 0.2);
+            padding: 8px 0px;
+        }
+
+        .arrow-left {
+            left: -32px;
+            border-top-left-radius: 8px;
+            border-bottom-left-radius: 8px;
+        }
+
+        .arrow-right {
+            right: -32px;
+            border-top-right-radius: 8px;
+            border-bottom-right-radius: 8px;
+        }
+    }
+}
+</style>
diff --git a/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue
index b43c811b6d0441de6b8f4dfac447f839ce2afc78..7f7c86d1e7504304fefa72e9cc7b2f047957cd03 100644
--- a/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue
@@ -41,39 +41,42 @@
                             :msgCompanion="$gettext('Dieses Fach enthält keine Blöcke.')">
                         </courseware-companion-box>
                         <draggable
+                            v-bind="dragOptions"
                             v-if="canEdit"
                             class="cw-container-accordion-block-list cw-container-accordion-sort-mode"
                             :class="[section.blocks.length === 0 ? 'cw-container-accordion-sort-mode-empty' : '']"
                             tag="ol"
                             role="listbox"
                             v-model="section.blocks"
-                            v-bind="dragOptions"
                             handle=".cw-sortable-handle"
                             group="blocks"
                             @start="isDragging = true"
                             @end="dropBlock"
                             :containerId="container.id"
                             :sectionId="index"
+                            item-key="id"
                         >
-                            <li v-for="block in section.blocks" :key="block.id" class="cw-block-item cw-block-item-sortable">
-                                <span
-                                    :class="{ 'cw-sortable-handle-dragging': isDragging }"
-                                    class="cw-sortable-handle"
-                                    tabindex="0"
-                                    role="button"
-                                    aria-describedby="operation"
-                                    :ref="'sortableHandle' + block.id"
-                                    @keydown="keyHandler($event, block.id, index)"
-                                ></span>
-                                <component
-                                    :is="component(block)"
-                                    :block="block"
-                                    :canEdit="canEdit"
-                                    :isTeacher="isTeacher"
-                                    :class="{ 'cw-block-item-selected': keyboardSelected === block.id}"
-                                    :blockId="block.id"
-                                />
-                            </li>
+                            <template #item="{element, index}">
+                                <li class="cw-block-item cw-block-item-sortable">
+                                    <span
+                                        :class="{ 'cw-sortable-handle-dragging': isDragging }"
+                                        class="cw-sortable-handle"
+                                        tabindex="0"
+                                        role="button"
+                                        aria-describedby="operation"
+                                        :ref="'sortableHandle' + element.id"
+                                        @keydown="keyHandler($event, element.id, index)"
+                                    ></span>
+                                    <component
+                                        :is="component(element)"
+                                        :block="element"
+                                        :canEdit="canEdit"
+                                        :isTeacher="isTeacher"
+                                        :class="{ 'cw-block-item-selected': keyboardSelected === element.id}"
+                                        :blockId="element.id"
+                                    />
+                                </li>
+                            </template>
                         </draggable>
                     </template>
                     <div v-else class="progress-wrapper">
@@ -104,7 +107,7 @@
                     <label>
                         {{ $gettext('Symbol')}}
                         <studip-select :options="icons" v-model="section.icon">
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
                             </template>
                             <template #no-options>
@@ -147,6 +150,7 @@ export default {
     components: Object.assign(ContainerComponents, {
         CoursewareCollapsibleBox,
     }),
+    emits: ['blockAdded'],
     props: {
         container: Object,
         canEdit: Boolean,
@@ -324,11 +328,14 @@ export default {
                         this.keyboardSelected = blockId;
                         const block = this.blockById({id: blockId});
                         const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-                        this.assistiveLive =
-                            this.$gettextInterpolate(
-                                this.$gettext('%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
-                                , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                            );
+                        this.assistiveLive = this.$gettext(
+                            '%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.',
+                            {
+                                blockTitle: block.attributes.title,
+                                pos: currentIndex + 1,
+                                listLength: this.currentSections[sectionIndex].blocks.length
+                            }
+                        );
                     }
                     break;
             }
@@ -354,11 +361,14 @@ export default {
             if (currentIndex !== 0) {
                 const newPos = currentIndex - 1;
                 this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                    );
+                this.assistiveLive = this.$gettext(
+                    '%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
+                    {
+                        blockTitle: block.attributes.title,
+                        pos: newPos + 1,
+                        listLength: this.currentSections[sectionIndex].blocks.length
+                    }
+                );
             } else if (sectionIndex !== 0) {
                 const newSectionIndex = sectionIndex - 1;
                 if (!this.sortInSlots.includes(newSectionIndex)) {
@@ -373,11 +383,14 @@ export default {
             if (this.currentSections[sectionIndex].blocks.length - 1 > currentIndex) {
                 const newPos = currentIndex + 1;
                 this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                    );
+                this.assistiveLive = this.$gettext(
+                    '%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
+                    {
+                        blockTitle: block.attributes.title,
+                        pos: newPos + 1,
+                        listLength: this.currentSections[sectionIndex].blocks.length
+                    }
+                );
             } else if (this.currentSections.length - 1 > sectionIndex) {
                 const newSectionIndex = sectionIndex + 1;
                 if (!this.sortInSlots.includes(newSectionIndex)) {
@@ -389,22 +402,24 @@ export default {
         abortKeyboardSorting(blockId, sectionIndex) {
             const block = this.blockById({id: blockId});
             this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, Neuordnung abgebrochen.')
-                    , {blockTitle: block.attributes.title}
-                );
+            this.assistiveLive = this.$gettext(
+                '%{blockTitle} Block, Neuordnung abgebrochen.',
+                { blockTitle: block.attributes.title }
+            );
             this.initCurrentData();
         },
         storeKeyboardSorting(blockId, sectionIndex) {
             const block = this.blockById({id: blockId});
             const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
             this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.')
-                    , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                );
+            this.assistiveLive = this.$gettext(
+                '%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.',
+                {
+                    blockTitle: block.attributes.title,
+                    pos: currentIndex + 1,
+                    listLength: this.currentSections[sectionIndex].blocks.length
+                }
+            );
             this.storeSort();
         }
     },
@@ -418,7 +433,7 @@ export default {
         },
         currentSections: {
             handler(newSections, oldSections) {
-                if (oldSections.length > 0 && 
+                if (oldSections.length > 0 &&
                     newSections[oldSections.length -1].blocks.length > oldSections[oldSections.length - 1].blocks.length) {
                         this.$emit('blockAdded');
                         this.$nextTick(() => {
@@ -436,7 +451,7 @@ export default {
         currentContainer: {
             handler() {
                 this.editDataValid = true;
-                this.currentContainer.attributes.payload.sections.forEach(section => {
+                this.currentContainer.attributes.payload.sections?.forEach(section => {
                     if (!section.icon && !section.name) {
                         this.editDataValid = false;
                     }
diff --git a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
index 875ab1fce91b80be360e4f97b660bd2ebd0b51b0..ce7c5457709c4f8d146ccda6a5276cc4986ee73b 100644
--- a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
+++ b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
@@ -21,6 +21,13 @@ export default {
         canEdit: Boolean,
         container: Object,
     },
+    emits: [
+        'changeContainer',
+        'copyToClipboard',
+        'deleteContainer',
+        'editContainer',
+        'removeLock',
+    ],
     computed: {
         ...mapGetters({
             userId: 'userId',
diff --git a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
index 7f4a882e84feb29ef6ad30ae739c4a91ad744f0a..b2eb62487261a88ba84eaa169e85404fed2e69d4 100644
--- a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
@@ -11,7 +11,7 @@
                     <span>{{ container.attributes.title }} ({{container.attributes.width}})</span>
                     <studip-icon v-if="blockedByAnotherUser" shape="lock-locked" />
                     <span v-if="blockedByAnotherUser" class="cw-default-container-blocker-warning">
-                        {{ $gettextInterpolate($gettext('Wird im Moment von %{ userName } bearbeitet'), { userName: this.blockingUserName }) }}
+                        {{ $gettext('Wird im Moment von %{ userName } bearbeitet', { userName: this.blockingUserName }) }}
                     </span>
                 </a>
                 <courseware-container-actions
@@ -144,6 +144,7 @@ export default {
         CoursewareContainerActions,
         StudipDialog,
     },
+    emits: ['closeEdit', 'showEdit', 'storeContainer'],
     props: {
         containerClass: String,
         container: Object,
@@ -263,9 +264,9 @@ export default {
             this.closeChange();
             if (this.blockedByAnotherUser) {
                 this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
+                    info: this.$gettext(
+                        'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.',
+                        { blockingUserName: this.blockingUserName }
                     )
                 });
                 return;
@@ -308,9 +309,9 @@ export default {
             }
             if (this.blockedByAnotherUser) {
                 this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
+                    info: this.$gettext(
+                        'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.',
+                        { blockingUserName: this.blockingUserName }
                     )
                 });
                 this.$emit('closeEdit');
@@ -331,9 +332,9 @@ export default {
                     this.showDeleteDialog = true;
                 } else {
                     this.companionInfo({
-                        info: this.$gettextInterpolate(
-                            this.$gettext('Löschen nicht möglich, da %{blockingUserName} den Abschnitt bearbeitet.'),
-                            {blockingUserName: this.blockingUserName}
+                        info: this.$gettext(
+                            'Löschen nicht möglich, da %{blockingUserName} den Abschnitt bearbeitet.',
+                            { blockingUserName: this.blockingUserName }
                         )
                     });
                 }
@@ -350,9 +351,9 @@ export default {
             await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
             if (this.blockedByAnotherUser) {
                 this.companionInfo({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'),
-                        {blockingUserName: this.blockingUserName}
+                    info: this.$gettext(
+                        'Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.',
+                        { blockingUserName: this.blockingUserName }
                     )
                 });
                 return false;
diff --git a/resources/vue/components/courseware/containers/CoursewareListContainer.vue b/resources/vue/components/courseware/containers/CoursewareListContainer.vue
index 327b20eb9c6d8482a7eef8daf5c401fa63b44a5e..730a32939d6f4b1bea806313b93d2dff597d071a 100644
--- a/resources/vue/components/courseware/containers/CoursewareListContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareListContainer.vue
@@ -25,11 +25,11 @@
                     </courseware-companion-box>
                     <draggable
                         v-if="canEdit"
+                        v-bind="dragOptions"
                         class="cw-container-list-block-list cw-container-list-sort-mode"
                         tag="ol"
                         role="listbox"
                         v-model="blockList"
-                        v-bind="dragOptions"
                         handle=".cw-sortable-handle"
                         group="blocks"
                         @start="isDragging = true"
@@ -37,30 +37,31 @@
                         ref="sortables"
                         :containerId="container.id"
                         sectionId="0"
+                        item-key="id"
                     >
-                        <li
-                            v-for="block in blockList"
-                            :key="block.id"
-                            class="cw-block-item cw-block-item-sortable"
-                        >
-                            <span
-                                :class="{ 'cw-sortable-handle-dragging': isDragging }"
-                                class="cw-sortable-handle"
-                                tabindex="0"
-                                role="button"
-                                aria-describedby="operation"
-                                :ref="'sortableHandle' + block.id"
-                                @keydown="keyHandler($event, block.id)"
-                            ></span>
-                            <component
-                                :is="component(block)"
-                                :block="block"
-                                :canEdit="canEdit"
-                                :isTeacher="isTeacher"
-                                :class="{ 'cw-block-item-selected': keyboardSelected === block.id}"
-                                :blockId="block.id"
-                            />
-                        </li>
+                        <template #item="{element}">
+                            <li
+                                class="cw-block-item cw-block-item-sortable"
+                            >
+                                <span
+                                    :class="{ 'cw-sortable-handle-dragging': isDragging }"
+                                    class="cw-sortable-handle"
+                                    tabindex="0"
+                                    role="button"
+                                    aria-describedby="operation"
+                                    :ref="'sortableHandle' + element.id"
+                                    @keydown="keyHandler($event, element.id)"
+                                ></span>
+                                <component
+                                    :is="component(element)"
+                                    :block="element"
+                                    :canEdit="canEdit"
+                                    :isTeacher="isTeacher"
+                                    :class="{ 'cw-block-item-selected': keyboardSelected === element.id}"
+                                    :blockId="element.id"
+                                />
+                            </li>
+                        </template>
                     </draggable>
                 </template>
                 <div v-else class="progress-wrapper" :style="{ height: contentHeight + 'px' }">
@@ -197,11 +198,14 @@ export default {
                         this.keyboardSelected = blockId;
                         const block = this.blockById({id: blockId});
                         const index = this.blockList.findIndex(b => b.id === block.id);
-                        this.assistiveLive = 
-                            this.$gettextInterpolate(
-                                this.$gettext('%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
-                                , {blockTitle: block.attributes.title, pos: index + 1, listLength: this.blockList.length}
-                            );
+                        this.assistiveLive = this.$gettext(
+                            '%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.',
+                            {
+                                blockTitle: block.attributes.title,
+                                pos: index + 1,
+                                listLength: this.blockList.length
+                            }
+                        );
                     }
                     break;
             }
@@ -227,11 +231,14 @@ export default {
                 const block = this.blockById({id: blockId});
                 const newPos = currentIndex - 1;
                 this.blockList.splice(newPos, 0, this.blockList.splice(currentIndex, 1)[0]);
-                this.assistiveLive = 
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.blockList.length}
-                    );
+                this.assistiveLive = this.$gettext(
+                    '%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
+                    {
+                        blockTitle: block.attributes.title,
+                        pos: newPos + 1,
+                        listLength: this.blockList.length
+                    }
+                );
             }
         },
         moveItemDown(blockId) {
@@ -240,32 +247,37 @@ export default {
                 const block = this.blockById({id: blockId});
                 const newPos = currentIndex + 1;
                 this.blockList.splice(newPos, 0, this.blockList.splice(currentIndex, 1)[0]);
-                this.assistiveLive = 
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.blockList.length}
-                    );
+                this.assistiveLive = this.$gettext(
+                    '%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
+                    {
+                        blockTitle: block.attributes.title,
+                        pos: newPos + 1,
+                        listLength: this.blockList.length
+                    }
+                );
             }
         },
         abortKeyboardSorting(blockId) {
             const block = this.blockById({id: blockId});
             this.keyboardSelected = null;
-            this.assistiveLive = 
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, Neuordnung abgebrochen.')
-                    , {blockTitle: block.attributes.title}
-                );
+            this.assistiveLive = this.$gettext(
+                '%{blockTitle} Block, Neuordnung abgebrochen.',
+                { blockTitle: block.attributes.title }
+            );
             this.initCurrentData();
         },
         storeKeyboardSorting(blockId) {
             const block = this.blockById({id: blockId});
             const currentIndex = this.blockList.findIndex(block => block.id === blockId);
             this.keyboardSelected = null;
-            this.assistiveLive = 
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.')
-                    , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.blockList.length}
-                );
+            this.assistiveLive = this.$gettext(
+                '%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.',
+                {
+                    blockTitle: block.attributes.title,
+                    pos: currentIndex + 1,
+                    listLength: this.blockList.length
+                }
+            );
             this.storeSort();
         }
     },
@@ -273,17 +285,23 @@ export default {
         this.initCurrentData();
     },
     watch: {
-        blocks() {
-            this.initCurrentData();
+        blocks: {
+            handler() {
+                this.initCurrentData();
+            },
+            deep: true
         },
-        blockList() {
-            if (this.keyboardSelected) {
-                this.$nextTick(() => {
-                    const selected = this.$refs['sortableHandle' + this.keyboardSelected][0];
-                    selected.focus();
-                    selected.scrollIntoView({behavior: "smooth", block: "center"});
-                });
-            }
+        blockList: {
+            handler() {
+                if (this.keyboardSelected) {
+                    this.$nextTick(() => {
+                        const selected = this.$refs['sortableHandle' + this.keyboardSelected][0];
+                        selected.focus();
+                        selected.scrollIntoView({behavior: "smooth", block: "center"});
+                    });
+                }
+            },
+            deep: true
         }
     }
 };
diff --git a/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue b/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue
index e9c45ca4fe6c36ee40e696fcab3ce8a004e0727f..490ad3cc992bb006cff676c9430f203d070a7bc1 100644
--- a/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue
@@ -24,14 +24,12 @@
                     {{ $gettext('Drücken Sie die Leertaste, um neu anzuordnen.') }}
                 </span>
             </template>
-            <courseware-tabs @selectTab="selectTabHandler">
+            <courseware-tabs v-if="!processing" v-model="selectedTab">
                 <courseware-tab
-                    v-for="(section, index) in currentSections"
-                    :key="index"
-                    :index="index"
+                    v-for="(section, sectionIndex) in currentSections"
+                    :key="sectionIndex"
                     :name="section.name"
                     :icon="section.icon"
-                    :selected="sortInTab === index"
                 >
                     <ul v-if="!canEdit || currentElementisLink" class="cw-container-tabs-block-list">
                         <li v-for="block in section.blocks" :key="block.id" class="cw-block-item">
@@ -51,38 +49,41 @@
                                 :msgCompanion="$gettext('Dieses Fach enthält keine Blöcke.')">
                             </courseware-companion-box>
                             <draggable
+                                v-bind="dragOptions"
                                 class="cw-container-tabs-block-list cw-container-tabs-sort-mode"
                                 :class="[section.blocks.length === 0 ? 'cw-container-tabs-sort-mode-empty' : '']"
                                 tag="ol"
                                 role="listbox"
                                 v-model="section.blocks"
-                                v-bind="dragOptions"
                                 handle=".cw-sortable-handle"
                                 group="blocks"
                                 @start="isDragging = true"
                                 @end="dropBlock"
                                 :containerId="container.id"
-                                :sectionId="index"
+                                :sectionId="sectionIndex"
+                                item-key="id"
                             >
-                                <li v-for="block in section.blocks" :key="block.id" class="cw-block-item cw-block-item-sortable">
-                                    <span
-                                        :class="{ 'cw-sortable-handle-dragging': isDragging }"
-                                        class="cw-sortable-handle"
-                                        tabindex="0"
-                                        role="button"
-                                        aria-describedby="operation"
-                                        :ref="'sortableHandle' + block.id"
-                                        @keydown="keyHandler($event, block.id, index)"
-                                    ></span>
-                                    <component
-                                        :is="component(block)"
-                                        :block="block"
-                                        :canEdit="canEdit"
-                                        :isTeacher="isTeacher"
-                                        :class="{ 'cw-block-item-selected': keyboardSelected === block.id}"
-                                        :blockId="block.id"
-                                    />
-                                </li>
+                                <template #item="{element}">
+                                    <li class="cw-block-item cw-block-item-sortable">
+                                        <span
+                                            :class="{ 'cw-sortable-handle-dragging': isDragging }"
+                                            class="cw-sortable-handle"
+                                            tabindex="0"
+                                            role="button"
+                                            aria-describedby="operation"
+                                            :ref="'sortableHandle' + element.id"
+                                            @keydown="keyHandler($event, element.id, sectionIndex)"
+                                        ></span>
+                                        <component
+                                            :is="component(element)"
+                                            :block="element"
+                                            :canEdit="canEdit"
+                                            :isTeacher="isTeacher"
+                                            :class="{ 'cw-block-item-selected': keyboardSelected === element.id}"
+                                            :blockId="element.id"
+                                        />
+                                    </li>
+                                </template>
                             </draggable>
                         </template>
                     </template>
@@ -103,7 +104,7 @@
                     <label>
                         {{ $gettext('Symbol') }}
                         <studip-select :options="icons" v-model="section.icon">
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
                             </template>
                             <template #no-options>
@@ -138,6 +139,7 @@ import CoursewareTabs from '../layouts/CoursewareTabs.vue';
 import CoursewareTab from '../layouts/CoursewareTab.vue';
 import containerMixin from '@/vue/mixins/courseware/container.js';
 import contentIconsMixin from '@/vue/mixins/courseware/content-icons.js';
+import draggable from "vuedraggable";
 
 import { mapGetters, mapActions } from 'vuex';
 
@@ -147,6 +149,7 @@ export default {
     components: Object.assign(ContainerComponents, {
         CoursewareTabs,
         CoursewareTab,
+        draggable
     }),
     props: {
         container: Object,
@@ -172,7 +175,7 @@ export default {
             },
             processing: false,
             keyboardSelected: null,
-            sortInTab: 0,
+            selectedTab: 0,
             assistiveLive: '',
             showDeleteDialog: false,
             currentSection: null,
@@ -209,7 +212,6 @@ export default {
         }),
         initCurrentData() {
             this.currentContainer = _.cloneDeep(this.container);
-
             let view = this;
             let sections = this.currentContainer.attributes.payload.sections;
 
@@ -276,7 +278,8 @@ export default {
             this.closeDeleteDialog();
         },
         async storeContainer() {
-            const timeout = setTimeout(() => this.processing = true, 800);
+            this.processing = true;
+            await this.$nextTick();
             this.currentContainer.attributes.payload.sections = this.currentContainer.attributes.payload.sections.filter(section => !section.locked);
             this.currentContainer.attributes.payload.sections.forEach(section => {
                 section.blocks = section.blocks.map((block) => {return block.id;});
@@ -289,7 +292,6 @@ export default {
             await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
             await this.loadContainer({id : this.container.id });
             this.initCurrentData();
-            clearTimeout(timeout);
             this.processing = false;
         },
         async storeSort() {
@@ -325,11 +327,14 @@ export default {
                         this.keyboardSelected = blockId;
                         const block = this.blockById({id: blockId});
                         const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
-                        this.assistiveLive =
-                            this.$gettextInterpolate(
-                                this.$gettext('%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.')
-                                , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                            );
+                        this.assistiveLive = this.$gettext(
+                            '%{blockTitle} Block ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.',
+                            {
+                                blockTitle: block.attributes.title,
+                                pos: currentIndex + 1,
+                                listLength: this.currentSections[sectionIndex].blocks.length
+                            }
+                        );
                     }
                     break;
             }
@@ -355,14 +360,17 @@ export default {
             if (currentIndex !== 0) {
                 const newPos = currentIndex - 1;
                 this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                    );
+                this.assistiveLive = this.$gettext(
+                    '%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
+                    {
+                        blockTitle: block.attributes.title,
+                        pos: newPos + 1,
+                        listLength: this.currentSections[sectionIndex].blocks.length
+                    }
+                );
             } else if (sectionIndex !== 0) {
                 const newSectionIndex = sectionIndex - 1;
-                this.sortInTab = newSectionIndex;
+                this.selectedTab = newSectionIndex;
                 this.currentSections[newSectionIndex].blocks.push(this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
             }
         },
@@ -372,47 +380,51 @@ export default {
             if (this.currentSections[sectionIndex].blocks.length - 1 > currentIndex) {
                 const newPos = currentIndex + 1;
                 this.currentSections[sectionIndex].blocks.splice(newPos, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
-                this.assistiveLive =
-                    this.$gettextInterpolate(
-                        this.$gettext('%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.')
-                        , {blockTitle: block.attributes.title, pos: newPos + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                    );
+                this.assistiveLive = this.$gettext(
+                    '%{blockTitle} Block. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
+                    {
+                        blockTitle: block.attributes.title,
+                        pos: newPos + 1,
+                        listLength: this.currentSections[sectionIndex].blocks.length
+                    }
+                );
             } else if (this.currentSections.length - 1 > sectionIndex) {
                 const newSectionIndex = sectionIndex + 1;
-                this.sortInTab = newSectionIndex;
+                this.selectedTab = newSectionIndex;
                 this.currentSections[newSectionIndex].blocks.splice(0, 0, this.currentSections[sectionIndex].blocks.splice(currentIndex, 1)[0]);
             }
         },
         abortKeyboardSorting(blockId, sectionIndex) {
             const block = this.blockById({id: blockId});
             this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, Neuordnung abgebrochen.')
-                    , {blockTitle: block.attributes.title}
-                );
+            this.assistiveLive = this.$gettext(
+                '%{blockTitle} Block, Neuordnung abgebrochen.',
+                { blockTitle: block.attributes.title }
+            );
             this.initCurrentData();
         },
         storeKeyboardSorting(blockId, sectionIndex) {
             const block = this.blockById({id: blockId});
             const currentIndex = this.currentSections[sectionIndex].blocks.findIndex(block => block.id === blockId);
             this.keyboardSelected = null;
-            this.assistiveLive =
-                this.$gettextInterpolate(
-                    this.$gettext('%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.')
-                    , {blockTitle: block.attributes.title, pos: currentIndex + 1, listLength: this.currentSections[sectionIndex].blocks.length}
-                );
+            this.assistiveLive = this.$gettext(
+                '%{blockTitle} Block, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.',
+                {
+                    blockTitle: block.attributes.title,
+                    pos: currentIndex + 1,
+                    listLength: this.currentSections[sectionIndex].blocks.length
+                }
+            );
             this.storeSort();
         },
-        selectTabHandler(event) {
-            const tabIndex = event.index;
+        selectTabHandler() {
             let container = _.cloneDeep(this.container);
-            container.activeSection = tabIndex;
+            container.activeSection = this.selectedTab;
             this.storeContainerRecord(container);
             if (this.blockAdder.container.id === this.container.id) {
                 this.setAdderStorage({
                     container: this.container,
-                    section: tabIndex
+                    section: this.selectedTab
                 });
             }
         }
@@ -445,6 +457,9 @@ export default {
                 });
             },
             deep: true
+        },
+        selectedTab() {
+            this.selectTabHandler();
         }
     }
 };
diff --git a/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue
index 601a339e1a569eb33bd8bcab955e1295ea193adb..ffe3953fad5b0c7b761898d48777b10a10a80a9e 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue
@@ -19,6 +19,7 @@ export default {
     components: {
         StudipIcon
     },
+    emits: ['click'],
     props: {
         iconShape: {
             type: String,
@@ -67,4 +68,4 @@ export default {
         }
     }
 }
-</script>
\ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
index e79df40ffeaec2bbc19aa0900dca8da3d72e6ccf..7e41a92f16dcc4f3a16e5cf011e40934ecc9891f 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
@@ -45,7 +45,7 @@ export default {
     methods: {
         updateCollapsible() {
             if (this.isOpen) {
-                STUDIP.eventBus.emit('courseware:update-collapsible', { 'uid': this._uid });
+                STUDIP.eventBus.emit('courseware:update-collapsible', { 'uid': this._.uid });
             }
         }
     },
diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
index bfd0a93374357b2fa3ba59317175e2c3545319e4..0f214f0179b469ea56f7b5bd1ce337a9a9539ba6 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
@@ -3,7 +3,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-companion-overlay',
-    render(createElement) {
+    render() {
         return null;
     },
     computed: {
diff --git a/resources/vue/components/courseware/layouts/CoursewareDateInput.vue b/resources/vue/components/courseware/layouts/CoursewareDateInput.vue
index 57a5619fbee207cf40021b276591a274d0a80bc4..602431d6295d117d9eefdc6671e850dc922a7702 100644
--- a/resources/vue/components/courseware/layouts/CoursewareDateInput.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareDateInput.vue
@@ -8,7 +8,8 @@ const toISO8601 = (date) => date.toISOString();
 const pad = (what, length = 2) => `00000000${what}`.substr(-length);
 
 export default {
-    props: ['value'],
+    emits: ['update:model-value'],
+    props: ['model-value'],
     data: () => ({
         date: new Date(),
     }),
@@ -20,14 +21,14 @@ export default {
     methods: {
         onInput({ target }) {
             const newValue = toISO8601(target.valueAsDate);
-            if (newValue !== this.value) {
-                this.$emit('input', newValue);
+            if (newValue !== this.modelValue) {
+                this.$emit('update:model-value', newValue);
             }
         },
     },
-    beforeMount() {
-        if (this.value) {
-            this.date = fromISO8601(this.value);
+    created() {
+        if (this.model-value) {
+            this.date = fromISO8601(this.modelValue);
         }
     },
 };
diff --git a/resources/vue/components/courseware/layouts/CoursewareFileChooser.vue b/resources/vue/components/courseware/layouts/CoursewareFileChooser.vue
index 6571b3430e0de5618673c3661ea3f78dc52c4cb2..a9b2aa3dc858f7ce58e76c4cd4264c09af914e95 100644
--- a/resources/vue/components/courseware/layouts/CoursewareFileChooser.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareFileChooser.vue
@@ -1,17 +1,17 @@
 <template>
     <div class="cw-file-chooser">
-        <span v-translate>Ordner-Filter</span>
+        <span>{{ $gettext('Ordner-Filter') }}</span>
         <courseware-folder-chooser allowUserFolders unchoose v-model="selectedFolderId" />
-        <span v-translate>Datei</span>
+        <span>{{ $gettext('Datei') }}</span>
         <select v-model="currentValue" @change="selectFile">
             <option v-show="canBeEmpty" value="">
-                <translate>Keine Auswahl</translate>
+                {{ $gettext('Keine Auswahl') }}
             </option>
             <option v-for="(file, index) in files" :key="index" :value="file.id">
                 {{ file.name }}
             </option>
             <option v-show="files.length === 0" disabled>
-                <translate>Keine Dateien vorhanden</translate>
+                {{ $gettext('Keine Dateien vorhanden') }}
             </option>
         </select>
     </div>
@@ -25,6 +25,7 @@ import { mapActions, mapGetters } from 'vuex';
 export default {
     name: 'courseware-file-chooser',
     components: { CoursewareFolderChooser },
+    emits: ['selectFile'],
     props: {
         value: String,
         mimeType: { type: String, default: '' },
diff --git a/resources/vue/components/courseware/layouts/CoursewareFolderChooser.vue b/resources/vue/components/courseware/layouts/CoursewareFolderChooser.vue
index e0716795d10626ecc834a8b95219b77c9812bb05..2c0a270468ad1da24d174687b6397aa1c610f81f 100644
--- a/resources/vue/components/courseware/layouts/CoursewareFolderChooser.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareFolderChooser.vue
@@ -1,6 +1,6 @@
 <template>
     <select v-model="currentValue" @change="changeSelection">
-        <option v-if="unchoose" value=""><translate>kein Ordner ausgewählt</translate></option>
+        <option v-if="unchoose" value="">{{ $gettext('kein Ordner ausgewählt') }}</option>
         <optgroup v-if="this.context.type === 'courses'" :label="textOptGroupCourse">
             <option v-for="folder in loadedCourseFolders" :key="folder.id" :value="folder.id">
                 {{ folder.attributes.name }}
@@ -69,8 +69,9 @@ function filterCourseFolders(folders, { allowHomeworkFolders }) {
 }
 export default {
     name: 'courseware-folder-chooser',
+    emits: ['update:model-value'],
     props: {
-        value: String,
+        modelValue: String,
         allowUserFolders: { type: Boolean, default: false },
         allowHomeworkFolders: { type: Boolean, default: false },
         unchoose: { type: Boolean, default: false },
@@ -120,7 +121,7 @@ export default {
         }),
 
         changeSelection() {
-            this.$emit('input', this.currentValue);
+            this.$emit('update:model-value', this.currentValue);
         },
 
         getCourseFolders() {
@@ -151,7 +152,7 @@ export default {
         }
     },
     async mounted() {
-        this.currentValue = this.value;
+        this.currentValue = this.modelValue;
         if (this.context.type !== 'users') {
             await this.getCourseFolders();
         }
@@ -159,8 +160,8 @@ export default {
         this.confirmSelectedFolder();
     },
     watch: {
-        value() {
-            this.currentValue = this.value;
+        modelValue() {
+            this.currentValue = this.modelValue;
         }
     },
 };
diff --git a/resources/vue/components/courseware/layouts/CoursewareTab.vue b/resources/vue/components/courseware/layouts/CoursewareTab.vue
index ae007074872456d532c2f0443b825135a7bbf425..5e1f20b9bff363c56c586ebdd24d452ee101ac6d 100644
--- a/resources/vue/components/courseware/layouts/CoursewareTab.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareTab.vue
@@ -1,51 +1,35 @@
 <template>
-    <div
-        v-if="isActive"
-        role="tabpanel"
-        class="cw-tab"
-        :id="id"
-        :aria-labelledby="selectorId"
-    >
+    <div v-if="isActive">
         <slot></slot>
     </div>
 </template>
 
 <script>
 export default {
-    name: 'courseware-tab',
+    inject: ['addTab', 'activeTab'],
     props: {
-        name: {type: String, required: true },
-        alias: {type: String, default: ''},
-        selected: { type: Boolean, default: false },
-        index: {type: Number, required: true },
-        icon: {type: String, default: ''},
+        name: {
+            type: String,
+            required: true,
+        },
+        icon: {
+            type: String,
+            default: '',
+        },
     },
     data() {
         return {
-            isActive: false,
+            index: null,
         };
     },
     computed: {
-        selectorId() {
-            return '#' + this._uid + '-' + this.name.toLowerCase().replace(/ /g, '-');
+        isActive() {
+            return this.activeTab() === this.index;
         },
-        id() {
-            return this._uid + '-' + this.name.toLowerCase().replace(/ /g, '-') + '-tabpanel';
-        }
     },
     mounted() {
-        this.isActive = this.selected;
-    },
-    updated () {
-        if (this.isActive) {
-            STUDIP.eventBus.emit('courseware:update-tab',{ 'uid': this._uid });
-        }
-
+        this.index = this.$parent.tabs.length;
+        this.addTab({ name: this.name, icon: this.icon});
     },
-    watch: {
-        selected(newValue) {
-            this.isActive = newValue;
-        }
-    }
 };
-</script>
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/layouts/CoursewareTabs.vue b/resources/vue/components/courseware/layouts/CoursewareTabs.vue
index 5bdcc47cfb9580f7aed6077369d0692587fc245b..c29d88e64f1dbcc8fb0eee6ce46d41fa5cbba5b7 100644
--- a/resources/vue/components/courseware/layouts/CoursewareTabs.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareTabs.vue
@@ -1,26 +1,25 @@
 <template>
-    <div class="cw-tabs">
-        <div role="tablist" class="cw-tabs-nav">
+    <div class="tabs cw-tabs">
+        <div class="tab-buttons cw-tabs-nav">
             <button
                 v-for="(tab, index) in tabs"
                 :key="index"
+                :data-index="index"
                 :class="[
-                    tab.isActive ? 'is-active' : '',
+                    activeTab === index ? 'is-active' : '',
                     tab.icon !== '' && tab.name !== '' ? 'cw-tabs-nav-icon-text-' + tab.icon : '',
                     tab.icon !== '' && tab.name === '' ? 'cw-tabs-nav-icon-solo-' + tab.icon : '',
                 ]"
-                :aria-selected="tab.isActive"
-                :aria-controls="tab.id"
-                :id="tab.selectorId"
-                :tabindex="tab.isActive ? 0 : -1"
-                @click="selectTab(tab.selectorId)"
+                :tabindex="activeTab === index ? 0 : -1"
+                :aria-selected="activeTab === index"
+                @click="selectTab(index)"
                 @keydown="handleKeyEvent($event)"
-                :ref="tab.selectorId"
+                :ref="'tabnav' + index"
             >
                 {{ tab.name }}
             </button>
         </div>
-        <div class="cw-tabs-content">
+        <div class="tab-content cw-tabs-content">
             <slot></slot>
         </div>
     </div>
@@ -28,106 +27,78 @@
 
 <script>
 export default {
-    name: 'courseware-tabs',
+    name: 'CoursewareTabs',
+    emits: ['update:modelValue'],
     props: {
-        setSelected: { type: Number, required: false, default: 0 },
+        modelValue: { type: Number },
     },
     data() {
         return {
+            activeTab: 0,
             tabs: [],
         };
     },
-    created() {
-        this.tabs = this.$children.sort((a, b) => {
-            return a.index - b.index;
-        });
-    },
     methods: {
-        selectTab(selectorId) {
-            let view = this;
-            this.tabs.forEach((tab) => {
-                tab.isActive = false;
-                if (tab.selectorId === selectorId) {
-                    tab.isActive = true;
-                    view.$refs[selectorId][0].focus();
-                    view.$emit('selectTab', tab);
-                }
-            });
+        selectTab(index) {
+            if (index >= this.tabs.length || index < 0) {
+                return;
+            }
+            this.activeTab = index;
+            this.$refs['tabnav' + index][0].focus();
         },
-        selectTabByIndex(selectedIndex) {
-            let view = this;
-            this.tabs.forEach((tab) => {
-                tab.isActive = false;
-                if (tab.index === selectedIndex) {
-                    tab.isActive = true;
-                    view.$refs[tab.selectorId][0].focus();
-                    view.$emit('selectTab', tab);
-                }
-            });
+        addTab(tab) {
+            this.tabs.push(tab);
         },
         handleKeyEvent(e) {
-            let tablist = e.target.parentElement;
+            const index = parseInt(e.target.dataset.index);
             switch (e.keyCode) {
                 case 37: // left
                 case 38: // up
-                    if (tablist.firstChild.id !== e.target.id) {
-                        this.selectTab(e.target.previousElementSibling.id);
+                    if (index !== 0) {
+                        this.selectTab(index - 1);
                     } else {
-                        this.selectTab(tablist.lastChild.id);
+                        this.selectTab(this.tabs.length - 1);
                     }
                     break;
                 case 39: // right
                 case 40: // down
-                    if (tablist.lastChild.id !== e.target.id) {
-                        this.selectTab(e.target.nextElementSibling.id);
+                    if (index !== this.tabs.length - 1) {
+                        this.selectTab(index + 1);
                     } else {
-                        this.selectTab(tablist.firstChild.id);
+                        this.selectTab(0);
                     }
                     break;
                 case 36: //pos1
-                    this.selectTab(tablist.firstChild.id);
+                    this.selectTab(0);
                     break;
                 case 35: //end
-                    this.selectTab(tablist.lastChild.id);
+                    this.selectTab(this.tabs.length - 1);
                     break;
             }
         },
-        getButtonById(id) {
-            return this.$refs[id][0];
-        },
-        getFirstTabButton() {
-            let selectorId = this.tabs[0].selectorId;
-            return this.$refs[selectorId][0];
-        },
-        getTabButtonByName(name) {
-            let selectorId = null;
-            this.tabs.forEach((tab) => {
-                if (tab.name === name) {
-                    selectorId = tab.selectorId;
-                }
-            });
-            if (selectorId) {
-                return this.$refs[selectorId][0];
-            }
-            return null;
-        },
-        getTabButtonByAlias(alias) {
-            let selectorId = null;
-            this.tabs.forEach((tab) => {
-                if (tab.alias === alias) {
-                    selectorId = tab.selectorId;
-                }
-            });
-            if (selectorId) {
-                return this.$refs[selectorId][0];
+    },
+    provide() {
+        return {
+            addTab: this.addTab,
+            activeTab: () => this.activeTab,
+        };
+    },
+    mounted() {
+        this.$nextTick(() => {
+            if (this.modelValue) {
+                this.selectTab(this.modelValue);
             }
-            return null;
-        },
+        });
     },
     watch: {
-        setSelected(tab) {
-            this.selectTabByIndex(tab);
+        modelValue(newValue) {
+            this.selectTab(newValue);
+        },
+        activeTab(newValue) {
+            if (this.modelValue !== newValue) {
+                this.$emit('update:modelValue', newValue);
+            }
         },
     },
 };
-</script>
\ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue b/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue
index 007a72ef6e3dd9891f79043b2b8d4bacc469313b..b3df5715c908ab9c51db26803662e0bc7077ef57 100644
--- a/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue
@@ -34,6 +34,7 @@ import { mapGetters } from 'vuex';
 export default {
     name: 'courseware-talk-bubble',
     components: { IsoDate },
+    emits: ['delete'],
     props: {
         payload: Object,
     },
diff --git a/resources/vue/components/courseware/layouts/CoursewareTile.vue b/resources/vue/components/courseware/layouts/CoursewareTile.vue
index d2f53c3535698707384e7bbccf0e403a408afdef..4298439e09d21db73bfd335557d54796fd356d7f 100644
--- a/resources/vue/components/courseware/layouts/CoursewareTile.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareTile.vue
@@ -55,6 +55,7 @@ export default {
     components: {
         StudipIdentImage
     },
+    emits: ['handle-keydown', 'showProgress'],
     props: {
         tag: {
             type: String,
@@ -146,15 +147,15 @@ export default {
         previewImageStyle() {
             if (this.hasImage) {
                 return { 'background-image': 'url(' + this.imageUrl + ')' };
-            } 
+            }
 
             return { 'background-image': 'url(' + this.identimage + ')' };
         },
         progressTitle() {
             if (this.userIsTeacher) {
-                return this.$gettextInterpolate(this.$gettext("Fortschritt aller Teilnehmenden: %{progress}%"), { progress: this.progress });    
+                return this.$gettext("Fortschritt aller Teilnehmenden: %{progress}%", { progress: this.progress });
             }
-            return this.$gettextInterpolate(this.$gettext("Mein Fortschritt: %{progress}%"), { progress: this.progress });
+            return this.$gettext("Mein Fortschritt: %{progress}%", { progress: this.progress });
         },
         hasDescriptionLink() {
             return this.descriptionLink !== '';
diff --git a/resources/vue/components/courseware/structural-element/CoursewareFeedbackPopup.vue b/resources/vue/components/courseware/structural-element/CoursewareFeedbackPopup.vue
index 687ca3ca7bd2c14f7592a429e6fa835565292b3b..81e1e4a822adc5a4edabecfd3e30ba7b33e448d5 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareFeedbackPopup.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareFeedbackPopup.vue
@@ -11,7 +11,7 @@
         @confirm="submitEntry"
     >
         <template v-slot:dialogContent>
-            <h2>{{ $gettextInterpolate($gettext('Bewertung für %{title}'),  { title: structuralElement.attributes.title }) }}</h2>
+            <h2>{{ $gettext('Bewertung für %{title}', { title: structuralElement.attributes.title }) }}</h2>
 
             <div class="feedback-entry-create">
                 <studip-five-stars-input v-model="rating" />
@@ -34,6 +34,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-feedback-popup',
+    emits: ['close', 'submit'],
     components: {
         StudipFiveStarsInput,
     },
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
index 3f6eaba4489d9f5736bd6ecea2136f980230e4b0..5a9e98a7ba83bf3efbc12048d718e7750fb697ac 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
@@ -31,14 +31,33 @@
 
 <script lang="ts">
 import ContentBar from '../../ContentBar.vue';
-import { mapActions, mapGetters, mapState } from 'vuex';
-import Vue from 'vue';
+import { mapActions, mapGetters } from 'vuex';
 import CoursewareRibbonToolbar from './CoursewareRibbonToolbar.vue';
 import { store } from '../../../../assets/javascripts/chunks/vue';
+import { defineComponent } from "vue";
 
-export default Vue.extend({
+export default defineComponent({
     name: 'CoursewareRibbon',
-    components: { CoursewareRibbonToolbar, ContentBar },
+    components: {
+        CoursewareRibbonToolbar, ContentBar
+    },
+    emits: ['blockAdded'],
+    props: {
+        canEdit: Boolean,
+        showToolbarButton: {
+            default: true,
+            type: Boolean
+        },
+        showModeSwitchButton: {
+            default: true,
+            type: Boolean
+        },
+        buttonsClass: String,
+        isContentBar: {
+            type: Boolean,
+            default: false
+        }
+    },
     data() {
         return {
             // This value is derived from stickyRibbonChange events emitted by
@@ -81,13 +100,13 @@ export default Vue.extend({
         },
     },
     methods: {
-        onStickyRibbonChange(value: boolean) {
-            this.stickyRibbon = value;
-        },
         ...mapActions({
             coursewareViewMode: 'coursewareViewMode',
             coursewareShowToolbar: 'coursewareShowToolbar',
         }),
+        onStickyRibbonChange(value: boolean) {
+            this.stickyRibbon = value;
+        },
         activateToolbar() {
             this.coursewareShowToolbar(true);
         },
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
index fe99808e6406006045751341a6e18f678c246626..9af7a490b3f598f7c90d1ea22e984cdd3dd2b40d 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
@@ -1,5 +1,5 @@
 <template>
-    <focus-trap v-model="trap" :initial-focus="() => initialFocusElement" :clickOutsideDeactivates="true" :fallbackFocus ="() => fallbackFocusElement">
+    <focus-trap v-model="trap" :clickOutsideDeactivates="false" :fallbackFocus ="() => fallbackFocusElement">
         <div
             class="cw-ribbon-tools"
             :class="{ 'cw-ribbon-tools-consume': consumeMode }"
@@ -12,7 +12,6 @@
                     >
                         <courseware-tab
                             :name="$gettext('Inhaltsverzeichnis')"
-                            :selected="showContents"
                             alias="contents"
                             ref="contents"
                             :index="0"
@@ -53,6 +52,7 @@ import { store } from "../../../../assets/javascripts/chunks/vue";
 
 export default {
     name: 'courseware-ribbon-toolbar',
+    emits: ['deactivate'],
     components: {
         CoursewareTabs,
         CoursewareTab,
@@ -71,7 +71,6 @@ export default {
             showContents: true,
             showUnits: false,
             trap: false,
-            initialFocusElement: null
         };
     },
     computed: {
@@ -113,7 +112,7 @@ export default {
                 this.initialFocusElement = focusElement;
                 this.trap = true;
             }
-        },
+        }
     },
     mounted() {
         this.$nextTick(() => {
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue
index ee820c4a1754a63878b61c7c671b5a5d0e163c85..bc6833822c24e5f4badbbfc8c2a8c6f22111c0ca 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue
@@ -60,8 +60,10 @@
                             <template #footer>
                                 <p class="cw-root-content-toc-tile-footer">
                                 {{
-                                    $gettextInterpolate(
-                                        $ngettext('%{pages} Seite', '%{pages} Seiten', countChildChildren(child)),
+                                    $ngettext(
+                                        '%{pages} Seite',
+                                        '%{pages} Seiten',
+                                        countChildChildren(child),
                                         { pages: countChildChildren(child) }
                                     )
                                 }}
diff --git a/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
index 5d7547d4d9e42bc0ae2532b4b01fefb199aa624e..961fae3f51be23046052cc8b12f821aaa88e9740 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
@@ -5,7 +5,7 @@
                 <studip-icon shape="search" :size="24" />
             </template>
             <template #breadcrumb-list>
-                <translate>Suchergebnisse</translate>
+                {{ $gettext('Suchergebnisse') }}
             </template>
             <template #menu>
                 <button :title="$gettext('Suchergebnisse schließen')" @click="closeResults">
@@ -18,7 +18,7 @@
                 <section v-for="result in searchResults" :key="result['structural-element-id']">
                     <router-link
                         :to="'/structural_element/' + result['structural-element-id']"
-                        @click.native="closeResults"
+                        @click="closeResults"
                     >
                         <div v-show="result.img !== null" class="search-result-img hidden-tiny-down">
                             <img :src="result.img" />
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
index 271af3ec19a6275397bedbd2fe673a1158869d03..583717e0e72a79f53ca9e957535897bf79479c43 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
@@ -1,5 +1,5 @@
 <template>
-    <focus-trap v-model="consumModeTrap">
+    <focus-trap v-model:active="consumModeTrap">
         <div>
             <div
                 v-if="validContext"
@@ -54,11 +54,10 @@
                                         />
                                         <span
                                             v-else
-                                            :title="$gettextInterpolate(
-                                                        $gettext('Fortschritt: %{progress} %'),{
-                                                        progress: elementProgress,
-                                            })
-                                                    "
+                                            :title="$gettext(
+                                                'Fortschritt: %{progress} %',
+                                                {progress: elementProgress}
+                                            )"
                                         >
                                             ({{ elementProgress }} %)
                                         </span>
@@ -69,11 +68,12 @@
                                         :size="16"
                                         :role="hasFeedbackAverage ? 'status-yellow' : 'inactive'"
                                         :title="
-                                        hasFeedbackAverage
-                                            ?$gettextInterpolate($gettext('Seite wurde mit %{avg} Sternen bewertet'), {
-                                                avg: feedbackAverage,
-                                            })
-                                            :$gettext('Seite wurde noch nicht bewertet')
+                                            hasFeedbackAverage
+                                                ? $gettext(
+                                                    'Seite wurde mit %{avg} Sternen bewertet',
+                                                    { avg: feedbackAverage }
+                                                  )
+                                                : $gettext('Seite wurde noch nicht bewertet')
                                         "
                                         @click="menuAction('showFeedback')"
                                     />
@@ -142,10 +142,8 @@
                                 <courseware-companion-box
                                     v-if="blockedByAnotherUser"
                                     :msgCompanion="
-                                        $gettextInterpolate(
-                                            $gettext(
-                                                'Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet.'
-                                            ),
+                                        $gettext(
+                                            'Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet.',
                                             { blockingUserName: blockingUserName }
                                         )
                                     "
@@ -199,10 +197,8 @@
                                 <div v-if="canEdit" class="cw-companion-box-wrapper">
                                     <courseware-companion-box
                                         :msgCompanion="
-                                            $gettextInterpolate(
-                                                $gettext(
-                                                    'Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.'
-                                                ),
+                                            $gettext(
+                                                'Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.',
                                                 { ownerName: ownerName }
                                             )
                                         "
@@ -227,42 +223,41 @@
                                         {{ $gettext('Drücken Sie die Leertaste, um neu anzuordnen.') }}
                                     </span>
                                     <draggable
+                                        v-bind="dragOptions"
                                         class="cw-structural-element-list"
                                         tag="ol"
                                         role="listbox"
                                         v-model="containerList"
-                                        v-bind="dragOptions"
                                         handle=".cw-sortable-handle"
                                         @start="isDragging = true"
                                         @end="dropContainer"
+                                        item-key="id"
                                     >
-                                        <li
-                                            v-for="container in containerList"
-                                            :key="container.id"
-                                            class="cw-container-item-sortable"
-                                        >
-                                            <span
-                                                :class="{ 'cw-sortable-handle-dragging': isDragging }"
-                                                class="cw-sortable-handle"
-                                                tabindex="0"
-                                                role="option"
-                                                aria-describedby="operation"
-                                                :ref="'sortableHandle' + container.id"
-                                                @keydown="keyHandler($event, container.id)"
-                                            ></span>
-                                            <component
-                                                :is="containerComponent(container)"
-                                                :container="container"
-                                                :canEdit="canEdit"
-                                                :canAddElements="canAddElements"
-                                                :isTeacher="userIsTeacher"
-                                                class="cw-container-item"
-                                                ref="containers"
-                                                :class="{
-                                                    'cw-container-item-selected': keyboardSelected === container.id,
-                                                }"
-                                            />
-                                        </li>
+                                        <template #item="{element}">
+                                            <li class="cw-container-item-sortable">
+                                                <span
+                                                    :class="{ 'cw-sortable-handle-dragging': isDragging }"
+                                                    class="cw-sortable-handle"
+                                                    tabindex="0"
+                                                    role="option"
+                                                    aria-describedby="operation"
+                                                    :ref="'sortableHandle' + element.id"
+                                                    @keydown="keyHandler($event, element.id)"
+                                                ></span>
+                                                <component
+                                                    :is="containerComponent(element)"
+                                                    :container="element"
+                                                    :canEdit="canEdit"
+                                                    :canAddElements="canAddElements"
+                                                    :isTeacher="userIsTeacher"
+                                                    class="cw-container-item"
+                                                    ref="containers"
+                                                    :class="{
+                                                        'cw-container-item-selected': keyboardSelected === element.id,
+                                                    }"
+                                                />
+                                            </li>
+                                        </template>
                                     </draggable>
                                 </template>
                                 <studip-progress-indicator
@@ -495,6 +490,8 @@ export default {
 
     mixins: [CoursewareExport, colorMixin, wizardMixin, containerMixin],
 
+    emits: ['select'],
+
     data() {
         return {
             currentElement: '',
@@ -621,8 +618,8 @@ export default {
             textDelete.title = this.$gettext('Seite unwiderruflich löschen');
             textDelete.alert = this.$gettext('Möchten Sie die Seite wirklich löschen?');
             if (this.structuralElementLoaded) {
-                textDelete.alert = this.$gettextInterpolate(
-                    this.$gettext('Möchten Sie die Seite %{ pageTitle } und alle ihre Unterseiten wirklich löschen?'),
+                textDelete.alert = this.$gettext(
+                    'Möchten Sie die Seite %{ pageTitle } und alle ihre Unterseiten wirklich löschen?',
                     { pageTitle: this.structuralElement.attributes.title }
                 );
             }
@@ -681,11 +678,12 @@ export default {
                     return null;
                 }
                 const element = this.structuralElementById({ id: parentId });
-                if (element.relationships.parent.data === null && !this.showRootElement) {
-                    return null;
-                }
                 if (!element) {
                     console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`);
+                    return null;
+                }
+                if (element.relationships.parent.data === null && !this.showRootElement) {
+                    return null;
                 }
 
                 return element;
@@ -1082,12 +1080,10 @@ export default {
             return true;
         },
         callToActionTitleFeedback() {
-            return this.$gettextInterpolate(
-                this.$ngettext(
-                    '%{length} Anmerkung zur Seite (Nur für Nutzende mit Schreibrechten sichtbar)',
-                    '%{length} Anmerkungen zur Seite (Nur für Nutzende mit Schreibrechten sichtbar)',
-                    this.feedbackCounter
-                ),
+            return this.$ngettext(
+                '%{length} Anmerkung zur Seite (Nur für Nutzende mit Schreibrechten sichtbar)',
+                '%{length} Anmerkungen zur Seite (Nur für Nutzende mit Schreibrechten sichtbar)',
+                this.feedbackCounter,
                 { length: this.feedbackCounter }
             );
         },
@@ -1103,8 +1099,10 @@ export default {
             return this.comments?.length ?? 0;
         },
         callToActionTitleComments() {
-            return this.$gettextInterpolate(
-                this.$ngettext('%{length} Kommentar zur Seite', '%{length} Kommentare zur Seite', this.commentsCounter),
+            return this.$ngettext(
+                '%{length} Kommentar zur Seite',
+                '%{length} Kommentare zur Seite',
+                this.commentsCounter,
                 { length: this.commentsCounter }
             );
         },
@@ -1196,8 +1194,8 @@ export default {
                     await this.loadStructuralElement(this.currentId);
                     if (this.blockedByAnotherUser) {
                         this.companionInfo({
-                            info: this.$gettextInterpolate(
-                                this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Seite bearbeitet.'),
+                            info: this.$gettext(
+                                'Löschen nicht möglich, da %{blockingUserName} die Seite bearbeitet.',
                                 { blockingUserName: this.blockingUserName }
                             ),
                         });
@@ -1323,8 +1321,8 @@ export default {
             }
             if (this.blockedByAnotherUser) {
                 this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.'),
+                    info: this.$gettext(
+                        'Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.',
                         { blockingUserName: this.blockingUserName }
                     ),
                 });
@@ -1394,10 +1392,8 @@ export default {
                         this.keyboardSelected = containerId;
                         const container = this.containerById({ id: containerId });
                         const index = this.containerList.findIndex((c) => c.id === container.id);
-                        this.assistiveLive = this.$gettextInterpolate(
-                            this.$gettext(
-                                '%{containerTitle} Abschnitt ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.'
-                            ),
+                        this.assistiveLive = this.$gettext(
+                            '%{containerTitle} Abschnitt ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen.',
                             {
                                 containerTitle: container.attributes.title,
                                 pos: index + 1,
@@ -1429,10 +1425,8 @@ export default {
                 const container = this.containerById({ id: containerId });
                 const newPos = currentIndex - 1;
                 this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]);
-                this.assistiveLive = this.$gettextInterpolate(
-                    this.$gettext(
-                        '%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.'
-                    ),
+                this.assistiveLive = this.$gettext(
+                    '%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
                     {
                         containerTitle: container.attributes.title,
                         pos: newPos + 1,
@@ -1447,10 +1441,8 @@ export default {
                 const container = this.containerById({ id: containerId });
                 const newPos = currentIndex + 1;
                 this.containerList.splice(newPos, 0, this.containerList.splice(currentIndex, 1)[0]);
-                this.assistiveLive = this.$gettextInterpolate(
-                    this.$gettext(
-                        '%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.'
-                    ),
+                this.assistiveLive = this.$gettext(
+                    '%{containerTitle} Abschnitt. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
                     {
                         containerTitle: container.attributes.title,
                         pos: newPos + 1,
@@ -1462,8 +1454,8 @@ export default {
         abortKeyboardSorting(containerId) {
             const container = this.containerById({ id: containerId });
             this.keyboardSelected = null;
-            this.assistiveLive = this.$gettextInterpolate(
-                this.$gettext('%{containerTitle} Abschnitt, Neuordnung abgebrochen.'),
+            this.assistiveLive = this.$gettext(
+                '%{containerTitle} Abschnitt, Neuordnung abgebrochen.',
                 { containerTitle: container.attributes.title }
             );
             this.selectCurrent();
@@ -1472,10 +1464,8 @@ export default {
             const container = this.containerById({ id: containerId });
             const currentIndex = this.containerList.findIndex((container) => container.id === containerId);
             this.keyboardSelected = null;
-            this.assistiveLive = this.$gettextInterpolate(
-                this.$gettext(
-                    '%{containerTitle} Abschnitt, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.'
-                ),
+            this.assistiveLive = this.$gettext(
+                '%{containerTitle} Abschnitt, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.',
                 {
                     containerTitle: container.attributes.title,
                     pos: currentIndex + 1,
@@ -1653,18 +1643,24 @@ export default {
             },
             deep: true,
         },
-        containers() {
-            this.containerList = this.containers;
-            this.scrollToContainerHash();
-        },
-        containerList() {
-            if (this.keyboardSelected) {
-                this.$nextTick(() => {
-                    const selected = this.$refs['sortableHandle' + this.keyboardSelected][0];
-                    selected.focus();
-                    selected.scrollIntoView({ behavior: 'smooth', block: 'center' });
-                });
-            }
+        containers: {
+            handler() {
+                this.containerList = this.containers;
+                this.scrollToContainerHash();
+            },
+            deep: true
+        },
+        containerList: {
+            handler() {
+                if (this.keyboardSelected) {
+                    this.$nextTick(() => {
+                        const selected = this.$refs['sortableHandle' + this.keyboardSelected][0];
+                        selected.focus();
+                        selected.scrollIntoView({behavior: 'smooth', block: 'center'});
+                    });
+                }
+            },
+            deep: true
         },
         consumeMode(newState) {
             this.consumModeTrap = newState;
@@ -1693,7 +1689,7 @@ export default {
         window.addEventListener('scroll', this.handleDebouncedScroll);
     },
 
-    beforeDestroy() {
+    beforeUnmount() {
         if (this.handleDebouncedScroll) {
             window.removeEventListener('scroll', this.handleDebouncedScroll);
         }
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue
index e78377f4f20a70ed428b3fc6bae905cc4ca1c1e4..24a05f822f797bc1af0935c2579cefb5bd141022 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue
@@ -14,7 +14,7 @@
         </div>
         <div class="cw-structural-element-comment-create">
             <textarea v-model="createComment" :placeholder="placeHolder" spellcheck="true"></textarea>
-            <button class="button" @click="postComment"><translate>Senden</translate></button>
+            <button class="button" @click="postComment">{{ $gettext('Senden') }}</button>
         </div>
     </section>
 </template>
@@ -25,6 +25,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-structural-element-comments',
+    emits: ['hasComments'],
     components: {
         CoursewareTalkBubble,
     },
@@ -142,4 +143,4 @@ export default {
         }
     }
 }
-</script>
\ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
index bdbfa944cf68b951c57ee081353e7484e49d91d9..215e188f440503f357a68c21bc9d531e6a4ace49 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
@@ -83,7 +83,7 @@
                 <label>
                     {{ $gettext('Farbe') }}
                     <studip-select v-model="color" :options="colors" :reduce="(color) => color.class" label="class" name="color">
-                        <template #open-indicator="selectAttributes">
+                        <template #open-indicator="{ selectAttributes }">
                             <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
                         </template>
                         <template #no-options>
@@ -345,9 +345,10 @@ export default {
             await this.loadStructuralElementById({ id: newCreated.id });
             const newElement = this.structuralElementById({ id: newCreated.id });
             this.companionSuccess({
-                info: this.$gettextInterpolate(this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), {
-                    pageTitle: newElement.attributes.title,
-                }),
+                info: this.$gettext(
+                    'Die Seite %{ pageTitle } wurde erfolgreich angelegt.',
+                    { pageTitle: newElement.attributes.title }
+                ),
             });
 
             if (file && this.uploadFileError === '') {
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
index 8d0406cfc498295ef3861c7389d8bc5dc21d7faf..3eeba744882e7ddd6a56ed9c1ddeee1d88cab06c 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
@@ -73,7 +73,7 @@
                             :getOptionLabel="option => option.attributes.title"
                             v-model="selectedRange"
                         >
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"
                                     ><studip-icon shape="arr_1down" :size="10"
                                 /></span>
@@ -93,17 +93,16 @@
         <template v-slot:unit>
             <form class="default" @submit.prevent="">
                 <fieldset v-if="units.length !== 0" class="radiobutton-set">
-                    <template v-for="unit in units">
+                    <template v-for="unit in units" :key="unit.id">
                         <input
                             :id="'cw-element-copy-unit-' + unit.id"
                             type="radio"
                             v-model="selectedUnit"
                             :checked="unit.id === selectedUnitId"
                             :value="unit"
-                            :key="'radio-' + unit.id"
                             :aria-description="unit.element.attributes.title"
                         />
-                        <label :key="'label-' + unit.id" :for="'cw-element-copy-unit-' + unit.id">
+                        <label :for="'cw-element-copy-unit-' + unit.id">
                             <div class="icon"><studip-icon shape="courseware" :size="32"/></div>
                             <div class="text">{{ unit.element.attributes.title }}</div>
                             <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
@@ -148,7 +147,7 @@
                         :clearable="false"
                         label="class"
                     >
-                        <template #open-indicator="selectAttributes">
+                        <template #open-indicator="{ selectAttributes }">
                             <span v-bind="selectAttributes"
                                 ><studip-icon shape="arr_1down" :size="10"
                             /></span>
@@ -259,10 +258,10 @@ export default {
         },
         selectedUnitId() {
             return this.selectedUnit?.id;
-        }, 
+        },
         selectedUnitRootId() {
             return this.selectedUnit?.relationships?.['structural-element']?.data?.id;
-        }, 
+        },
         selectedElementTitle() {
             return this.selectedElement?.attributes?.title;
         },
@@ -386,17 +385,17 @@ export default {
             })
             .then( () => {
                 view.companionSuccess({
-                    info: view.$gettextInterpolate(
-                        view.$gettext('Die Seite %{ pageTitle } wurde erfolgreich kopiert.'),
-                        {pageTitle: view.selectedElementTitle}
+                    info: view.$gettext(
+                        'Die Seite %{ pageTitle } wurde erfolgreich kopiert.',
+                        { pageTitle: view.selectedElementTitle }
                     )
                 });
             })
             .catch(error => {
                 view.companionError({
-                    info: view.$gettextInterpolate(
-                        view.$gettext('Die Seite %{ pageTitle } konnte nicht kopiert werden.'),
-                        {pageTitle: view.selectedElementTitle}
+                    info: view.$gettext(
+                        'Die Seite %{ pageTitle } konnte nicht kopiert werden.',
+                        { pageTitle: view.selectedElementTitle }
                     )
                 });
             })
@@ -427,7 +426,7 @@ export default {
                 this.resetElementData();
                 this.wizardSlots[2].valid = false;
             }
-            
+
         },
         async selectedUnit(newUnit) {
             this.validateSelection();
@@ -438,7 +437,7 @@ export default {
             } else {
                 this.wizardSlots[1].valid = false;
             }
-            
+
         },
         selectedRange(newRid) {
             this.validateSelection();
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExport.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExport.vue
index 0058845caa0e3bba7e030bd776f28d1a639a65bd..754a4c7027239f5fff1f903fd39a26057e9981bd 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExport.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExport.vue
@@ -12,9 +12,10 @@
         <template v-slot:dialogContent>
             <div v-show="!exportRunning">
                 {{
-                    $gettextInterpolate($gettext('Hiermit exportieren Sie die Seite "%{ pageTitle }" als ZIP-Datei.'), {
-                        pageTitle: structuralElement.attributes.title,
-                    })
+                    $gettext(
+                        'Hiermit exportieren Sie die Seite "%{ pageTitle }" als ZIP-Datei.',
+                        { pageTitle: structuralElement.attributes.title }
+                    )
                 }}
                 <div class="cw-element-export">
                     <label>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportPdf.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportPdf.vue
index 7cb88d9bf709577839d45a9f0f3043dd7985630e..e486c1b5c83f22c3309d4a17c0dfc22580031ba8 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportPdf.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogExportPdf.vue
@@ -11,9 +11,10 @@
     >
         <template v-slot:dialogContent>
             {{
-                $gettextInterpolate($gettext('Hiermit exportieren Sie die Seite "%{ pageTitle }" als PDF-Datei.'), {
-                    pageTitle: structuralElement.attributes.title,
-                })
+                $gettext(
+                    'Hiermit exportieren Sie die Seite "%{ pageTitle }" als PDF-Datei.',
+                    { pageTitle: structuralElement.attributes.title }
+                )
             }}
             <div class="cw-element-export">
                 <label>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogInfo.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogInfo.vue
index fcc5a8796be8c0c0f4acea243d1bc6f40c0a8175..5fda85d1f4514326dae32dd810d623a19bb0eacc 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogInfo.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogInfo.vue
@@ -6,30 +6,32 @@
     >
         <template v-slot:dialogContent>
             <table class="cw-structural-element-info">
-                <tr>
-                    <td>{{ $gettext('Titel') }}:</td>
-                    <td>{{ structuralElement.attributes.title }}</td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Beschreibung') }}:</td>
-                    <td>{{ structuralElement.attributes.payload.description }}</td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Seite wurde erstellt von') }}:</td>
-                    <td>{{ ownerName }}</td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Seite wurde erstellt am') }}:</td>
-                    <td><iso-date :date="structuralElement.attributes.mkdate" /></td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Zuletzt bearbeitet von') }}:</td>
-                    <td>{{ editorName }}</td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Zuletzt bearbeitet am') }}:</td>
-                    <td><iso-date :date="structuralElement.attributes.chdate" /></td>
-                </tr>
+                <tbody>
+                    <tr>
+                        <td>{{ $gettext('Titel') }}:</td>
+                        <td>{{ structuralElement.attributes.title }}</td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Beschreibung') }}:</td>
+                        <td>{{ structuralElement.attributes.payload.description }}</td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Seite wurde erstellt von') }}:</td>
+                        <td>{{ ownerName }}</td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Seite wurde erstellt am') }}:</td>
+                        <td><iso-date :date="structuralElement.attributes.mkdate" /></td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Zuletzt bearbeitet von') }}:</td>
+                        <td>{{ editorName }}</td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Zuletzt bearbeitet am') }}:</td>
+                        <td><iso-date :date="structuralElement.attributes.chdate" /></td>
+                    </tr>
+                </tbody>
             </table>
         </template>
     </studip-dialog>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogLink.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogLink.vue
index 2029d5681f81dfed83c58d8e83b6f1e148cafd2a..7d7e6ccf438426bc6bfe797674f1a8ab41230e19 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogLink.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogLink.vue
@@ -12,16 +12,15 @@
         <template v-slot:unit>
             <form v-if="!loadingUnits" class="default" @submit.prevent="">
                 <fieldset v-if="hasUnits" class="radiobutton-set">
-                    <template v-for="unit in units">
+                    <template v-for="unit in units" :key="unit.id">
                         <input
                             :id="'cw-element-link-unit-' + unit.id"
                             type="radio"
                             :checked="unit.id === selectedUnitId"
                             :value="unit.id"
-                            :key="'radio-' + unit.id"
                             :aria-description="unit.element.attributes.title"
                         />
-                        <label @click="selectedUnit = unit" :key="'label-' + unit.id" :for="'cw-element-link-unit-' + unit.id">
+                        <label @click="selectedUnit = unit" :for="'cw-element-link-unit-' + unit.id">
                             <div class="icon"><studip-icon shape="courseware" :size="32"/></div>
                             <div class="text">{{ unit.element.attributes.title }}</div>
                             <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
@@ -35,7 +34,7 @@
                     :msgCompanion="$gettext('Es konnte leider kein Lernmaterial gefunden werden. Bitte erstellen Sie unter Arbeitsplatz/Courseware ein Lernmaterial.')"
                 />
             </form>
-            <studip-progress-indicator 
+            <studip-progress-indicator
                 v-else
                 :description="$gettext('Lade Lernmaterialien…')"
             />
@@ -75,9 +74,9 @@ export default {
     data() {
         return {
             wizardSlots: [
-                {id: 1, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware', 
+                {id: 1, valid: false, name: 'unit', title: this.$gettext('Lernmaterial'), icon: 'courseware',
                 description: this.$gettext('Wählen Sie das Lernmaterial aus, in dem sich der zu verknüpfende Lerninhalt befindet. Die Lerninhalte, die verknüpft werden können, müssen unter Arbeitsplatz/Courseware vorher erstellt werden.')},
-                {id: 2, valid: false, name: 'element', title: this.$gettext('Seite'), icon: 'content2', 
+                {id: 2, valid: false, name: 'element', title: this.$gettext('Seite'), icon: 'content2',
                 description: this.$gettext('Wählen Sie die zu verknüpfende Seite aus. Um Unterseiten anzuzeigen, klicken Sie auf den Seitennamen. Mit einem weiteren Klick werden die Unterseiten wieder zugeklappt.')},            ],
             loadingUnits: false,
             selectedUnit: null,
@@ -110,10 +109,10 @@ export default {
         },
         selectedUnitId() {
             return this.selectedUnit?.id;
-        }, 
+        },
         selectedUnitRootId() {
             return this.selectedUnit?.relationships?.['structural-element']?.data?.id;
-        }, 
+        },
         selectedElementTitle() {
             return this.selectedElement?.attributes?.title;
         },
@@ -176,16 +175,16 @@ export default {
                 })
                 .then( () => {
                     view.companionSuccess({
-                        info: view.$gettextInterpolate(
-                            view.$gettext('Die Seite %{ pageTitle } wurde erfolgreich verknüpft.'),
+                        info: view.$gettext(
+                            'Die Seite %{ pageTitle } wurde erfolgreich verknüpft.',
                             { pageTitle: view.selectedElementTitle }
                         )
                     });
                 })
                 .catch( () => {
                     view.companionError({
-                        info: view.$gettextInterpolate(
-                            view.$gettext('Die Seite %{ pageTitle } konnte nicht verknüpft werden.'),
+                        info: view.$gettext(
+                            'Die Seite %{ pageTitle } konnte nicht verknüpft werden.',
                             { pageTitle: view.selectedElementTitle }
                         )
                     });
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogOerSuggest.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogOerSuggest.vue
index 30a57d441289dd3e39e3f022d08330c87f163548..8f1ba0cb28f8189b5008ae7045d7f21cb2e35101 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogOerSuggest.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogOerSuggest.vue
@@ -13,23 +13,23 @@
         <template v-slot:dialogContent>
             <p>
                 {{
-                    $gettextInterpolate(
-                        $gettext(
-                            'Der folgende Lerninhalt wird %{ ownerName } zur Veröffentlichung im OER Campus vorgeschlagen:'
-                        ),
+                    $gettext(
+                        'Der folgende Lerninhalt wird %{ ownerName } zur Veröffentlichung im OER Campus vorgeschlagen:',
                         { ownerName: ownerName }
                     )
                 }}
             </p>
             <table class="cw-structural-element-info">
-                <tr>
-                    <td>{{ $gettext('Titel') }}:</td>
-                    <td>{{ structuralElement.attributes.title }}</td>
-                </tr>
-                <tr>
-                    <td>{{ $gettext('Beschreibung') }}:</td>
-                    <td>{{ structuralElement.attributes.payload.description }}</td>
-                </tr>
+                <tbody>
+                    <tr>
+                        <td>{{ $gettext('Titel') }}:</td>
+                        <td>{{ structuralElement.attributes.title }}</td>
+                    </tr>
+                    <tr>
+                        <td>{{ $gettext('Beschreibung') }}:</td>
+                        <td>{{ structuralElement.attributes.payload.description }}</td>
+                    </tr>
+                </tbody>
             </table>
             <form class="default" @submit.prevent="">
                 <label>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue
index 930cd909ae54774b50a58c21705700f029047e3c..fbec044a02de884b1e71133f7a915a0608428f39 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue
@@ -261,6 +261,7 @@ import axios from 'axios';
 import { mapActions, mapGetters } from 'vuex';
 export default {
     name: 'courseware-structural-element-dialog-permissions',
+    emits: ['close', 'store'],
     components: {
         CoursewareCompanionBox,
         Datepicker,
@@ -437,10 +438,8 @@ export default {
             await this.loadStructuralElement(this.structuralElement.id);
             if (this.blockedByAnotherUser) {
                 this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext(
-                            'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'
-                        ),
+                    info: this.$gettext(
+                        'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.',
                         { blockingUserName: this.blockingUserName }
                     ),
                 });
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogSettings.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogSettings.vue
index dd81d541a86804169406729b739db05759194d40..1481926e15ad65659b0069ac18facfd29dc01f54 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogSettings.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogSettings.vue
@@ -112,7 +112,9 @@
                                 :alt="$gettext('Vorschaubild')"
                             />
                             <label>
-                                <button class="button" @click="deleteImage" v-translate>Bild löschen</button>
+                                <button class="button" @click="deleteImage">
+                                    {{ $gettext('Bild löschen') }}
+                                </button>
                             </label>
                         </template>
 
@@ -167,6 +169,7 @@ import StockImageSelector from '../../stock-images/SelectorDialog.vue';
 import { mapActions, mapGetters } from 'vuex';
 export default {
     name: 'courseware-structural-element-dialog-settings',
+    emits: ['close', 'store'],
     mixins: [colorMixin, wizardMixin],
     components: {
         CoursewareContentPermissions,
@@ -280,10 +283,8 @@ export default {
             await this.loadStructuralElement(this.currentElement.id);
             if (this.blockedByAnotherUser) {
                 this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext(
-                            'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'
-                        ),
+                    info: this.$gettext(
+                        'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.',
                         { blockingUserName: this.blockingUserName }
                     ),
                 });
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue
index 8d9f95863d98636ecb59d76af8a28708f6c333f9..a965e84957eed3a53c5701c1989e1884352e533c 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue
@@ -34,6 +34,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-structural-element-feedback',
+    emits: ['hasFeedback'],
     components: {
         CoursewareCompanionBox,
         CoursewareTalkBubble,
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementPermissions.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementPermissions.vue
index eead7ab856f52686c9afdff102afbef0d102e00d..49650d0bfb02e56798d7be68ab8dff0c8daaf4c7 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementPermissions.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementPermissions.vue
@@ -2,16 +2,16 @@
     <div class="cw-element-permissions">
         <label>
             <input type="checkbox" class="default" v-model="userPermsReadAll" />
-            <translate>Alle Teilnehmenden haben Leserechte</translate>
+            {{ $gettext('Alle Teilnehmenden haben Leserechte') }}
         </label>
         <label>
             <input type="checkbox" class="default" v-model="userPermsWriteAll" />
-            <translate>Alle Teilnehmenden haben Schreibrechte</translate>
+            {{ $gettext('Alle Teilnehmenden haben Schreibrechte') }}
         </label>
 
         <table class="default" v-if="autor_members.length">
             <caption>
-                <translate>Studierende</translate>
+                {{ $gettext('Studierende') }}
             </caption>
             <colgroup>
                 <col style="width:1%" />
@@ -23,10 +23,10 @@
             <thead>
                 <tr>
                     <th><input type="checkbox" v-model="bulkSelectAutorRead" @click="handleBulkSelectRead($event, 'autor')"/></th>
-                    <th><translate>Lesen</translate></th>
+                    <th>{{ $gettext('Lesen') }}</th>
                     <th><input type="checkbox" v-model="bulkSelectAutorWrite" @click="handleBulkSelectWrite($event)"/></th>
-                    <th><translate>Lesen und Schreiben</translate></th>
-                    <th><translate>Name</translate></th>
+                    <th>{{ $gettext('Lesen und Schreiben') }}</th>
+                    <th>{{ $gettext('Name') }}</th>
                 </tr>
             </thead>
             <tbody>
@@ -71,7 +71,7 @@
 
         <table class="default" v-if="user_members.length">
             <caption>
-                <translate>Leser/-innen</translate>
+                {{ $gettext('Leser/-innen') }}
             </caption>
             <colgroup>
                 <col style="width:1%" />
@@ -81,8 +81,8 @@
             <thead>
                 <tr>
                     <th><input type="checkbox" v-model="bulkSelectUserRead" @click="handleBulkSelectRead($event, 'user')"/></th>
-                    <th><translate>Lesen</translate></th>
-                    <th><translate>Name</translate></th>
+                    <th>{{ $gettext('Lesen') }}</th>
+                    <th>{{ $gettext('Name') }}</th>
                 </tr>
             </thead>
             <tbody>
@@ -119,7 +119,7 @@
 
         <table class="default" v-if="groups.length">
             <caption>
-                <translate>Gruppen</translate>
+                {{ $gettext('Gruppen') }}
             </caption>
             <colgroup>
                 <col style="width:20%" />
@@ -128,9 +128,9 @@
             </colgroup>
             <thead>
                 <tr>
-                    <th><translate>Lesen</translate></th>
-                    <th><translate>Lesen und Schreiben</translate></th>
-                    <th><translate>Name</translate></th>
+                    <th>{{ $gettext('Lesen') }}</th>
+                    <th>{{ $gettext('Lesen und Schreiben') }}</th>
+                    <th>{{ $gettext('Name') }}</th>
                 </tr>
             </thead>
             <tbody>
@@ -169,6 +169,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-structural-element-permissions',
+    emits: ['updateReadApproval', 'updateWriteApproval'],
     props: {
         element: Object,
     },
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelector.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelector.vue
index 0e8acda3bbcecc526859cc5a9638a977c4cd5e06..67c02e6dd71ab609b930b890c7587635f27f0f26 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelector.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelector.vue
@@ -27,11 +27,13 @@ export default {
         CoursewareStructuralElementSelectorItem
     },
     model: {
-        prop: 'element'
+         prop: 'modelValue',
+         event: 'update:modelValue'
     },
     props: {
-        element: {
-            type: Object
+        modelValue: {
+            type: Object,
+            required: true
         },
         rootId: {
             type: String,
@@ -75,7 +77,7 @@ export default {
                 .filter(Boolean);
         },
         selectedId() {
-            return this.element?.id ?? '';
+            return this.modelValue?.id ?? '';
         },
         targetElement() {
             return this.structuralElementById({ id: this.targetId });
@@ -116,7 +118,7 @@ export default {
             companionSuccess: 'companionSuccess',
         }),
         handleInput(id) {
-            this.$emit('input', this.structuralElementById({ id }));
+            this.$emit('update:modelValue', this.structuralElementById({ id }));
             this.focusedElementId = id;
         },
         handleFocus(id) {
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelectorItem.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelectorItem.vue
index f6b3cf6a66ee2c3b1b79bfd37ec783b9f06bddaa..c268e5170d62d4b151f80cb169d4c3b261197b7e 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelectorItem.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementSelectorItem.vue
@@ -14,7 +14,7 @@
                 <studip-icon v-if="selected" shape="radiobutton-checked" />
                 <studip-icon v-else shape="radiobutton-unchecked" />
             </template>
-            <studip-icon v-else shape="decline" role="inactive" />    
+            <studip-icon v-else shape="decline" role="inactive" />
         </span>
         <template v-if="hasChildren">
             <a href="#" :aria-expanded="isOpen ? 'true' : 'false'" @click.prevent="toggleChildrenVisibility">
@@ -58,6 +58,7 @@ import { mapActions, mapGetters } from 'vuex'
 
 export default {
     name: 'courseware-structural-element-selector-item',
+    emits: ['focus', 'input', 'selectable'],
     props: {
         element: {
             type: Object
@@ -75,7 +76,7 @@ export default {
         rootId: {
             type: String,
             required: true
-        },  
+        },
         validateAncestors: {
             type: Boolean,
             default: false
@@ -171,7 +172,7 @@ export default {
                         return element.id;
                 } else {
                     return this.siblings[index].id;
-                } 
+                }
             } else {
                 return this.$parent.element.id;
             }
@@ -229,7 +230,7 @@ export default {
                 case 38: // arrow up
                     event.preventDefault();
                     if (this.previousElementId !== null) {
-                        this.$emit('focus', this.previousElementId);    
+                        this.$emit('focus', this.previousElementId);
                     }
                     break;
                 case 39: // arrow right
diff --git a/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue b/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue
index 6426b7c83100d3d10a396c0b9b04452f745f0137..2842aced12d79154b5e959b2286fbf7fd251c319 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue
@@ -82,36 +82,40 @@ export default {
 };
 </script>
 <style scoped lang="scss">
-.cw-tools-contents-header {
-    display: flex;
-    flex-direction: row;
-    height: 100px;
-    margin-top: 8px;
-    .cw-tools-contents-header-image {
-        height: 100px;
-        width: 150px;
-        min-width: 150px;
-        background-size: 100% auto;
-        background-repeat: no-repeat;
-        background-position: center;
-        background-color: var(--content-color-20);
-    }
+.cw-tools-contents {
+    padding: 0 5px;
 
-    .cw-tools-contents-header-details {
-        margin: 0 8px;
-        display: -webkit-box;
-        overflow: hidden;
+    .cw-tools-contents-header {
+        display: flex;
+        flex-direction: row;
         height: 100px;
-        -webkit-line-clamp: 5;
-        -webkit-box-orient: vertical;
-        header {
-            margin: 0 0 6px 0;
-            font-size: 16px;
-            line-height: 16px;
+        margin-top: 8px;
+        .cw-tools-contents-header-image {
+            height: 100px;
+            width: 150px;
+            min-width: 150px;
+            background-size: 100% auto;
+            background-repeat: no-repeat;
+            background-position: center;
+            background-color: var(--content-color-20);
         }
-        p {
-            margin: 0;
-            color: var(--black);
+
+        .cw-tools-contents-header-details {
+            margin: 0 8px;
+            display: -webkit-box;
+            overflow: hidden;
+            height: 100px;
+            -webkit-line-clamp: 5;
+            -webkit-box-orient: vertical;
+            header {
+                margin: 0 0 6px 0;
+                font-size: 16px;
+                line-height: 16px;
+            }
+            p {
+                margin: 0;
+                color: var(--black);
+            }
         }
     }
 }
diff --git a/resources/vue/components/courseware/structural-element/CoursewareTree.vue b/resources/vue/components/courseware/structural-element/CoursewareTree.vue
index bd25c6b66900169db242632d07dd355993c27231..915f2e340cc009dc79e99dd48824063bd9920138 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareTree.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareTree.vue
@@ -1,11 +1,9 @@
 <template>
     <div class="cw-tree" ref="tree">
-        <template>
-                <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
-                <span id="operation" class="assistive-text">
-                    {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}}
-                </span>
-        </template>
+        <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span>
+        <span id="operation" class="assistive-text">
+            {{ $gettext('Drücken Sie die Leertaste, um neu anzuordnen.') }}
+        </span>
         <ol v-if="!processing" class="cw-tree-root-list" role="listbox">
             <courseware-tree-item
                 class="cw-tree-item"
@@ -21,7 +19,7 @@
             <courseware-tree-item-adder v-if="canEditRoot" :parentId="rootElement.id"/>
         </ol>
         <studip-progress-indicator
-            v-else 
+            v-else
             :description="$gettext('Vorgang wird bearbeitet...')"
         />
     </div>
@@ -35,7 +33,7 @@ import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
-    components: { 
+    components: {
         CoursewareTreeItem,
         CoursewareTreeItemAdder,
         StudipProgressIndicator
@@ -55,7 +53,7 @@ export default {
             relatedStructuralElement: 'courseware-structural-elements/related',
             structuralElementById: 'courseware-structural-elements/byId',
             childrenById: 'courseware-structure/children',
-            viewMode: 'viewMode',   
+            viewMode: 'viewMode',
             structuralElements: 'courseware-structural-elements/all',
             assistiveLive: 'assistiveLiveContents',
         }),
@@ -188,9 +186,14 @@ export default {
                 element.nestedChildren[newPos].sortArray = element.nestedChildren.map(c => {return {id: c.id, type: 'courseware-structural-elements'}});
                 element.nestedChildren[newPos].moveDirection = data.direction;
 
-                const assistiveLive = this.$gettextInterpolate(
-                    this.$gettext('%{elementTitle} eine Ebene nach oben bewegt. Übergeordnete Seite: %{parentTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}'), 
-                    { elementTitle: data.element.attributes.title, parentTitle: element.attributes.title, pos: newPos + 1, listLength: element.nestedChildren[newPos].sortArray.length }
+                const assistiveLive = this.$gettext(
+                    '%{elementTitle} eine Ebene nach oben bewegt. Übergeordnete Seite: %{parentTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}',
+                    {
+                        elementTitle: data.element.attributes.title,
+                        parentTitle: element.attributes.title,
+                        pos: newPos + 1,
+                        listLength: element.nestedChildren[newPos].sortArray.length
+                    }
                 );
                 this.setAssistiveLiveContents(assistiveLive);
             }
@@ -206,9 +209,13 @@ export default {
                         element.nestedChildren[newPos].sortArray = element.nestedChildren.map(c => {return {id: c.id, type: 'courseware-structural-elements'}});
                         element.nestedChildren[newPos].moveDirection = data.direction;
 
-                        const assistiveLive = this.$gettextInterpolate(
-                            this.$gettext('%{elementTitle} bewegt. Aktuelle Position in der Liste: %{pos} von %{listLength}'), 
-                            { elementTitle: data.element.attributes.title, pos: newPos + 1, listLength: element.nestedChildren[newPos].sortArray.length }
+                        const assistiveLive = this.$gettext(
+                            '%{elementTitle} bewegt. Aktuelle Position in der Liste: %{pos} von %{listLength}',
+                            {
+                                elementTitle: data.element.attributes.title,
+                                pos: newPos + 1,
+                                listLength: element.nestedChildren[newPos].sortArray.length
+                            }
                         );
                         this.setAssistiveLiveContents(assistiveLive);
                     }
@@ -225,9 +232,14 @@ export default {
                         element.nestedChildren[newParentIndex].nestedChildren[newPos].newParentId = parseInt(newParentId);
                         element.nestedChildren[newParentIndex].nestedChildren[newPos].sortArray = element.nestedChildren[newParentIndex].nestedChildren.map(c => {return {id: c.id, type: 'courseware-structural-elements'}});
 
-                        const assistiveLive = this.$gettextInterpolate(
-                            this.$gettext('%{elementTitle} eine Ebene nach unten bewegt. Übergeordnete Seite: %{parentTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}'), 
-                            { elementTitle: data.element.attributes.title, parentTitle: element.nestedChildren[newParentIndex].attributes.title, pos: newPos + 1, listLength: element.nestedChildren[newParentIndex].nestedChildren[newPos].sortArray.length }
+                        const assistiveLive = this.$gettext(
+                            '%{elementTitle} eine Ebene nach unten bewegt. Übergeordnete Seite: %{parentTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}',
+                            {
+                                elementTitle: data.element.attributes.title,
+                                parentTitle: element.nestedChildren[newParentIndex].attributes.title,
+                                pos: newPos + 1,
+                                listLength: element.nestedChildren[newParentIndex].nestedChildren[newPos].sortArray.length
+                            }
                         );
                         this.setAssistiveLiveContents(assistiveLive);
                     }
diff --git a/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue b/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue
index 6fe7da87d39c4e872eb5090d8fb1f63f21ffbff4..d9d7b3c26c8572b4fbc9db412cc15577fba6d18c 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareTreeItem.vue
@@ -54,7 +54,7 @@
                     <span
                         v-else
                         class="cw-tree-item-sequential cw-tree-item-sequential-percentage"
-                        :title="$gettextInterpolate($gettext('Fortschritt: %{progress}%'), { progress: itemProgress })"
+                        :title="$gettext('Fortschritt: %{progress}%', { progress: itemProgress })"
                     >
                         {{ itemProgress }} %
                     </span>
@@ -79,35 +79,36 @@
         </ol>
         <draggable
             v-if="canEdit"
+            v-bind="dragOptions"
             :class="{ 'cw-tree-chapter-list-empty': nestedChildren.length === 0 }"
             tag="ol"
             :component-data="draggableData"
             class="cw-tree-draggable-list"
             handle=".cw-sortable-handle"
-            v-bind="dragOptions"
             :elementId="element.id"
             :list="nestedChildren"
             :group="{ name: 'g1' }"
             @end="endDrag"
+            item-key="id"
         >
-            <courseware-tree-item
-                v-for="el in nestedChildren"
-                :key="el.id"
-                :element="el"
-                :currentElement="currentElement"
-                :depth="depth + 1"
-                :newPos="el.newPos"
-                :newParentId="el.newParentId"
-                :siblingCount="nestedChildren.length"
-                class="cw-tree-item"
-                :elementid="el.id"
-                @sort="sort"
-                @moveItemUp="moveItemUp"
-                @moveItemDown="moveItemDown"
-                @moveItemPrevLevel="moveItemPrevLevel"
-                @moveItemNextLevel="moveItemNextLevel"
-                @childrenUpdated="$emit('childrenUpdated')"
-            />
+            <template #item="{element}">
+                <courseware-tree-item
+                    :element="element"
+                    :currentElement="currentElement"
+                    :depth="depth + 1"
+                    :newPos="element.newPos"
+                    :newParentId="element.newParentId"
+                    :siblingCount="nestedChildren.length"
+                    class="cw-tree-item"
+                    :elementid="element.id"
+                    @sort="sort"
+                    @moveItemUp="moveItemUp"
+                    @moveItemDown="moveItemDown"
+                    @moveItemPrevLevel="moveItemPrevLevel"
+                    @moveItemNextLevel="moveItemNextLevel"
+                    @childrenUpdated="$emit('childrenUpdated')"
+                />
+            </template>
         </draggable>
         <ol v-if="canEdit && isFirstLevel" class="cw-tree-adder-list">
             <courseware-tree-item-adder :parentId="element.id" />
@@ -124,6 +125,14 @@ import { mapGetters, mapActions } from 'vuex';
 
 export default {
     name: 'courseware-tree-item',
+    emits: [
+        'childrenUpdated',
+        'moveItemDown',
+        'moveItemNextLevel',
+        'moveItemPrevLevel',
+        'moveItemUp',
+        'sort',
+    ],
     components: {
         CoursewareTreeItemAdder,
         CoursewareTreeItemUpdater,
@@ -194,9 +203,10 @@ export default {
             return {
                 attrs: {
                     role: 'listbox',
-                    ['aria-label']: this.$gettextInterpolate(this.$gettext('Unterseiten von %{elementName}'), {
-                        elementName: this.element.attributes?.title,
-                    }),
+                    ['aria-label']: this.$gettext(
+                        'Unterseiten von %{elementName}',
+                        { elementName: this.element.attributes?.title }
+                    ),
                 },
             };
         },
@@ -247,23 +257,28 @@ export default {
                         break;
                     case 'users': {
                         const users = this.element.attributes['visible-approval'].length;
-                        persons = this.$gettextInterpolate(
-                            this.$ngettext('einen Studierenden', '%{count} Studierende', users),
+                        persons = this.$ngettext(
+                            'einen Studierenden',
+                            '%{count} Studierende',
+                            users,
                             { count: users }
                         );
                         break;
                     }
                     case 'groups': {
                         const groups = this.element.attributes['visible-approval'].length;
-                        persons = this.$gettextInterpolate(this.$ngettext('eine Gruppe', '%{count} Gruppen', groups), {
-                            count: groups,
-                        });
+                        persons = this.$ngettext(
+                            'eine Gruppe',
+                            '%{count} Gruppen',
+                            groups,
+                            { count: groups }
+                        );
                         break;
                     }
                 }
 
-                return this.$gettextInterpolate(
-                    this.$gettext('Diese Seite ist vom %{start} bis zum %{end} für %{persons} sichtbar'),
+                return this.$gettext(
+                    'Diese Seite ist vom %{start} bis zum %{end} für %{persons} sichtbar',
                     { start: startDate, end: endDate, persons: persons }
                 );
             }
@@ -291,21 +306,28 @@ export default {
             const count = writableApproval.length;
             switch (this.element.attributes?.['permission-type']) {
                 case 'users':
-                    persons = this.$gettextInterpolate(this.$ngettext('einem Studierendem', '%{count} Studierenden', count), {
-                        count: count,
-                    });
+                    persons = this.$ngettext(
+                        'einem Studierendem',
+                        '%{count} Studierenden',
+                        count,
+                        { count: count }
+                    );
                     break;
                 case 'groups':
-                    persons = this.$gettextInterpolate(this.$ngettext('einer Gruppe', '%{count} Gruppen', count), {
-                        count: count,
-                    });
+                    persons =
+                        this.$ngettext(
+                            'einer Gruppe',
+                            '%{count} Gruppen',
+                            count,
+                            { count: count }
+                        );
                     break;
             }
 
-            return this.$gettextInterpolate(
-                    this.$gettext('Diese Seite kann von %{persons} bearbeitet werden'),
-                    { persons: persons }
-                );
+            return this.$gettext(
+                'Diese Seite kann von %{persons} bearbeitet werden',
+                { persons: persons }
+            );
         },
         hasNoReadApproval() {
             if (this.context.type === 'users' || this.element.attributes?.['visible-all']) {
@@ -318,10 +340,10 @@ export default {
                 case 'users':
                     return this.autorMembersCount !== visibleApproval.length;
                 case 'groups':
-                    return  this.statusGroupsCount !== visibleApproval.length; 
+                    return  this.statusGroupsCount !== visibleApproval.length;
             }
 
-            return true;            
+            return true;
         },
         cantReadFlagTitle() {
             if (!this.hasNoReadApproval) {
@@ -335,22 +357,29 @@ export default {
                     return this.$gettext('Diese Seite kann von Studierenden nicht gesehen werden');
                 case 'users':
                     count = this.autorMembersCount - visibleApproval.length;
-                    persons = this.$gettextInterpolate(this.$ngettext('einem Studierendem', '%{count} Studierenden', count), {
-                        count: count,
-                    });
+                    persons = this.$ngettext(
+                        'einem Studierendem',
+                        '%{count} Studierenden',
+                        count,
+                        { count: count }
+                    );
                     break;
                 case 'groups':
                     count = this.statusGroupsCount - visibleApproval.length
-                    persons = this.$gettextInterpolate(this.$ngettext('einer Gruppe', '%{count} Gruppen', count), {
-                        count: count,
-                    });
+                    persons =
+                        this.$ngettext(
+                            'einer Gruppe',
+                            '%{count} Gruppen',
+                            count,
+                            { count: count }
+                        );
                     break;
             }
 
-            return this.$gettextInterpolate(
-                    this.$gettext('Diese Seite kann von %{persons} nicht gesehen werden'),
-                    { persons: persons }
-                );
+            return this.$this.$gettext(
+                'Diese Seite kann von %{persons} nicht gesehen werden',
+                { persons: persons }
+            );
         },
         hasPurposeClass() {
             return this.purposeClass !== '';
@@ -450,7 +479,7 @@ export default {
                 newPos: e.newIndex,
                 oldPos: e.oldIndex,
                 oldParent: e.item._underlying_vm_.relationships.parent.data.id,
-                newParent: e.to.__vue__.$attrs.elementId,
+                newParent: e.to.__vnode.ctx.attrs.elementId,
                 sortArray: sortArray,
             };
 
@@ -475,10 +504,8 @@ export default {
                         this.storeKeyboardSorting();
                     } else {
                         this.keyboardSelected = true;
-                        const assistiveLive = this.$gettextInterpolate(
-                            this.$gettext(
-                                '%{elementTitle} ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen. Mit Pfeiltasten links und rechts kann die Position in der Hierarchie verändert werden.'
-                            ),
+                        const assistiveLive = this.$gettext(
+                            '%{elementTitle} ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste zum Ablegen, die Escape-Taste zum Abbrechen. Mit Pfeiltasten links und rechts kann die Position in der Hierarchie verändert werden.',
                             {
                                 elementTitle: this.element.attributes.title,
                                 pos: this.element.attributes.position + 1,
@@ -537,9 +564,10 @@ export default {
         },
         abortKeyboardSorting() {
             this.$emit('childrenUpdated');
-            const assistiveLive = this.$gettextInterpolate(this.$gettext('%{elementTitle}. Neuordnung abgebrochen.'), {
-                elementTitle: this.element.attributes.title,
-            });
+            const assistiveLive = this.$gettext(
+                '%{elementTitle}. Neuordnung abgebrochen.',
+                { elementTitle: this.element.attributes.title }
+            );
             this.setAssistiveLiveContents(assistiveLive);
             this.$nextTick(() => {
                 this.keyboardSelected = false;
@@ -557,8 +585,8 @@ export default {
             this.keyboardSelected = false;
 
             if (data.newParent === undefined || data.newPos === undefined) {
-                const assistiveLive = this.$gettextInterpolate(
-                    this.$gettext('%{elementTitle}. Neuordnung nicht möglich.'),
+                const assistiveLive = this.$gettext(
+                    '%{elementTitle}. Neuordnung nicht möglich.',
                     { elementTitle: this.element.attributes.title }
                 );
                 this.setAssistiveLiveContents(assistiveLive);
@@ -566,17 +594,21 @@ export default {
             }
 
             if (data.oldParent === data.newParent && data.oldPos === data.newPos) {
-                const assistiveLive = this.$gettextInterpolate(
-                    this.$gettext('%{elementTitle}. Neuordnung abgebrochen.'),
+                const assistiveLive = this.$gettext(
+                    '%{elementTitle}. Neuordnung abgebrochen.',
                     { elementTitle: this.element.attributes.title }
                 );
                 this.setAssistiveLiveContents(assistiveLive);
                 return;
             }
             this.$emit('sort', data);
-            const assistiveLive = this.$gettextInterpolate(
-                this.$gettext('%{elementTitle}, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.'),
-                { elementTitle: this.element.attributes.title, pos: data.newPos + 1, listLength: this.siblingCount }
+            const assistiveLive = this.$gettext(
+                '%{elementTitle}, abgelegt. Entgültige Position in der Liste: %{pos} von %{listLength}.',
+                {
+                    elementTitle: this.element.attributes.title,
+                    pos: data.newPos + 1,
+                    listLength: this.siblingCount
+                }
             );
             this.setAssistiveLiveContents(assistiveLive);
         },
diff --git a/resources/vue/components/courseware/structural-element/CoursewareTreeItemUpdater.vue b/resources/vue/components/courseware/structural-element/CoursewareTreeItemUpdater.vue
index 78690c9684156f719020a47e16d4fb3b317bab0a..006def158b85940afae430135643363c7265177b 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareTreeItemUpdater.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareTreeItemUpdater.vue
@@ -11,6 +11,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-tree-item-adder',
+    emits: ['childrenUpdated', 'close'],
     props: {
         structuralElement: {
             type: Object,
@@ -61,10 +62,8 @@ export default {
                 await this.loadUser({ id: blockerData.id });
                 const blocker = this.userById({ id: blockerData.id });
                 this.companionWarning({
-                    info: this.$gettextInterpolate(
-                        this.$gettext(
-                            'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'
-                        ),
+                    info: this.$gettext(
+                        'Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.',
                         { blockingUserName: blocker.attributes['formatted-name'] }
                     ),
                 });
diff --git a/resources/vue/components/courseware/structural-element/CoursewareTreeUnit.vue b/resources/vue/components/courseware/structural-element/CoursewareTreeUnit.vue
index 9e5482a22a6ba6d5a4ffaa68d84b4add05874b53..167bb11dcf0b89b69804d903d9bf428560eed6d3 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareTreeUnit.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareTreeUnit.vue
@@ -31,6 +31,7 @@ import { mapActions, mapGetters } from 'vuex';
 export default {
     name: 'CoursewareTreeUnit',
     mixins: [colorMixin],
+    emits: ['removeUnitLink'],
     components: {
         StudipIdentImage,
     },
diff --git a/resources/vue/components/courseware/tasks/AddFeedbackDialog.vue b/resources/vue/components/courseware/tasks/AddFeedbackDialog.vue
index 2d7c28104e9f7bb9be195f6016bbad0ea44bb6bc..97c230bc3df135150860b7e7c356cdc08d8e52f6 100644
--- a/resources/vue/components/courseware/tasks/AddFeedbackDialog.vue
+++ b/resources/vue/components/courseware/tasks/AddFeedbackDialog.vue
@@ -23,6 +23,7 @@
 <script>
 export default {
     props: ['content'],
+    emits: ['close', 'create'],
     data: () => ({
         localContent: '',
     }),
diff --git a/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue b/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue
index cb3577ad5638f2b927e7519f2fcb78f746feb829..9bf7b6356577a004831d61cc2cc7c687a8984fa0 100644
--- a/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue
@@ -332,13 +332,17 @@ export default {
 
             if (attributes.visible) {
                 this.companionSuccess({
-                    info: this.$gettextInterpolate(this.$gettext('"%{ title }" wurde freigegeben.'),
-                    { title: taskTitle }),
+                    info: this.$gettext(
+                        '"%{ title }" wurde freigegeben.',
+                        { title: taskTitle }
+                    ),
                 });
             } else {
                 this.companionSuccess({
-                    info: this.$gettextInterpolate(this.$gettext('Die Freigabe für %{ "title }" wurde zurückgenommen.'),
-                    { title: taskTitle }),
+                    info: this.$gettext(
+                        'Die Freigabe für %{ "title }" wurde zurückgenommen.',
+                        { title: taskTitle }
+                    ),
                 });
             }
         }
diff --git a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
index 7680167cd2e71ab3fa8fd32aa7d85479783aa202..525a9c123546f0962e1f698a8997914095b13229 100644
--- a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
@@ -12,19 +12,17 @@
         <template v-slot:sourceunit>
             <form class="default" @submit.prevent="">
                 <fieldset v-if="sourceUnits.length !== 0" class="radiobutton-set">
-                    <template v-for="unit in sourceUnits">
+                    <template v-for="unit in sourceUnits" :key="'radio-' + unit.id">
                         <input
                             :id="'cw-task-dist-source-unit' + unit.id"
                             type="radio"
                             v-model="selectedSourceUnit"
                             :checked="unit.id === selectedSourceUnitId"
                             :value="unit"
-                            :key="'radio-' + unit.id"
                             :aria-description="unit.element.attributes.title"
                         />
                         <label
                             @click="selectedSourceUnit = unit"
-                            :key="'label-' + unit.id"
                             :for="'cw-task-dist-source-unit' + unit.id"
                         >
                             <div class="icon"><studip-icon shape="courseware" :size="32" /></div>
@@ -89,19 +87,17 @@
         <template v-slot:targetunit>
             <form v-if="selectedTaskIsTask" class="default" @submit.prevent="">
                 <fieldset v-if="targetUnits.length !== 0" class="radiobutton-set">
-                    <template v-for="unit in targetUnits">
+                    <template v-for="unit in targetUnits" :key="'radio-' + unit.id">
                         <input
                             :id="'cw-task-dist-target-unit' + unit.id"
                             type="radio"
                             v-model="selectedTargetUnit"
                             :checked="unit.id === selectedTargetUnitId"
                             :value="unit"
-                            :key="'radio-' + unit.id"
                             :aria-description="unit.element.attributes.title"
                         />
                         <label
                             @click="selectedTargetUnit = unit"
-                            :key="'label-' + unit.id"
                             :for="'cw-task-dist-target-unit' + unit.id"
                         >
                             <div class="icon"><studip-icon shape="courseware" :size="32" /></div>
@@ -176,11 +172,11 @@
                                         type="checkbox"
                                         v-model="selectedAutors"
                                         :value="user.user_id"
-                                        :aria-label="
-                                            $gettextInterpolate($gettext('%{userName} auswählen'), {
-                                                userName: user.formattedname,
-                                            }, true)
-                                        "
+                                        :aria-label="$gettext(
+                                            '%{userName} auswählen',
+                                            { userName: user.formattedname },
+                                            true
+                                        )"
                                     />
                                 </td>
                                 <td>{{ user.formattedname }}</td>
@@ -214,11 +210,11 @@
                                         type="checkbox"
                                         v-model="selectedGroups"
                                         :value="group.id"
-                                        :aria-label="
-                                            $gettextInterpolate($gettext('%{groupName} auswählen'), {
-                                                groupName: group.name,
-                                            }, true)
-                                        "
+                                        :aria-label="$gettext(
+                                            '%{groupName} auswählen',
+                                            { groupName: group.name },
+                                            true
+                                        )"
                                     />
                                 </td>
                                 <td>{{ group.name }}</td>
@@ -253,6 +249,7 @@ const dateString = (date) =>
 
 export default {
     name: 'courseware-tasks-dialog-distribute',
+    emits: ['newtask'],
     components: {
         CoursewareCompanionBox,
         CoursewareStructuralElementSelector,
diff --git a/resources/vue/components/courseware/tasks/EditFeedbackDialog.vue b/resources/vue/components/courseware/tasks/EditFeedbackDialog.vue
index a07356dfc68d186a676cbbb31fc78bbd711d13ca..b9f6d16a8bec185a5d92154fae7d9b7ec0522408 100644
--- a/resources/vue/components/courseware/tasks/EditFeedbackDialog.vue
+++ b/resources/vue/components/courseware/tasks/EditFeedbackDialog.vue
@@ -32,6 +32,7 @@ import CompanionBox from '../layouts/CoursewareCompanionBox.vue';
 
 export default {
     props: ['content'],
+    emits: ['close', 'update'],
     components: {
         CompanionBox,
     },
diff --git a/resources/vue/components/courseware/tasks/PagesTaskGroupsIndex.vue b/resources/vue/components/courseware/tasks/PagesTaskGroupsIndex.vue
index 5701580d4894e20790de9294ce407a6ceb0f6f43..f57fa8d5500dec29fc91c11701e33937b5879341 100644
--- a/resources/vue/components/courseware/tasks/PagesTaskGroupsIndex.vue
+++ b/resources/vue/components/courseware/tasks/PagesTaskGroupsIndex.vue
@@ -4,9 +4,9 @@
             <CoursewareDashboardStudents v-if="userIsTeacher" />
             <CoursewareDashboardTasks v-else />
         </div>
-        <MountingPortal mountTo="#courseware-action-widget" name="sidebar-actions" v-if="userIsTeacher">
+        <Teleport to="#courseware-action-widget" name="sidebar-actions" v-if="userIsTeacher">
             <CoursewareTasksActionWidget />
-        </MountingPortal>
+        </Teleport>
         <courseware-companion-overlay />
     </div>
 </template>
diff --git a/resources/vue/components/courseware/tasks/PagesTaskGroupsShow.vue b/resources/vue/components/courseware/tasks/PagesTaskGroupsShow.vue
index cacd3abf10b334172ec3ce21339c56ea39036e6b..a0ec342e1ca7713cdf9f52f82a2f9639ecf19a0d 100644
--- a/resources/vue/components/courseware/tasks/PagesTaskGroupsShow.vue
+++ b/resources/vue/components/courseware/tasks/PagesTaskGroupsShow.vue
@@ -1,8 +1,8 @@
 <template>
     <div class="cw-tasks-wrapper">
-        <MountingPortal mountTo="#courseware-action-widget" name="sidebar-actions" v-if="userIsTeacher">
+        <Teleport to="#courseware-action-widget" name="sidebar-actions" v-if="userIsTeacher">
             <CoursewareTasksActionWidget :taskGroup="taskGroup" />
-        </MountingPortal>
+        </Teleport>
 
         <div v-if="taskGroup" class="cw-tasks-list">
             <ContentBar isContentBar>
@@ -237,6 +237,3 @@ export default {
     },
 };
 </script>
-
-<style scoped>
-</style>
diff --git a/resources/vue/components/courseware/tasks/RenewalDialog.vue b/resources/vue/components/courseware/tasks/RenewalDialog.vue
index f08719e22d2d13d17f059a13298545e0052334f5..a8dd83221afc5b7d319ff8967859a5f48e074af5 100644
--- a/resources/vue/components/courseware/tasks/RenewalDialog.vue
+++ b/resources/vue/components/courseware/tasks/RenewalDialog.vue
@@ -38,6 +38,7 @@ export default {
     components: {
         DateInput,
     },
+    emits: ['close', 'update'],
     data: () => ({
         date: null,
         state: null,
diff --git a/resources/vue/components/courseware/tasks/TaskGroup.vue b/resources/vue/components/courseware/tasks/TaskGroup.vue
index 541ce7b780ece045a81915efc053bd87d893e158..122cc7e53ece735caf153967f25200253966c211 100644
--- a/resources/vue/components/courseware/tasks/TaskGroup.vue
+++ b/resources/vue/components/courseware/tasks/TaskGroup.vue
@@ -43,15 +43,17 @@ import TaskItem from './TaskGroupTaskItem.vue';
 
 export default {
     components: { CompanionBox, TaskItem },
+    emits: ['add-feedback', 'edit-feedback', 'solve-renewal'],
     props: ['taskGroup', 'tasks'],
     computed: {
         ...mapGetters({
             coursewareContext: 'context',
         }),
         actionMenuContext() {
-            return this.$gettextInterpolate(this.$gettext('Courseware-Aufgabe "%{ taskGroup }"'), {
-                taskGroup: this.taskGroup.attributes.title,
-            });
+            return this.$gettext(
+                'Courseware-Aufgabe "%{ taskGroup }"',
+                { taskGroup: this.taskGroup.attributes.title }
+            );
         },
     },
 };
diff --git a/resources/vue/components/courseware/tasks/TaskGroupTaskItem.vue b/resources/vue/components/courseware/tasks/TaskGroupTaskItem.vue
index b684f105b658596ab7c7aaab90f13936aaabfce0..eed19ccd2673ed91be80a43af0e4e0f36d0c03cc 100644
--- a/resources/vue/components/courseware/tasks/TaskGroupTaskItem.vue
+++ b/resources/vue/components/courseware/tasks/TaskGroupTaskItem.vue
@@ -56,11 +56,10 @@
         <td class="responsive-hidden">
             <span
                 v-if="feedback"
-                :title="
-                    $gettextInterpolate($gettext('Feedback geschrieben am: %{ date }'), {
-                        date: getReadableDate(feedback.attributes['chdate']),
-                    })
-                "
+                :title="$gettext(
+                    'Feedback geschrieben am: %{ date }',
+                    { date: getReadableDate(feedback.attributes['chdate']) }
+                )"
             >
                 <studip-icon shape="accept" role="status-green" />
                 {{ $gettext('Feedback gegeben') }}
@@ -84,6 +83,7 @@ import { mapGetters } from 'vuex';
 
 export default {
     mixins: [taskHelper],
+    emits: ['add-feedback', 'edit-feedback', 'solve-renewal'],
     props: ['task', 'taskGroup'],
     computed: {
         ...mapGetters({
diff --git a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
index d7db6e19963619749770042803280394fcd9bee0..2e1fe62971c7946f8a41f1d65a33ae104cc2e9f2 100644
--- a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
+++ b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
@@ -41,11 +41,11 @@
                                         v-model="selectedAutors"
                                         :disabled="isSolver(user.user_id)"
                                         :value="user.user_id"
-                                        :aria-label="
-                                            $gettextInterpolate($gettext('%{userName} auswählen'), {
-                                                userName: user.formattedname,
-                                            }, true)
-                                        "
+                                        :aria-label="$gettext(
+                                            '%{userName} auswählen',
+                                            { userName: user.formattedname },
+                                            true
+                                        )"
                                     />
                                 </td>
                                 <td>{{ user.formattedname }}</td>
@@ -74,11 +74,11 @@
                                         v-model="selectedGroups"
                                         :disabled="isSolver(group.id)"
                                         :value="group.id"
-                                        :aria-label="
-                                            $gettextInterpolate($gettext('%{groupName} auswählen'), {
-                                                groupName: group.name,
-                                            }, true)
-                                        "
+                                        :aria-label="$gettext(
+                                            '%{groupName} auswählen',
+                                            { groupName: group.name },
+                                            true
+                                        )"
                                     />
                                 </td>
                                 <td>{{ group.name }}</td>
@@ -100,6 +100,7 @@ export default {
     components: {
         CoursewareCompanion,
     },
+    emits: ['newtask'],
     data: () => ({
         selectedAutors: [],
         selectedGroups: [],
diff --git a/resources/vue/components/courseware/tasks/TaskGroupsModifyDeadlineDialog.vue b/resources/vue/components/courseware/tasks/TaskGroupsModifyDeadlineDialog.vue
index 39198af5965358d45db02ae25cf5a9b4e44003a9..5f201e00ebe0e07aba2825cf9cee9346b8368a26 100644
--- a/resources/vue/components/courseware/tasks/TaskGroupsModifyDeadlineDialog.vue
+++ b/resources/vue/components/courseware/tasks/TaskGroupsModifyDeadlineDialog.vue
@@ -14,7 +14,7 @@
                     {{ $gettext('Aktuelle Bearbeitungszeit:') }} <StudipDate :date="startDate" /> - <StudipDate
                         :date="endDate"
                     />
-                    ({{ $gettextInterpolate($gettext('%{ count } Tage'), { count: oldDuration }) }})
+                    ({{ $gettext('%{ count } Tage', { count: oldDuration }) }})
                 </p>
                 <div class="formpart">
                     <label class="studiprequired">
@@ -35,7 +35,7 @@
                     {{ $gettext('Verlängerte Bearbeitungszeit:') }} <StudipDate :date="startDate" /> - <StudipDate
                         :date="newEndDate"
                     />
-                    ({{ $gettextInterpolate($gettext('%{ count } Tage'), { count: newDuration }) }})
+                    ({{ $gettext('%{ count } Tage', { count: newDuration }) }})
                 </p>
             </form>
         </template>
diff --git a/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue b/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue
index dc9d9293e36ebf23ecbcf4c5a524d86cfdaa1395..a21d9721a408f551b5d0efd5d21a5ae074875abd 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue
@@ -51,16 +51,16 @@ export default {
         },
         favButtonTitle() {
             if (this.blockTypeIsFav) {
-                return this.$gettextInterpolate(
-                    this.$gettext('%{ blockName } Block aus den Favoriten entfernen'),
+                return this.$gettext(
+                    '%{ blockName } Block aus den Favoriten entfernen',
                     { blockName: this.title }
                 );
             }
 
-            return this.$gettextInterpolate(
-                this.$gettext('%{ blockName } Block zu Favoriten hinzufügen'),
+            return this.$gettext(
+                '%{ blockName } Block zu Favoriten hinzufügen',
                 { blockName: this.title }
-            );   
+            );
         }
     },
     methods: {
@@ -80,9 +80,9 @@ export default {
         async addBlock() {
             if (!this.addInProgress) {
                 this.addInProgress = true;
-                this.setAdderStorage({ 
-                    container: this.blockAdder.container, 
-                    section: this.blockAdder.section, 
+                this.setAdderStorage({
+                    container: this.blockAdder.container,
+                    section: this.blockAdder.section,
                     type: this.type ,
                     position: false
                 });
diff --git a/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue b/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue
index 132362628a75b04c901036bdaa5345afde2ef606..3ef18daa5389b3f782a2230d6eb5b9ee564f390f 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue
@@ -118,9 +118,9 @@ export default {
             return menuItems;
         },
         srTitle() {
-            return this.isBlock ? 
-                this.$gettextInterpolate(this.$gettext(`Block %{name} einfügen`), { name: this.name }) :
-                this.$gettextInterpolate(this.$gettext(`Abschnitt %{name} einfügen`), { name: this.name });
+            return this.isBlock ?
+                this.$gettext(`Block %{name} einfügen`, { name: this.name }) :
+                this.$gettext(`Abschnitt %{name} einfügen`, { name: this.name });
         }
     },
     methods: {
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
index bbf322c5991ef48f699626b8aec53543749c9a96..2b2047d624025e4ca2b9cd2148a7aad66e7381f2 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
@@ -195,7 +195,7 @@ export default {
         });
         this.resetAdderStorage();
     },
-    beforeDestroy() {
+    beforeUnmount() {
         window.removeEventListener('scroll', this.updateToolbarTop);
         window.removeEventListener('resize', this.onResize);
     },
@@ -204,10 +204,13 @@ export default {
         consumeMode(newState) {
             this.setHideEditLayout(newState);
         },
-        containers(newValue, oldValue) {
-            if (newValue) {
-                this.resetAdderStorage();
-            }
+        containers: {
+            handler(newValue) {
+                if (newValue) {
+                    this.resetAdderStorage();
+                }
+            },
+            deep: true,
         },
         toolbarActive(newState, oldState) {
             let view = this;
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
index 4a95f698dac3ed39f6cee312762c72f9a2c3f6b5..4d8aa1024f1ada6328716d7bf6457a972e7aaaf9 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
@@ -44,7 +44,7 @@
                       :reduce="(category) => category.type"
                       v-model="currentFilterCategory"
                       >
-                      <template #open-indicator="selectAttributes">
+                      <template #open-indicator="{ selectAttributes }">
                           <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
                       </template>
                       <template #no-options>
@@ -73,16 +73,17 @@
                     @end="dropNewBlock($event)"
                     ref="sortables"
                     sectionId="0"
+                    item-key="id"
                 >
-                    <courseware-blockadder-item
-                        v-for="(block, index) in filteredBlockTypes"
-                        :key="index"
-                        :title="block.title"
-                        :type="block.type"
-                        :data-blocktype="block.type"
-                        :description="block.description"
-                        @blockAdded="$emit('blockAdded')"
-                    />
+                    <template #item="{element}">
+                        <courseware-blockadder-item
+                            :title="element.title"
+                            :type="element.type"
+                            :data-blocktype="element.type"
+                            :description="element.description"
+                            @blockAdded="$emit('blockAdded')"
+                        />
+                    </template>
                 </draggable>
             </div>
             <courseware-companion-box
@@ -117,6 +118,7 @@ export default {
             required: true,
         },
     },
+    emits: ['blockAdded'],
     setup() {
         const { categories } = useBlockCategoryManager();
 
@@ -254,17 +256,18 @@ export default {
             this.isDragging = true;
         },
         async dropNewBlock(e) {
-            const target = e.to.__vue__.$attrs;
-            const blockType = e.item.__vue__.$attrs['data-blocktype'];
+            // TODO: This seems way to hackish
+            const targetAttributes = e.to.__vnode.ctx.attrs;
+            const blockType = e.item.dataset.blocktype;
 
             // only execute if dropped in destined list
-            if (!target.containerId) {
+            if (!targetAttributes.containerId) {
                 return;
             }
             // set chosen container and section and pass block data
             this.setAdderStorage({
-                container: this.containerById({ id: target.containerId }),
-                section: target.sectionId,
+                container: this.containerById({ id: targetAttributes.containerId }),
+                section: targetAttributes.sectionId,
                 type: blockType,
                 position: e.newIndex,
             });
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
index c6f1e920ddcea3a591777f653275c24a36cfe40d..d11d4fcd93b99f83e0865eb245121265fbc87130 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
@@ -16,13 +16,14 @@
                         @end="dropClipboardBlock($event)"
                         ref="clipboardSortables"
                         sectionId="0"
+                        item-key="id"
                     >
-                        <courseware-clipboard-item
-                            v-for="(clipboard, index) in clipboardBlocks"
-                            :key="index"
-                            :clipboard="clipboard"
-                            @inserted="$emit('blockAdded')"
-                        />
+                        <template #item="{element}">
+                            <courseware-clipboard-item
+                                :clipboard="element"
+                                @inserted="$emit('blockAdded')"
+                            />
+                        </template>
                     </draggable>
                 </div>
                 <button class="button trash" @click="clearClipboard('courseware-blocks')">
@@ -50,12 +51,13 @@
                         :clone="cloneClipboardContainer"
                         @end="dropNewContainer($event)"
                         ref="clipboardContainerSortables"
+                        item-key="id"
                     >
-                        <courseware-clipboard-item
-                            v-for="(clipboard, index) in clipboardContainers"
-                            :key="index"
-                            :clipboard="clipboard"
-                        />
+                        <template #item="{element}">
+                            <courseware-clipboard-item
+                                :clipboard="element"
+                            />
+                        </template>
                     </draggable>
                 </div>
                 <button class="button trash" @click="clearClipboard('courseware-containers')">
@@ -101,6 +103,7 @@ export default {
         StudipDialog,
         draggable,
     },
+    emits: ['blockAdded'],
     props: {
         toolbarContentHeight: {
             type: Number,
@@ -185,7 +188,7 @@ export default {
             return original;
         },
         async dropClipboardBlock(e) {
-            const target = e.to.__vue__.$attrs;
+            const target = e.to.__vnode.ctx.attrs;
             // only execute if dropped in destined list
             if (!target.containerId) {
                 return;
@@ -196,7 +199,7 @@ export default {
                 section: target.sectionId,
                 position: e.newIndex,
             });
-            await this.insertItem(e.item.__vue__._data.currentClipboard);
+            await this.insertItem(e.item.__vnode.ctx.data.currentClipboard);
             this.resetAdderStorage();
         },
         cloneClipboardContainer(original) {
@@ -222,7 +225,7 @@ export default {
 
             // if the container is from the clipboard, insert it via clipboard mixin, else add it via container mixin
             if (item.clipContainer) {
-                this.insertItem(e.item.__vue__._data.currentClipboard, e.newIndex);
+                this.insertItem(e.item.__vnode.ctx.data.currentClipboard, e.newIndex);
             } else {
                 const data = {
                     type: item.attributes['container-type'],
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue
index c5781b24e49164e23b734bcd7ea0bb356bf88b76..dda1d2d004915609e2cc268d6c90cd2a2282b66f 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue
@@ -2,9 +2,8 @@
     <div class="cw-toolbar-containers">
         <div class="cw-container-style-selector" role="group" aria-labelledby="cw-containeradder-style">
             <p class="sr-only" id="cw-containeradder-style">{{ $gettext('Abschnitt-Stil') }}</p>
-            <template v-for="style in containerStyles">
+            <template v-for="style in containerStyles" :key="style.key + '-input'">
                 <input
-                    :key="style.key + '-input'"
                     type="radio"
                     name="container-style"
                     :id="'style-' + style.colspan"
@@ -12,7 +11,6 @@
                     :value="style.colspan"
                 />
                 <label
-                    :key="style.key + '-label'"
                     :for="'style-' + style.colspan"
                     :class="[
                         selectedContainerStyle === style.colspan ? 'cw-container-style-selector-active' : '',
@@ -36,18 +34,19 @@
             @start="dragContainerStart($event)"
             @end="dropNewContainer($event)"
             ref="containerSortables"
+            item-key="type"
         >
-            <courseware-container-adder-item
-                v-for="container in containerTypes"
-                :key="container.type"
-                :title="container.title"
-                :type="container.type"
-                :colspan="selectedContainerStyle"
-                :description="container.description"
-                :firstSection="firstSection"
-                :secondSection="secondSection"
-                :newPosition="newContainerPosition"
-            ></courseware-container-adder-item>
+            <template #item="{element}">
+                <courseware-container-adder-item
+                    :title="element.title"
+                    :type="element.type"
+                    :colspan="selectedContainerStyle"
+                    :description="element.description"
+                    :firstSection="firstSection"
+                    :secondSection="secondSection"
+                    :newPosition="newContainerPosition"
+                ></courseware-container-adder-item>
+            </template>
         </draggable>
     </div>
 </template>
@@ -87,8 +86,8 @@ export default {
         },
         containers() {
             return this.relatedContainers({
-                parent: this.structuralElementById({id: this.$route.params.id}), 
-                relationship: 'containers'    
+                parent: this.structuralElementById({id: this.$route.params.id}),
+                relationship: 'containers'
             });
         },
         newContainerPosition() {
@@ -141,7 +140,7 @@ export default {
 
             // if the container is from the clipboard, insert it via clipboard mixin, else add it via container mixin
             if (item.clipContainer) {
-                this.insertItem(e.item.__vue__._data.currentClipboard, e.newIndex);
+                this.insertItem(e.item.__node.ctx.data.currentClipboard, e.newIndex);
             } else {
                 const data = {
                 type: item.attributes['container-type'],
diff --git a/resources/vue/components/courseware/unit/CoursewareSharedItems.vue b/resources/vue/components/courseware/unit/CoursewareSharedItems.vue
index e68179c715322aa14c156a988890e26e53b5317b..a7de0c91d647deefc4ddd2f6a08e13d3235ee959 100644
--- a/resources/vue/components/courseware/unit/CoursewareSharedItems.vue
+++ b/resources/vue/components/courseware/unit/CoursewareSharedItems.vue
@@ -27,12 +27,7 @@
                         </div>
                         <footer>
                             {{ countChildren(element) + 1 }}
-                            <translate
-                                :translate-n="countChildren(element) + 1"
-                                translate-plural="Seiten"
-                            >
-                                Seite
-                            </translate>
+                            {{ $ngettext('Seite', 'Seiten', countChildren(element) + 1) }}
                         </footer>
                     </div>
                 </a>
@@ -42,6 +37,7 @@
 </template>
 <script>
 import { mapGetters } from 'vuex';
+import {$ngettext} from "../../../../assets/javascripts/lib/gettext";
 
 export default {
     name: 'courseware-shared-items',
@@ -52,6 +48,7 @@ export default {
         }),
     },
     methods: {
+        $ngettext,
         getChildStyle(child) {
             let url = child.relationships?.image?.meta?.['download-url'];
 
@@ -74,7 +71,7 @@ export default {
             const ownerId = element.relationships.owner.data.id;
             const owner = this.userById({ id: ownerId });
 
-            return owner.attributes['formatted-name']; 
+            return owner.attributes['formatted-name'];
         },
         countChildren(element) {
             let data = element.relationships.children.data;
@@ -85,4 +82,4 @@ export default {
         },
     },
 }
-</script>
\ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
index 85379003a12e7f8ead524067f6fbcd7a585a11f4..13725b8584048f2fef7d79e03c4283d9ea74eabb 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
@@ -60,7 +60,7 @@
                         name="color"
                     >
                         <template #open-indicator="selectAttributes">
-                            <span v-bind="selectAttributes"
+                            <span v-bind="{ selectAttributes }"
                                 ><studip-icon shape="arr_1down" :size="10"
                             /></span>
                         </template>
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
index c7447cea5855cabfb244de836dc7f515b6f84151..260c456111671f9b84849f866ee595135dac11b3 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
@@ -74,7 +74,7 @@
                             :getOptionLabel="option => option.attributes.title"
                             v-model="selectedRange"
                         >
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"
                                     ><studip-icon shape="arr_1down" :size="10"
                                 /></span>
@@ -94,17 +94,16 @@
         <template v-slot:unit>
             <form class="default" @submit.prevent="">
                 <fieldset v-if="units.length !== 0" class="radiobutton-set">
-                    <template v-for="unit in units">
+                    <template v-for="unit in units" :key="'radio-' + unit.id">
                         <input
                             :id="'cw-shelf-copy-unit-' + unit.id"
                             type="radio"
                             v-model="selectedUnit"
                             :checked="unit.id === selectedUnitId"
                             :value="unit"
-                            :key="'radio-' + unit.id"
                             :aria-description="unit.element.attributes.title"
                         />
-                        <label @click="selectedUnit = unit" :key="'label-' + unit.id" :for="'cw-shelf-copy-unit-' + unit.id">
+                        <label @click="selectedUnit = unit" :for="'cw-shelf-copy-unit-' + unit.id">
                             <div class="icon"><studip-icon shape="courseware" :size="32"/></div>
                             <div class="text">{{ unit.element.attributes.title }}</div>
                             <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
@@ -134,7 +133,7 @@
                         :clearable="false"
                         label="class"
                     >
-                        <template #open-indicator="selectAttributes">
+                        <template #open-indicator="{ selectAttributes }">
                             <span v-bind="selectAttributes"
                                 ><studip-icon shape="arr_1down" :size="10"
                             /></span>
@@ -367,7 +366,7 @@ export default {
             this.selectedUnit = null;
             this.validateSelection();
             const slot = this.wizardSlots[0];
-            
+
             if (newRid !== '') {
                 slot.valid = true;
                 if (this.source === 'courses' || this.source === 'self') {
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogImport.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogImport.vue
index 0f9dc518ef65841e6af4ba841a1033be8261d2ec..467c3f381d8a875cd72ddf1ca6ff11b03ebce229 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogImport.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogImport.vue
@@ -43,7 +43,7 @@
                             :clearable="false"
                             label="class"
                         >
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"
                                     ><studip-icon shape="arr_1down" :size="10"
                                 /></span>
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue
index 70745e815807a231311c3ef01755ad3cef835021..ce18cbf8a74b6940eba42aa080a2fd478cedb6c8 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue
@@ -65,7 +65,7 @@
                     <label>
                         {{ $gettext('Farbe') }}
                         <studip-select v-model="color" :options="colors" :reduce="(color) => color.class" label="class">
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
                             </template>
                             <template #no-options>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
index f2c4d4a73f589c8eaa1cd47903cb0ac194cce25a..564747a19d672c729c711562151b922b62d2c287 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
@@ -35,11 +35,10 @@
                     v-if="hasFeedbackEntries"
                     :amount="feedbackAverage"
                     :size="16"
-                    :title="
-                        $gettextInterpolate($gettext('Lernmaterial wurde mit %{avg} Sternen bewertet'), {
-                            avg: feedbackAverage,
-                        })
-                    "
+                    :title="$gettext(
+                        'Lernmaterial wurde mit %{avg} Sternen bewertet',
+                        { avg: feedbackAverage }
+                    )"
                 />
                 <studip-five-stars
                     v-else
@@ -74,11 +73,11 @@
         <studip-dialog
             v-if="showDeleteDialog"
             :title="$gettext('Lernmaterial löschen')"
-            :question="
-                $gettextInterpolate($gettext('Möchten Sie das Lernmaterial %{ unitTitle } wirklich löschen?'), {
-                    unitTitle: title,
-                }, true)
-            "
+            :question="$gettext(
+                'Möchten Sie das Lernmaterial %{ unitTitle } wirklich löschen?',
+                 { unitTitle: title },
+                 true
+            )"
             height="200"
             @confirm="executeDelete"
             @close="closeDeleteDialog"
@@ -157,6 +156,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-unit-item',
+    emits: ['unit-keydown'],
     components: {
         CoursewareTile,
         CoursewareUnitItemDialogExport,
@@ -322,9 +322,10 @@ export default {
             if (!this.userIsTeacher) {
                 if (this.unit.attributes.visible === 'period') {
                     info.icon = 'date';
-                    info.text = this.$gettextInterpolate(this.$gettext('Sichtbar bis zum %{end}'), {
-                        end: this.permissionVisibleEndDate,
-                    });
+                    info.text = this.$gettext(
+                        'Sichtbar bis zum %{end}',
+                        { end: this.permissionVisibleEndDate }
+                    );
 
                     return info;
                 }
@@ -343,12 +344,10 @@ export default {
                                 info.text = this.$gettext('Sichtbar für alle');
                             } else {
                                 const users = this.unit.attributes['visible-approval'].length;
-                                info.text = this.$gettextInterpolate(
-                                    this.$ngettext(
-                                        'Sichtbar für einen Studierenden',
-                                        'Sichtbar für %{count} Studierende',
-                                        users
-                                    ),
+                                info.text = this.$ngettext(
+                                    'Sichtbar für einen Studierenden',
+                                    'Sichtbar für %{count} Studierende',
+                                    users,
                                     { count: users }
                                 );
                                 if (users === 0) {
@@ -363,8 +362,10 @@ export default {
                                 info.text = this.$gettext('Sichtbar für alle');
                             } else {
                                 const groups = this.unit.attributes['visible-approval'].length;
-                                info.text = this.$gettextInterpolate(
-                                    this.$ngettext('Sichtbar für eine Gruppe', 'Sichtbar für %{count} Gruppen', groups),
+                                info.text = this.$ngettext(
+                                    'Sichtbar für eine Gruppe',
+                                    'Sichtbar für %{count} Gruppen',
+                                    groups,
                                     { count: groups }
                                 );
                                 if (groups === 0) {
@@ -382,8 +383,8 @@ export default {
                     break;
                 case 'period': {
                     info.icon = 'date';
-                    info.title = this.$gettextInterpolate(
-                        this.$gettext('Für %{persons} sichtbar vom %{start} bis zum %{end}'),
+                    info.title = this.$gettext(
+                        'Für %{persons} sichtbar vom %{start} bis zum %{end}',
                         {
                             start: this.permissionVisibleStartDate,
                             end: this.permissionVisibleEndDate,
@@ -428,9 +429,10 @@ export default {
                 if (this.unit.attributes['can-edit-content']) {
                     info.icon = 'edit';
                     if (this.unit.attributes.writable === 'period') {
-                        info.text = this.$gettextInterpolate(this.$gettext('Bearbeitbar bis zum %{end}'), {
-                            end: this.permissionWritableEndDate,
-                        });
+                        info.text = this.$gettext(
+                            'Bearbeitbar bis zum %{end}',
+                            { end: this.permissionWritableEndDate }
+                        );
                     } else {
                         info.text = this.$gettext('Bearbeitbar');
                     }
@@ -451,13 +453,14 @@ export default {
                 }
                 info.icon = 'edit';
                 if (this.unit.attributes.writable === 'always') {
-                    info.text = this.$gettextInterpolate(this.$gettext('Bearbeitbar für %{persons} '), {
-                        persons: this.getPermissionPersons('writable-approval'),
-                    });
+                    info.text = this.$gettext(
+                        'Bearbeitbar für %{persons}',
+                        { persons: this.getPermissionPersons('writable-approval') }
+                    );
                 }
                 if (this.unit.attributes.writable === 'period') {
-                    info.title = this.$gettextInterpolate(
-                        this.$gettext('Für %{persons} bearbeitbar vom %{start} bis zum %{end}'),
+                    info.title = this.$gettext(
+                        'Für %{persons} bearbeitbar vom %{start} bis zum %{end}',
                         {
                             start: this.permissionWritableStartDate,
                             end: this.permissionWritableEndDate,
@@ -575,8 +578,10 @@ export default {
                         return this.$gettext('alle');
                     } else {
                         const users = this.unit.attributes[type].length;
-                        return this.$gettextInterpolate(
-                            this.$ngettext('einen Studierenden', '%{count} Studierende', users),
+                        return this.$ngettext(
+                            'einen Studierenden',
+                            '%{count} Studierende',
+                            users,
                             { count: users }
                         );
                     }
@@ -586,9 +591,12 @@ export default {
                         return this.$gettext('alle');
                     } else {
                         const groups = this.unit.attributes[type].length;
-                        return this.$gettextInterpolate(this.$ngettext('eine Gruppe', '%{count} Gruppen', groups), {
-                            count: groups,
-                        });
+                        return this.$ngettext(
+                            'eine Gruppe',
+                            '%{count} Gruppen',
+                            groups,
+                            { count: groups }
+                        );
                     }
                 }
             }
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogExport.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogExport.vue
index 08f0a21fbedd687da9fdf4a0f67fa4ad578f37c4..c606073e68b1cc4b2df0635db81432b8ed06950f 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogExport.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogExport.vue
@@ -12,13 +12,13 @@
             <template v-slot:dialogContent>
                 <courseware-companion-box
                     v-show="!exportRunning"
-                    :msgCompanion="$gettextInterpolate($gettext('Export des Lernmaterials: %{title}'), {title: title})"
+                    :msgCompanion="$gettext('Export des Lernmaterials: %{title}', { title })"
                     mood="curious"
                 />
 
                 <courseware-companion-box
                     v-show="exportRunning"
-                    :msgCompanion="$gettextInterpolate($gettext('%{title} wird exportiert, bitte haben sie einen Moment Geduld...'), {title: title})"
+                    :msgCompanion="$gettext('%{title} wird exportiert, bitte haben sie einen Moment Geduld...', { title })"
                     mood="pointing"
                 />
                 <div v-show="exportRunning" class="cw-import-zip">
@@ -48,6 +48,7 @@ import { mapActions, mapGetters } from 'vuex';
 export default {
     name: 'courseware-unit-item-dialog-export',
     mixins: [CoursewareExport],
+    emits: ['close'],
     components: {
         CoursewareCompanionBox,
     },
@@ -67,7 +68,7 @@ export default {
             exportState: 'exportState',
             instanceById: 'courseware-instances/byId',
             structuralElementById: 'courseware-structural-elements/byId',
-            userIsTeacher: 'userIsTeacher', 
+            userIsTeacher: 'userIsTeacher',
         }),
         instance() {
             if (this.inCourseContext) {
@@ -75,7 +76,7 @@ export default {
             } else {
                 return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id});
             }
-            
+
         },
         inCourseContext() {
             return this.context.type === 'courses';
@@ -103,7 +104,7 @@ export default {
             }
 
             this.exportRunning = true;
-            
+
             this.setExportState(this.$gettext('Lade Einstellungen'));
             await this.loadUnitInstance();
             this.setExportState('');
@@ -129,4 +130,4 @@ export default {
         await this.loadUnitInstance();
     }
 }
-</script>
\ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
index 572e171fc28eddfe3f5e72c79a4a7f2d4f452c92..a188b44adac0ddb3a9ab18ef43aed613bf608ca4 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
@@ -75,7 +75,7 @@
                             label="class"
                             class="cw-vs-select"
                         >
-                            <template #open-indicator="selectAttributes">
+                            <template #open-indicator="{ selectAttributes }">
                                 <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
                             </template>
                             <template #no-options> {{ $gettext('Es steht keine Auswahl zur Verfügung') }}. </template>
@@ -112,6 +112,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-unit-item-dialog-layout',
+    emits: ['close'],
     components: {
         CoursewareCompanionBox,
         StockImageSelector,
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissionScope.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissionScope.vue
index 46a85c5c2cd9e0d86e5a3ab9ebe86c0caea65ec1..deed35f29537b00d908d3da56e44ae4d24caf5a0 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissionScope.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissionScope.vue
@@ -17,6 +17,7 @@ import { mapActions } from 'vuex';
 
 export default {
     name: 'courseware-unit-item-dialog-permission-scope',
+    emits: ['close', 'switch'],
     props: {
         unit: {
             type: Object,
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue
index 86ac64128522a83e2c1de77e8df88d87ee434bf3..6a96d90cf6a18326e6aeae038de9b6addda056ad 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue
@@ -255,6 +255,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-unit-item-dialog-permissions',
+    emits: ['close'],
     components: {
         CoursewareCompanionBox,
         Datepicker,
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue
index 14c8d304d935564b5fb666da0e08333e8bcbaa4a..f4f7a22ab2dbd90d0ffcce6fb9e92d36f11d601e 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogSettings.vue
@@ -219,6 +219,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-unit-item-dialog-settings',
+    emits: ['close'],
     components: {
         CoursewareFileChooser,
         StudipProgressIndicator,
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItems.vue b/resources/vue/components/courseware/unit/CoursewareUnitItems.vue
index 8f747bfc445fbb07382dbbb1129a467362984698..def2d1712f9886867a5298c8ae9cbeec62b7ee72 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItems.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItems.vue
@@ -3,8 +3,8 @@
         <h2 v-if="!inCourseContext && hasUnits">{{ $gettext('Persönliche Lernmaterialien') }}</h2>
         <template v-if="hasUnits">
             <ol v-if="(!userIsTeacher && inCourseContext) || units.length === 1" class="cw-tiles">
-                <template v-for="unit in units">
-                    <courseware-unit-item :key="unit.id" :unit="unit" :handle="false"/>
+                <template v-for="unit in units" :key="unit.id">
+                    <courseware-unit-item :unit="unit" :handle="false"/>
                 </template>
             </ol>
             <template v-else>
@@ -13,23 +13,24 @@
                     {{ $gettext('Drücken Sie die Leertaste oder Entertaste, um neu anzuordnen.') }}
                 </span>
                 <draggable
+                    v-bind="dragOptions"
                     tag="ol"
                     role="listbox"
                     v-model="unitList"
-                    v-bind="dragOptions"
                     handle=".cw-tile-handle"
                     group="units"
                     @start="isDragging = true"
                     @end="dropUnit"
                     ref="sortables"
                     class="cw-tiles"
+                    item-key="id"
                 >
-                    <courseware-unit-item
-                        v-for="unit in unitList"
-                        :key="unit.id"
-                        :unit="unit"
-                        @unit-keydown="keyHandler($event, unit.id)"
-                    />
+                    <template #item="{element}">
+                        <courseware-unit-item
+                            :unit="element"
+                            @unit-keydown="keyHandler($event, element.id)"
+                        />
+                    </template>
                 </draggable>
             </template>
         </template>
@@ -159,13 +160,15 @@ export default {
                     } else {
                         this.keyboardSelected = { id: unitId, title: this.getUnitTitle(unitId) };
                         const index = this.unitList.findIndex((unit) => unit.id === unitId);
-                        this.assistiveLive = this.$gettextInterpolate(
-                            this.$gettext(
-                                'Lernmaterial %{unitTitle} ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. ' +
-                                    'Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste oder ' +
-                                    'Entertaste zum Ablegen, die Escape-Taste zum Abbrechen.'
-                            ),
-                            { unitTitle: this.keyboardSelected.title, pos: index + 1, listLength: this.unitList.length }
+                        this.assistiveLive = this.$gettext(
+                            'Lernmaterial %{unitTitle} ausgewählt. Aktuelle Position in der Liste: %{pos} von %{listLength}. ' +
+                                'Drücken Sie die Aufwärts- und Abwärtspfeiltasten, um die Position zu ändern, die Leertaste oder ' +
+                                'Entertaste zum Ablegen, die Escape-Taste zum Abbrechen.',
+                            {
+                                unitTitle: this.keyboardSelected.title,
+                                pos: index + 1,
+                                listLength: this.unitList.length
+                            }
                         );
                     }
                     break;
@@ -189,8 +192,8 @@ export default {
             }
         },
         abortKeyboardSorting() {
-            this.assistiveLive = this.$gettextInterpolate(
-                this.$gettext('Lernmaterial %{unitTitle}, Neuordnung abgebrochen.'),
+            this.assistiveLive = this.$gettext(
+                'Lernmaterial %{unitTitle}, Neuordnung abgebrochen.',
                 { unitTitle: this.keyboardSelected.title }
             );
             this.keyboardSelected = null;
@@ -198,11 +201,13 @@ export default {
         },
         storeKeyboardSorting() {
             const index = this.unitList.findIndex((unit) => unit.id === this.keyboardSelected.id);
-            this.assistiveLive = this.$gettextInterpolate(
-                this.$gettext(
-                    'Lernmaterial %{unitTitle}, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.'
-                ),
-                { unitTitle: this.keyboardSelected.title, pos: index + 1, listLength: this.unitList.length }
+            this.assistiveLive = this.$gettext(
+                'Lernmaterial %{unitTitle}, abgelegt. Endgültige Position in der Liste: %{pos} von %{listLength}.',
+                {
+                    unitTitle: this.keyboardSelected.title,
+                    pos: index + 1,
+                    listLength: this.unitList.length
+                }
             );
             this.keyboardSelected = null;
             this.dropUnit();
@@ -213,11 +218,13 @@ export default {
                 const newPos = currentIndex - 1;
                 this.unitList.splice(newPos, 0, this.unitList.splice(currentIndex, 1)[0]);
                 this.focusHandle(unitId);
-                this.assistiveLive = this.$gettextInterpolate(
-                    this.$gettext(
-                        'Lernmaterial %{unitTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}.'
-                    ),
-                    { unitTitle: this.keyboardSelected.title, pos: newPos + 1, listLength: this.unitList.length }
+                this.assistiveLive = this.$gettext(
+                    'Lernmaterial %{unitTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
+                    {
+                        unitTitle: this.keyboardSelected.title,
+                        pos: newPos + 1,
+                        listLength: this.unitList.length
+                    }
                 );
             }
         },
@@ -227,11 +234,13 @@ export default {
                 const newPos = currentIndex + 1;
                 this.unitList.splice(newPos, 0, this.unitList.splice(currentIndex, 1)[0]);
                 this.focusHandle(unitId);
-                this.assistiveLive = this.$gettextInterpolate(
-                    this.$gettext(
-                        'Lernmaterial %{unitTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}.'
-                    ),
-                    { unitTitle: this.keyboardSelected.title, pos: newPos + 1, listLength: this.unitList.length }
+                this.assistiveLive = this.$gettext(
+                    'Lernmaterial %{unitTitle}. Aktuelle Position in der Liste: %{pos} von %{listLength}.',
+                    {
+                        unitTitle: this.keyboardSelected.title,
+                        pos: newPos + 1,
+                        listLength: this.unitList.length
+                    }
                 );
             }
         },
@@ -245,9 +254,12 @@ export default {
         this.initCurrentData();
     },
     watch: {
-        units(newState) {
-            this.initCurrentData();
+        units: {
+            handler(newState) {
+                this.initCurrentData();
+            },
+            deep: true
         },
-    },
+    }
 };
 </script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
index 31c6c01d3a7eb37717d3c058c0c555c23524b4ee..9d21d4cd76baf1b670f256432358abebab76bba1 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
@@ -17,7 +17,7 @@
             <h1>
                 <a
                     :href="chapterUrl"
-                    :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name }, true)"
+                    :title="$gettext('%{ pageTitle } öffnen', { pageTitle: selected.name }, true)"
                 >
                     {{ selected.name }}
                 </a>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgressItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgressItem.vue
index b83a69289841360bd5b7c4af14e55d30835a8d93..c0614c193f5145a68b6e6ad627839277dd8c5f93 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitProgressItem.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitProgressItem.vue
@@ -1,5 +1,5 @@
 <template>
-    <a 
+    <a
         href="#"
         class="cw-unit-progress-item"
         :title="name"
@@ -19,6 +19,7 @@ import CoursewareProgressCircle from './CoursewareProgressCircle.vue';
 
 export default {
     name: 'courseware-unit-progress-item',
+    emits: ['selectChapter'],
     components: {
         CoursewareProgressCircle,
     },
diff --git a/resources/vue/components/courseware/widgets/CoursewareAdminActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareAdminActionWidget.vue
index 6c02699650e97cd40ddb8caf91c7e5f88ea5d156..78e529ce61f3b4291e0d9a256c6f2343a66b87b3 100644
--- a/resources/vue/components/courseware/widgets/CoursewareAdminActionWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareAdminActionWidget.vue
@@ -2,7 +2,7 @@
     <ul class="widget-list widget-links cw-action-widget">
         <li v-if="templatesView" class="cw-action-widget-add">
             <a href="#"  @click.prevent="addTemplate">
-                <translate>Vorlage hinzufügen</translate>
+                {{ $gettext('Vorlage hinzufügen') }}
             </a>
         </li>
     </ul>
diff --git a/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue b/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue
index 81a80715e37198c9094cbd4c3b81e22b170f8d52..aad266d0dace17d1d6a84e379a6febd264020c98 100644
--- a/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareAdminViewWidget.vue
@@ -2,7 +2,7 @@
   <ul class="widget-list widget-links sidebar-views cw-view-widget">
         <li :class="{ active: templatesView }">
             <a href="#" @click.prevent="setTemplatesView">
-                <translate>Vorlagen</translate>
+                {{ $gettext('Vorlagen') }}
             </a>
         </li>
   </ul>
diff --git a/resources/vue/components/feedback/FeedbackCreateDialog.vue b/resources/vue/components/feedback/FeedbackCreateDialog.vue
index 204254c395aef2129e149e02c2fc2d72de43f243..865cffd100498a86772e838b701ebe073fb8c9b5 100644
--- a/resources/vue/components/feedback/FeedbackCreateDialog.vue
+++ b/resources/vue/components/feedback/FeedbackCreateDialog.vue
@@ -37,10 +37,11 @@
 </template>
 
 <script>
-import { mapActions, mapGetters } from 'vuex';
+import { mapActions } from 'vuex';
 
 export default {
     name: 'feedback-create-dialog',
+    emits: ['close', 'created'],
     props: {
         defaultQuestion: {
             type: String,
diff --git a/resources/vue/components/feedback/FeedbackDialog.vue b/resources/vue/components/feedback/FeedbackDialog.vue
index e432b1b4354d32e68d3bdc6dc25950e311db4dbf..1c15b843a981ea719b1523dd1a4dcbb2d5e8ec77 100644
--- a/resources/vue/components/feedback/FeedbackDialog.vue
+++ b/resources/vue/components/feedback/FeedbackDialog.vue
@@ -119,6 +119,7 @@ export default {
         FeedbackFiveStarsHistogram,
         StudipProgressIndicator,
     },
+    emits: ['close', 'deleted'],
     props: {
         feedbackElementId: {
             type: Number,
diff --git a/resources/vue/components/feedback/FeedbackElementUpdate.vue b/resources/vue/components/feedback/FeedbackElementUpdate.vue
index d9706de490f66e9103667583e68c9a772f728a37..3d0c42b7bb32f3ce1271cd90f8b87e226bf50d3a 100644
--- a/resources/vue/components/feedback/FeedbackElementUpdate.vue
+++ b/resources/vue/components/feedback/FeedbackElementUpdate.vue
@@ -24,6 +24,7 @@
 import { mapActions, mapGetters } from 'vuex';
 export default {
     name: 'feedback-element-update',
+    emits: ['cancel', 'submit'],
     props: {
         feedbackElementId: {
             type: Number,
diff --git a/resources/vue/components/feedback/FeedbackEntryBox.vue b/resources/vue/components/feedback/FeedbackEntryBox.vue
index 0400c9d25a20cc6784ca806aa5146e9d5f01a57a..bd68a8fef193ad4e24a053d0fcfe46239c8e3d4b 100644
--- a/resources/vue/components/feedback/FeedbackEntryBox.vue
+++ b/resources/vue/components/feedback/FeedbackEntryBox.vue
@@ -27,6 +27,7 @@ export default {
     components: {
         StudipFiveStars,
     },
+    emits: ['delete', 'edit'],
     props: {
         entry: {
             type: Object,
diff --git a/resources/vue/components/feedback/FeedbackEntryCreate.vue b/resources/vue/components/feedback/FeedbackEntryCreate.vue
index 5735d45738776dfd332eb70d4f0b2ab19df298a0..7833a64b6650461fd7ddfbc941ce00cb18e64b56 100644
--- a/resources/vue/components/feedback/FeedbackEntryCreate.vue
+++ b/resources/vue/components/feedback/FeedbackEntryCreate.vue
@@ -16,7 +16,7 @@
             <button v-if="hasEntry" class="button cancel" @click="$emit('cancel')">
                 {{ $gettext('Abbrechen') }}
             </button>
-            
+
         </div>
     </div>
 </template>
@@ -30,6 +30,7 @@ export default {
     components: {
         StudipFiveStarsInput,
     },
+    emits: ['cancel', 'submit'],
     props: {
         feedbackElement: {
             type: Object || null,
@@ -82,7 +83,7 @@ export default {
                     author: {
                         data: {
                             id: this.currentUser.id,
-                            type: 'users' 
+                            type: 'users'
                         }
                     }
                 },
@@ -111,4 +112,4 @@ export default {
         }
     },
 };
-</script>
\ No newline at end of file
+</script>
diff --git a/resources/vue/components/feedback/FeedbackFiveStarsHistogram.vue b/resources/vue/components/feedback/FeedbackFiveStarsHistogram.vue
index b40245276ba41252d0dd1cb20f763b86be96e52f..f66653136da8be6082aa19c45a142c5f18f232f5 100644
--- a/resources/vue/components/feedback/FeedbackFiveStarsHistogram.vue
+++ b/resources/vue/components/feedback/FeedbackFiveStarsHistogram.vue
@@ -7,11 +7,12 @@
             </p>
             <studip-five-stars :amount="average" />
             <p class="total">
-                {{
-                    $gettextInterpolate($ngettext('%{n} Bewertung', '%{n} Bewertungen', entries.length), {
-                        n: entries.length,
-                    })
-                }}
+                {{ $ngettext(
+                    '%{n} Bewertung',
+                    '%{n} Bewertungen',
+                    entries.length,
+                    { n: entries.length }
+                ) }}
             </p>
         </div>
         <div class="five-stars-histogram-chart" v-if="ratings">
diff --git a/resources/vue/components/feedback/StudipFiveStarsInput.vue b/resources/vue/components/feedback/StudipFiveStarsInput.vue
index 6fa95f09ce8602f13654767669dada90ba1660d1..73448528f55a14c5734aa8c7de8953958c8d8917 100644
--- a/resources/vue/components/feedback/StudipFiveStarsInput.vue
+++ b/resources/vue/components/feedback/StudipFiveStarsInput.vue
@@ -4,16 +4,12 @@
             <studip-icon
                 :shape="getShape(i)"
                 :size="size"
-                :alt="
-                    $gettextInterpolate(
-                        $ngettext(
-                            'auswählen, um mit einem Stern zu bewerten.',
-                            'auswählen, um mit %{i} Sternen zu bewerten.',
-                            i
-                        ),
-                        { i: i }
-                    )
-                "
+                :alt="$ngettext(
+                    'auswählen, um mit einem Stern zu bewerten.',
+                    'auswählen, um mit %{i} Sternen zu bewerten.',
+                    i,
+                    { i }
+                )"
             />
         </button>
     </div>
@@ -25,8 +21,9 @@ export default {
     components: {
         StudipIcon,
     },
+    emits: ['update:model-value'],
     props: {
-        value: {
+        modelValue: {
             type: Number,
         },
         size: {
@@ -41,10 +38,10 @@ export default {
     },
     methods: {
         setValue(val) {
-            this.$emit('input', val);
+            this.$emit('update:model-value', val);
         },
         getShape(pos) {
-            return pos <= this.value ? 'star' : 'star-empty';
+            return pos <= this.modelValue ? 'star' : 'star-empty';
         },
     },
 };
diff --git a/resources/vue/components/file-chooser/FileChooserBox.vue b/resources/vue/components/file-chooser/FileChooserBox.vue
index 0a3896223868a16b89550d680dbe05df93ec81ad..38ce9924f1648634f349ec17511678b3a3047d79 100644
--- a/resources/vue/components/file-chooser/FileChooserBox.vue
+++ b/resources/vue/components/file-chooser/FileChooserBox.vue
@@ -72,6 +72,7 @@ export default {
         FileChooserToolbar,
         StudipProgressIndicator,
     },
+    emits: ['selectId'],
     props: {
         excludedFolderTypes: { type: Array, default: () => [] },
     },
diff --git a/resources/vue/components/file-chooser/FileChooserDialog.vue b/resources/vue/components/file-chooser/FileChooserDialog.vue
index 2b21f5188c2bae62a44f93f50907a2ecdc7337ad..49bff431389bd25516f47dcc74b5ac3f9c483fbb 100644
--- a/resources/vue/components/file-chooser/FileChooserDialog.vue
+++ b/resources/vue/components/file-chooser/FileChooserDialog.vue
@@ -56,6 +56,7 @@ export default {
         FileChooserBox,
         FileChooserTree,
     },
+    emits: ['close', 'selected'],
     props: {
         selectable: {
             type: String,
diff --git a/resources/vue/components/file-chooser/FileChooserFileItem.vue b/resources/vue/components/file-chooser/FileChooserFileItem.vue
index 90c2f397d46e432c4640ce1d65b5324732a0f04f..e7d1794f82267036c4170b3bfcff06af48c14fca 100644
--- a/resources/vue/components/file-chooser/FileChooserFileItem.vue
+++ b/resources/vue/components/file-chooser/FileChooserFileItem.vue
@@ -29,6 +29,7 @@
 import { mapActions, mapGetters } from 'vuex';
 export default {
     name: 'file-chooser-file-item',
+    emits: ['selectId'],
     props: {
         file: {
             type: Object,
diff --git a/resources/vue/components/file-chooser/FileChooserFolderItem.vue b/resources/vue/components/file-chooser/FileChooserFolderItem.vue
index 116f582458637ac511e8141a23466a0f8e05da6f..f3784f481d753919e399893e63c6899e078b0ba8 100644
--- a/resources/vue/components/file-chooser/FileChooserFolderItem.vue
+++ b/resources/vue/components/file-chooser/FileChooserFolderItem.vue
@@ -77,9 +77,12 @@ export default {
         },
         folderSize() {
             const length = this.folderSubfolderCounter + this.folderFilesCounter;
-            return this.$gettextInterpolate(this.$ngettext('%{length} Objekt', '%{length} Objekte', length), {
-                length: length,
-            });
+            return this.$ngettext(
+                '%{length} Objekt',
+                '%{length} Objekte',
+                length,
+                { length: length }
+            );
         },
     },
     methods: {
diff --git a/resources/vue/components/file-chooser/FileChooserTable.vue b/resources/vue/components/file-chooser/FileChooserTable.vue
index ad7363dc9e6d5e5e03d3bd28c39381471daca632..5606361e1fc2675d11ad879f3c8fff8cdbd70c3e 100644
--- a/resources/vue/components/file-chooser/FileChooserTable.vue
+++ b/resources/vue/components/file-chooser/FileChooserTable.vue
@@ -58,6 +58,7 @@ export default {
         fileChooserFileItem,
         fileChooserFolderItem,
     },
+    emits: ['selectId'],
     props: {
         files: {
             type: Array,
diff --git a/resources/vue/components/file-chooser/FileChooserToolbar.vue b/resources/vue/components/file-chooser/FileChooserToolbar.vue
index 4f3053e94288876d5177c8052fa333d6dbc70e2a..6f8442b8fd6263e785d88e472a8589780fbc4b78 100644
--- a/resources/vue/components/file-chooser/FileChooserToolbar.vue
+++ b/resources/vue/components/file-chooser/FileChooserToolbar.vue
@@ -46,6 +46,7 @@ import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'file-chooser-toolbar',
+    emits: ['fileAdded', 'folderAdded'],
     data() {
         return {
             showFolderAdder: false,
diff --git a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
index e507adce1e37f649b34426cc7e76fc0c1819c161..59130580833fd16e78b3a010cf154c188c83426c 100644
--- a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
+++ b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
@@ -28,19 +28,20 @@
                     <td>
                         <input type="checkbox" :name="name + '_write_permissions[]'" :value="user.id"
                                v-model="user.write_permissions"
-                               :aria-label="$gettextInterpolate(
-                                   $gettext('Schreibzugriff für %{name}'),
+                               :aria-label="$gettext(
+                                   'Schreibzugriff für %{name}',
                                    {name: user.name},
                                    true
                                )">
                     </td>
                     <td class="actions">
                         <studip-icon shape="trash" aria-role="button" @click="removeContact(user.id)"
-                                     :title="$gettextInterpolate(
-                                         $gettext('Kalender nicht mehr mit %{name} teilen'),
+                                     :title="$gettext(
+                                         'Kalender nicht mehr mit %{name} teilen',
                                          {name: user.name},
                                          true
-                                     )"></studip-icon>
+                                     )"
+                        ></studip-icon>
                     </td>
                 </tr>
             </tbody>
diff --git a/resources/vue/components/form_inputs/CaptchaInput.vue b/resources/vue/components/form_inputs/CaptchaInput.vue
index 1ace43ddc42b04d15d12d0cc59de5e69f392a38d..a30cdca65d01efa1f4062d13692af47d5b5b69f9 100644
--- a/resources/vue/components/form_inputs/CaptchaInput.vue
+++ b/resources/vue/components/form_inputs/CaptchaInput.vue
@@ -10,6 +10,7 @@ import { $gettext } from '../../../assets/javascripts/lib/gettext';
 
 export default {
     name: 'CaptchaInput',
+    emits: ['update:model-value'],
     props: {
         name: {
             type: String,
@@ -44,7 +45,7 @@ export default {
 
             this.$refs.widget.addEventListener('statechange', (ev) => {
                 if (ev.detail.state === 'verified') {
-                    this.$emit('input', ev.detail.payload);
+                    this.$emit('update:model-value', ev.detail.payload);
                 }
             })
         });
diff --git a/resources/vue/components/form_inputs/DateListInput.vue b/resources/vue/components/form_inputs/DateListInput.vue
index d77c993454fb949e279b8c95b6e50da2a049a107..48cf05c328da56b26a87d0b16e5bca55bf441115 100644
--- a/resources/vue/components/form_inputs/DateListInput.vue
+++ b/resources/vue/components/form_inputs/DateListInput.vue
@@ -2,7 +2,7 @@
     <div class="formpart">
         <div class="sr-only" aria-live="polite" ref="list_message_field"></div>
         <ul>
-            <li v-for="date in selected_date_list" v-bind="selected_date_list" :key="getISODate(date)">
+            <li v-for="date in selected_date_list" :key="getISODate(date)">
                 <input type="hidden" :name="input_name + '[]'" :value="getISODate(date)">
                 <studip-date-time :timestamp="Math.floor(date.getTime() / 1000)" :date_only="true"></studip-date-time>
                 <studip-icon shape="trash" :title="$gettext('Löschen')" @click="removeDate(date)"
@@ -22,11 +22,11 @@
 
 <script>
 import StudipDateTime from "../StudipDateTime.vue";
-import {$gettext, $gettextInterpolate} from "@/assets/javascripts/lib/gettext";
 
 export default {
     name: "date-list-input",
     components: {StudipDateTime},
+    emits: ['selected_date_list', 'selected_date_value'],
     props: {
         name: {
             type: String,
@@ -80,14 +80,14 @@ export default {
             }
             let reformatted_date = date_parts[2] + '-' + date_parts[1] + '-' + date_parts[0];
             this.selected_date_list.push(new Date(reformatted_date));
-            this.$refs.list_message_field.innerText = $gettextInterpolate($gettext('Datum %{date} hinzugefügt'), {date: this.selected_date_value});
+            this.$refs.list_message_field.innerText = this.$gettext('Datum %{date} hinzugefügt', {date: this.selected_date_value});
         },
         removeDate(date) {
             this.selected_date_list = this.selected_date_list.filter(d => d !== date);
 
-            this.$refs.list_message_field.innerText = $gettextInterpolate(
-                $gettext('Datum %{date} entfernt'),
-                {date: STUDIP.DateTime.getStudipDate(date, false, true)}
+            this.$refs.list_message_field.innerText = this.$gettext(
+                'Datum %{date} entfernt',
+                { date: STUDIP.DateTime.getStudipDate(date, false, true) }
             );
         },
         getISODate(date) {
diff --git a/resources/vue/components/form_inputs/DayOfWeekSelect.vue b/resources/vue/components/form_inputs/DayOfWeekSelect.vue
index c28ddb6db4a435f3c4dbb7c666ebe952dcd9eded..0f8792c49b2754b53d353b4f8e5b18337c765179 100644
--- a/resources/vue/components/form_inputs/DayOfWeekSelect.vue
+++ b/resources/vue/components/form_inputs/DayOfWeekSelect.vue
@@ -31,6 +31,7 @@
 <script>
 export default {
     name: "day-of-week-select",
+    emits: ['selected_value'],
     props: {
         name: {
             type: String,
diff --git a/resources/vue/components/form_inputs/FileUpload.vue b/resources/vue/components/form_inputs/FileUpload.vue
index b512c026a20bed7711774d42478faf4a7b808fad..d0b159e4c9c53a80d6078e47ef08a01522a5ecf2 100644
--- a/resources/vue/components/form_inputs/FileUpload.vue
+++ b/resources/vue/components/form_inputs/FileUpload.vue
@@ -21,7 +21,7 @@
                 {{ $gettext('Eine Datei gewählt') }}
             </template>
             <template v-else>
-                {{ $gettextInterpolate($gettext('%{number} Dateien gewählt'), { number: selectedFiles.length }) }}
+                {{ $gettext('%{number} Dateien gewählt', { number: selectedFiles.length }) }}
             </template>
         </div>
         <input type="file"
@@ -142,19 +142,19 @@ export default {
             }
         },
         getTextualFileSize(bytes) {
-            let unit = '';
-            let context = {size: bytes};
             if (bytes < 1024) {
-                unit = this.$gettext('%{size} B');
-            } else if (bytes < 1024 * 1024) {
-                unit = this.$gettext('%{size} KB');
-                context.size = (bytes / 1024).toFixed(2);
-            } else {
-                unit = this.$gettext('%{size} MB');
-                context.size = (bytes / (1024 * 1024)).toFixed(2);
+                return this.$gettext('%{size} B', {size: bytes});
             }
 
-            return this.$gettextInterpolate(unit, context);
+            if (bytes < 1024 * 1024) {
+                return this.$gettext('%{size} KB', {
+                    size: (bytes / 1024).toFixed(2)
+                });
+            }
+
+            return this.$gettext('%{size} MB', {
+                size: (bytes / (1024 * 1024)).toFixed(2)
+            });
         },
         openFileSelect() {
             this.$refs.files.click();
diff --git a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
index fe67db55661b5670712b6b5b2f1ea5430d2d4d33..d8431dd99993d4e49af5a846c1bca786c26f4b06 100644
--- a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
+++ b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
@@ -42,7 +42,7 @@
                         <input type="hidden" :name="`${name}_course_ids[${course.id}]`" value="0">
                         <input type="checkbox" :name="`${name}_course_ids[${course.id}]`"
                                value="1" :checked="selected_course_id_list.includes(course.id)"
-                               :title="$gettextInterpolate($gettext('%{course} auswählen'), {course: course.name}, true)">
+                               :title="$gettext('%{course} auswählen', {course: course.name}, true)">
                     </td>
                 </tr>
                 <tr v-if="loadedSemesters.includes(semester_id) && courses.length === 0">
diff --git a/resources/vue/components/form_inputs/QuicksearchListInput.vue b/resources/vue/components/form_inputs/QuicksearchListInput.vue
index 4a3e21c4cd3af36a6958ac72b77c24bb6199bc21..eb6c3b87028bb0a83200964ffe6745bce7e1476f 100644
--- a/resources/vue/components/form_inputs/QuicksearchListInput.vue
+++ b/resources/vue/components/form_inputs/QuicksearchListInput.vue
@@ -2,7 +2,7 @@
     <div>
         <quicksearch :searchtype="searchtype"
                      :autocomplete="autocomplete"
-                     @input="addElement"></quicksearch>
+                     @update:model-value="addElement"></quicksearch>
         <table v-if="elements.length > 0" ref="results" class="default">
             <tbody>
                 <tr v-for="(element, index) in elements"
@@ -52,7 +52,7 @@ export default {
     },
     data() {
         return {
-            elements: []
+            elements: [],
         }
     },
     computed: {
diff --git a/resources/vue/components/form_inputs/RepetitionInput.vue b/resources/vue/components/form_inputs/RepetitionInput.vue
index 2a265b1d52c3af40441beb7a81412027d9f7a432..ab033bd8c38d3534448980b2416bb60bc5fa534d 100644
--- a/resources/vue/components/form_inputs/RepetitionInput.vue
+++ b/resources/vue/components/form_inputs/RepetitionInput.vue
@@ -237,6 +237,18 @@
 <script>
 export default {
     name: "repetition-input",
+    emits: [
+        'input_number_of_dates',
+        'input_repetition_dom',
+        'input_repetition_dow',
+        'input_repetition_dow_week',
+        'input_repetition_end_date',
+        'input_repetition_end_type',
+        'input_repetition_interval',
+        'input_repetition_month',
+        'input_repetition_month_type',
+        'input_repetition_type',
+    ],
     props: {
         name: {
             type: String,
diff --git a/resources/vue/components/form_inputs/SerialTextMarkers.vue b/resources/vue/components/form_inputs/SerialTextMarkers.vue
index 20e542dab79671263b56a469b3bd8d928597992a..775f4e96e0fd18dec6fd057bfbbe98b81369872c 100644
--- a/resources/vue/components/form_inputs/SerialTextMarkers.vue
+++ b/resources/vue/components/form_inputs/SerialTextMarkers.vue
@@ -67,7 +67,7 @@ export default {
             }
         });
     },
-    destroyed() {
+    unmounted() {
         STUDIP.eventBus.off('editor-loaded');
     }
 }
diff --git a/resources/vue/components/massmail/MassMailPermissions.vue b/resources/vue/components/massmail/MassMailPermissions.vue
index 6342427f4eef649331a6089ec25cfe5baef785ed..b493dc5fd1b4f19639f4b36eac872abd88165ee5 100644
--- a/resources/vue/components/massmail/MassMailPermissions.vue
+++ b/resources/vue/components/massmail/MassMailPermissions.vue
@@ -26,13 +26,13 @@
                 </td>
                 <td>
                     <div v-if="permission.meta['allowed-degrees-count'] > 0">
-                        {{ $gettextInterpolate($gettext('%{degrees} Abschlüsse'), { degrees: permission.meta['allowed-degrees-count']}) }}
+                        {{ $gettext('%{degrees} Abschlüsse', { degrees: permission.meta['allowed-degrees-count']}) }}
                     </div>
                     <div v-if="permission.meta['allowed-subjects-count'] > 0">
-                        {{ $gettextInterpolate($gettext('%{subjects} Fächer'), { subjects: permission.meta['allowed-subjects-count']}) }}
+                        {{ $gettext('%{subjects} Fächer', { subjects: permission.meta['allowed-subjects-count']}) }}
                     </div>
                     <div v-if="permission.meta['allowed-institutes-count'] > 0">
-                        {{ $gettextInterpolate($gettext('%{institutes} Einrichtungen'), { institutes: permission.meta['allowed-institutes-count']}) }}
+                        {{ $gettext('%{institutes} Einrichtungen', { institutes: permission.meta['allowed-institutes-count']}) }}
                     </div>
                 </td>
                 <td>
diff --git a/resources/vue/components/questionnaires/FreetextEdit.vue b/resources/vue/components/questionnaires/FreetextEdit.vue
index 60852ffa858a249fa45821e5e87c683fc55aa273..d2c05358c0b8997e8b404602411af3f0aa8f94b3 100644
--- a/resources/vue/components/questionnaires/FreetextEdit.vue
+++ b/resources/vue/components/questionnaires/FreetextEdit.vue
@@ -14,10 +14,12 @@
 
 <script>
 import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "../StudipWysiwyg.vue";
 
 export default {
+    extends: QuestionnaireComponent,
     name: 'freetext-edit',
-    mixins: [ QuestionnaireComponent ],
+    components: {StudipWysiwyg},
     created() {
         this.setDefaultValues({
             description: '',
diff --git a/resources/vue/components/questionnaires/InputArray.vue b/resources/vue/components/questionnaires/InputArray.vue
index b26237ca392f5db4abca9d3cae79cf5423fb3861..8d514c574c1fc306c637b75334f094f1314a42ba 100644
--- a/resources/vue/components/questionnaires/InputArray.vue
+++ b/resources/vue/components/questionnaires/InputArray.vue
@@ -17,39 +17,46 @@
                     <th class="actions"></th>
                 </tr>
             </thead>
-            <Draggable v-model="options" handle=".dragarea" tag="tbody" class="statements">
-                <tr v-for="(option, index) in options" :key="index">
-                    <td class="dragcolumn">
-                        <a class="dragarea"
-                           tabindex="0"
-                           :title="$gettextInterpolate($gettext(`Sortierelement für %{label} %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.`), {option, label}, true)"
-                           @keydown="keyHandler($event, index)"
-                           ref="draghandle">
-                            <span class="drag-handle"></span>
-                        </a>
-                    </td>
-                    <td>
-                        <input type="text"
-                               ref="inputs"
-                               :placeholder="label"
-                               @paste="(ev) => onPaste(ev, index)"
-                               v-model="options[index]">
-                    </td>
-                    <slot name="body-cells" />
-                    <td class="actions">
-                        <StudipIcon name="delete"
-                                     shape="trash"
-                                     @click.prevent="deleteOption(index)"
-                                     :title="$gettextInterpolate($gettext('%{label} löschen'), {label}, true)"
-                        />
-                    </td>
-                </tr>
+            <Draggable v-model="options"
+                       item-key="index"
+                       handle=".dragarea"
+                       tag="tbody"
+                       class="statements"
+            >
+                <template #item="{element, index}">
+                    <tr>
+                        <td class="dragcolumn">
+                            <a class="dragarea"
+                               tabindex="0"
+                               :title="$gettext(`Sortierelement für %{label} %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.`, {option: element.value, label}, true)"
+                               @keydown="keyHandler($event, index)"
+                               :ref="`draghandle${index}`">
+                                <span class="drag-handle"></span>
+                            </a>
+                        </td>
+                        <td>
+                            <input type="text"
+                                   ref="inputs"
+                                   :placeholder="label"
+                                   @paste="(ev) => onPaste(ev, index)"
+                                   v-model="element.value">
+                        </td>
+                        <slot name="body-cells" />
+                        <td class="actions">
+                            <StudipIcon name="delete"
+                                        shape="trash"
+                                        @click.prevent="deleteOption(index)"
+                                        :title="$gettext('%{label} löschen', {label: element.value}, true)"
+                            />
+                        </td>
+                    </tr>
+                </template>
             </Draggable>
             <tfoot>
                 <tr>
                     <td :colspan="3 + additionalColspan">
                         <button class="as-link"
-                                :title="$gettextInterpolate($gettext('%{label} hinzufügen'),  {label}, true)"
+                                :title="$gettext('%{label} hinzufügen',  {label}, true)"
                                 @click.prevent="addOption()">
                             <StudipIcon shape="add" alt="" />
                         </button>
@@ -66,6 +73,7 @@ import { $gettext } from '../../../assets/javascripts/lib/gettext';
 
 export default {
     name: 'input-array',
+    emits: ['update:modelValue'],
     components: { Draggable },
     props: {
         additionalColspan: {
@@ -80,17 +88,32 @@ export default {
             type: String,
             default: $gettext('Optionen'),
         },
-        value: Array,
+        modelValue: Array,
     },
     data() {
         return {
-            options: [],
             assistiveLive: '',
         };
     },
+    computed: {
+        options: {
+            get() {
+                return this.modelValue.map((element, index) => ({
+                    value: element,
+                    index: index,
+                }));
+            },
+            set(value) {
+                this.$emit('update:modelValue', value.map(element => element.value));
+            }
+        }
+    },
     methods: {
         addOption(val = '', position = this.options.length) {
-            this.$set(this.options, position, val.trim());
+            this.$set(this.options, position, {
+                value: val.trim(),
+                index: position,
+            });
 
             this.$nextTick(() => {
                 this.$refs.inputs[position].focus();
@@ -120,45 +143,32 @@ export default {
             const moveUp = e.keyCode === 38;
 
             this.moveElement(index, moveUp ? -1 : 1).then((newIndex) => {
-                this.assistiveLive = this.$gettextInterpolate(
-                    this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
+                if (newIndex === false) {
+                    return;
+                }
+
+                this.assistiveLive = this.$gettext(
+                    'Aktuelle Position in der Liste: %{pos} von %{listLength}.',
                     {pos: newIndex + 1, listLength: this.options.length}
                 );
 
                 this.$nextTick(() => {
-                    this.$refs['draghandle'][newIndex].focus();
+                    this.$refs[`draghandle${newIndex}`].focus();
                 });
             })
         },
         moveElement(index, direction) {
             if (this.options[index + direction] === undefined) {
-                return Promise.resolve(index);
+                return Promise.resolve(false);
             }
 
             const indices = [index, index + direction].sort();
 
-            this.options.splice(
-                Math.min(...indices),
-                2,
-                ...indices.reverse().map(idx => this.options[idx])
-            );
+            [this.options[indices[0]], this.options[indices[1]]] = [this.options[indices[1]], this.options[indices[0]]];
+            this.options = [...this.options];
 
             return Promise.resolve(index + direction);
-        }
-    },
-    watch: {
-        options: {
-            handler(current) {
-                this.$emit('input', current);
-            },
-            deep: true
         },
-        value: {
-            handler(current) {
-                this.options = current;
-            },
-            immediate: true
-        }
     }
 }
 </script>
diff --git a/resources/vue/components/questionnaires/LikertEdit.vue b/resources/vue/components/questionnaires/LikertEdit.vue
index 311bf6d4109b7b80392a9d67f67231df5cfcaa58..c25126c02760c83842461701417e7fe29383b8dd 100644
--- a/resources/vue/components/questionnaires/LikertEdit.vue
+++ b/resources/vue/components/questionnaires/LikertEdit.vue
@@ -44,6 +44,7 @@
 import { $gettext } from '../../../assets/javascripts/lib/gettext';
 import InputArray from "./InputArray.vue";
 import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "../StudipWysiwyg.vue";
 
 // This is necesssar since $gettext does not seem to work in data() or created()
 const default_values = () => ({
@@ -62,8 +63,8 @@ const default_values = () => ({
 
 export default {
     name: 'likert-edit',
-    components: { InputArray },
-    mixins: [ QuestionnaireComponent ],
+    extends: QuestionnaireComponent,
+    components: {StudipWysiwyg, InputArray },
     created() {
         this.setDefaultValues(default_values());
     }
diff --git a/resources/vue/components/questionnaires/QuestionnaireEditor.vue b/resources/vue/components/questionnaires/QuestionnaireEditor.vue
index 089353bbfcae65fb0a172a60a0dc6da8c6bc58e8..7512bce5d07c318fc2a7501feb745958c5abc9d5 100644
--- a/resources/vue/components/questionnaires/QuestionnaireEditor.vue
+++ b/resources/vue/components/questionnaires/QuestionnaireEditor.vue
@@ -87,7 +87,8 @@
                 </div>
                 <div v-else>
                     <component :is="componentForQuestionIndex(indexForQuestion)"
-                               v-model="data.questions[indexForQuestion].questiondata"
+                               :model-value="data.questions[indexForQuestion].questiondata"
+                               @update:modelValue="(v) => data.questions[indexForQuestion].questiondata = v"
                                :question_id="data.questions[indexForQuestion].id"
                                :key="data.questions[indexForQuestion].id">
                     </component>
@@ -101,38 +102,47 @@
                     <span class="icon"><studip-icon shape="evaluation" :size="30" alt=""></studip-icon></span>
                     {{ $gettext('Einstellungen') }}
                 </a>
-                <draggable v-if="data.questions.length > 0" v-model="data.questions" handle=".drag-handle" group="questions" class="questions_container questions">
-                    <div v-for="question in data.questions"
-                         :key="question.id"
-                         @mouseenter="hoverTab = question.id"
-                         @mouseleave="hoverTab = null"
-                         :class="(activeTab === question.id || activeTab === 'meta_' + question.id ? 'active' : '') + (hoverTab === question.id ? ' hovered' : '')">
-                        <a href="#"
-                           @click.prevent="switchTab(question.id)">
-                            <span class="drag-handle"></span>
-                            <span class="icon type">
-                                <studip-icon :shape="questionTypes[question.questiontype].icon" :size="30" alt=""></studip-icon>
+                <draggable :list="data.questions"
+                           handle=".drag-handle"
+                           group="questions"
+                           class="questions_container questions"
+                           item-key="id"
+                >
+                    <template #item="{ element }">
+                        <div @mouseenter="hoverTab = element.id"
+                             @mouseleave="hoverTab = null"
+                             :class="{
+                                 active: activeTab === element.id || activeTab === 'meta_' + element.id,
+                                 hovered: hoverTab === element.id,
+                             }"
+                        >
+                            <a href="#"
+                               @click.prevent="switchTab(element.id)">
+                                <span class="drag-handle"></span>
+                                <span class="icon type">
+                                <studip-icon :shape="questionTypes[element.questiontype].icon" :size="30" alt=""></studip-icon>
                             </span>
 
-                            <div v-if="editInternalName !== question.id">{{ question.internal_name || questionTypes[question.questiontype].name}}</div>
-                            <div v-else class="inline_editing">
-                                <input type="text" ref="editInternalName" v-model="tempInternalName" class="inlineediting_internal_name">
-                                <button @click="saveInternalName(question.id)">
-                                    <studip-icon shape="accept" :size="20" :title="$gettext('Internen Namen speichern')"></studip-icon>
-                                </button>
-                                <button @click="editInternalName = null">
-                                    <studip-icon shape="decline" :size="20" :title="$gettext('Internen Namen nicht speichern')"></studip-icon>
-                                </button>
-                            </div>
-                        </a>
+                                <div v-if="editInternalName !== element.id">{{ element.internal_name || questionTypes[element.questiontype].name}}</div>
+                                <div v-else class="inline_editing">
+                                    <input type="text" ref="editInternalName" v-model="tempInternalName" class="inlineediting_internal_name">
+                                    <button @click="saveInternalName(element.id)">
+                                        <studip-icon shape="accept" :size="20" :title="$gettext('Internen Namen speichern')"></studip-icon>
+                                    </button>
+                                    <button @click="editInternalName = null">
+                                        <studip-icon shape="decline" :size="20" :title="$gettext('Internen Namen nicht speichern')"></studip-icon>
+                                    </button>
+                                </div>
+                            </a>
 
-                        <studip-action-menu :items="actionMenuItems"
-                                            @copy="duplicateQuestion(question.id)"
-                                            @rename="renameInternalName(question.id)"
-                                            @moveup="moveQuestionUp(question.id)"
-                                            @movedown="moveQuestionDown(question.id)"
-                                            @delete="deleteQuestion(question.id)"></studip-action-menu>
-                    </div>
+                            <studip-action-menu :items="actionMenuItems"
+                                                @copy="duplicateQuestion(element.id)"
+                                                @rename="renameInternalName(element.id)"
+                                                @moveup="moveQuestionUp(element.id)"
+                                                @movedown="moveQuestionDown(element.id)"
+                                                @delete="deleteQuestion(element.id)"></studip-action-menu>
+                        </div>
+                    </template>
                 </draggable>
                 <a :class="activeTab === 'add_question' ? 'add_question active' : 'add_question'"
                    href="#"
@@ -160,6 +170,7 @@ import md5 from 'md5';
 import StudipIcon from '../StudipIcon.vue';
 import StudipActionMenu from '../StudipActionMenu.vue';
 import Datetimepicker from '../Datetimepicker.vue';
+import {defineAsyncComponent} from 'vue';
 
 const loadedComponents = {};
 
@@ -198,8 +209,8 @@ export default {
             const componentInfo = this.questionTypes[this.data.questions[index].questiontype].component;
             if (loadedComponents[componentInfo[0]] === undefined) {
                 loadedComponents[componentInfo[0]] = componentInfo[1] === ''
-                    ? () => import(`./${componentInfo[0]}.vue`)
-                    : () => import(/* webpackIgnore: true */componentInfo[1]);
+                    ? defineAsyncComponent(() => import(`./${componentInfo[0]}.vue`))
+                    : defineAsyncComponent(() => import(/* webpackIgnore: true */componentInfo[1]));
             }
 
             return loadedComponents[componentInfo[0]];
@@ -329,7 +340,7 @@ export default {
             ];
         },
         activateFormSecure() {
-            return this.form_secured && !this.objectsEqual(this.oldData, this.data);
+            return this.form_secured && !this.objectsEqual(this.oldData, this.data) ? true : null;
         },
         indexForQuestion() {
             return this.getIndexForQuestion(this.activeTab);
diff --git a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
index 57452d49dd531e73517b9f4477ea7d02136b9154..828d24e7c392d62b945cc9c630a9ece226ae0fc3 100644
--- a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
+++ b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
@@ -15,10 +15,12 @@
 
 <script>
 import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "../StudipWysiwyg.vue";
 
 export default {
     name: 'questionnaire-info-edit',
-    mixins: [ QuestionnaireComponent ],
+    extends: QuestionnaireComponent,
+    components: {StudipWysiwyg},
     created() {
         this.setDefaultValues({
             url: '',
diff --git a/resources/vue/components/questionnaires/RangescaleEdit.vue b/resources/vue/components/questionnaires/RangescaleEdit.vue
index 044c921cd349681ef431916573ea93e76fb83efe..b3ff7da61ef1d905909b883d7c50a69f512cc1db 100644
--- a/resources/vue/components/questionnaires/RangescaleEdit.vue
+++ b/resources/vue/components/questionnaires/RangescaleEdit.vue
@@ -52,11 +52,12 @@
 <script>
 import InputArray from './InputArray.vue';
 import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "../StudipWysiwyg.vue";
 
 export default {
     name: 'rangescale-edit',
-    components: { InputArray },
-    mixins: [ QuestionnaireComponent ],
+    extends: QuestionnaireComponent,
+    components: {StudipWysiwyg, InputArray },
     created() {
         this.setDefaultValues({
             alternative_answer: '',
diff --git a/resources/vue/components/questionnaires/VoteEdit.vue b/resources/vue/components/questionnaires/VoteEdit.vue
index 56dd160f4446d683aaa0b3a02bab7ab178fb4f77..b4855cabf912db47426a1fb6e9f1dec6f188d09b 100644
--- a/resources/vue/components/questionnaires/VoteEdit.vue
+++ b/resources/vue/components/questionnaires/VoteEdit.vue
@@ -26,11 +26,12 @@
 <script>
 import InputArray from "./InputArray.vue";
 import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "../StudipWysiwyg.vue";
 
 export default {
+    extends: QuestionnaireComponent,
     name: 'vote-edit',
-    components: { InputArray },
-    mixins: [QuestionnaireComponent],
+    components: {StudipWysiwyg, InputArray },
     created() {
         this.setDefaultValues({
             description: '',
diff --git a/resources/vue/components/responsive/NavigationItem.vue b/resources/vue/components/responsive/NavigationItem.vue
index d65bda39917ec9e73bf1794da91ff2026870e505..8db20f8d38092ea5612a9142aa836b2b93b9a249 100644
--- a/resources/vue/components/responsive/NavigationItem.vue
+++ b/resources/vue/components/responsive/NavigationItem.vue
@@ -58,10 +58,10 @@
 </template>
 
 <script lang="ts">
-import Vue, { PropType } from 'vue';
+import { defineComponent } from 'vue';
 import StudipIcon from '../StudipIcon.vue';
 
-export default Vue.extend({
+export default defineComponent({
     name: 'NavigationItem',
     components: { StudipIcon },
     props: {
@@ -96,13 +96,17 @@ export default Vue.extend({
         hasChildren() {
             return this.item.children && Object.keys(this.item.children).length > 0;
         },
-        navigateToText(itemTitle: string) {
-            return this.$gettextInterpolate(this.$gettext('Navigiere zu %{ title }'), { title: itemTitle });
+        navigateToText(title: string) {
+            return this.$gettext(
+                'Navigiere zu %{title}',
+                { title }
+            );
         },
-        openNavigationText(itemTitle: string): string {
-            return this.$gettextInterpolate(this.$gettext('Unternavigation zu %{ title } öffnen'), {
-                title: itemTitle,
-            });
+        openNavigationText(title: string): string {
+            return this.$gettext(
+                'Unternavigation zu %{title} öffnen',
+                { title }
+            );
         },
     },
 });
diff --git a/resources/vue/components/responsive/ResponsiveContentBar.vue b/resources/vue/components/responsive/ResponsiveContentBar.vue
index d21ae21028c78afa6903d7b9be4ef50cca6f2333..408fe95a9baef157cd3eee294f421f9996547b36 100644
--- a/resources/vue/components/responsive/ResponsiveContentBar.vue
+++ b/resources/vue/components/responsive/ResponsiveContentBar.vue
@@ -1,8 +1,8 @@
 <template>
     <div v-if="realContentbarSource === ''">
-        <MountingPortal mount-to="#responsive-contentbar-container" append>
+        <Teleport to="#responsive-contentbar-container" append>
             <portal-target name="layout-page"></portal-target>
-        </MountingPortal>
+        </Teleport>
         <portal to="layout-page">
             <div id="responsive-contentbar" class="contentbar" ref="contentbar">
                 <div v-if="hasSidebar" class="contentbar-nav" ref="leftNav">
@@ -225,9 +225,10 @@ export default {
         this.globalOn('courseware-contentbar-mounted', this.onCoursewareContentbarMounted)
         this.globalOn('courseware-contentbar-before-destroy', this.onCoursewareContentbarBeforeDestroy);
     },
-    beforeDestroy() {
+    beforeUnmount() {
         this.globalOff('courseware-contentbar-mounted', this.onCoursewareContentbarMounted);
         this.globalOff('courseware-contentbar-before-destroy', this.onCoursewareContentbarBeforeDestroy);
+
         if (this.realContentbar) {
             this.adjustExistingContentbar(false);
         }
diff --git a/resources/vue/components/responsive/ResponsiveNavigation.vue b/resources/vue/components/responsive/ResponsiveNavigation.vue
index e53e19c0914319ae2a9395178f8e0a95fb72f685..33b3ccbc246a3e0d7aca51d4a84da2ba8b5f94ce 100644
--- a/resources/vue/components/responsive/ResponsiveNavigation.vue
+++ b/resources/vue/components/responsive/ResponsiveNavigation.vue
@@ -585,7 +585,7 @@ export default {
         // Check initial state after load
         this.headerMagic = document.querySelector('body').classList.contains('fixed');
     },
-    beforeDestroy() {
+    beforeUnmount() {
         this.classObserver.disconnect();
         this.dialogObserver.disconnect();
         document.getElementById('header-links').style.display = null;
@@ -603,13 +603,11 @@ export default {
     transition: all var(--transition-duration) ease;
 }
 
-.slide-enter-to,
-.slide-leave-from,
-.slide-leave {
+.slide-enter-from,
+.slide-leave-from {
     margin-left: -3px;
 }
 
-.slide-enter,
 .slide-enter-from,
 .slide-leave-to {
     margin-left: -50px;
@@ -620,13 +618,11 @@ export default {
     transition: opacity var(--transition-duration) ease;
 }
 
-.appear-leave,
 .appear-leave-from,
 .appear-enter-to {
     opacity: 1;
 }
 
-.appear-enter,
 .appear-enter-from,
 .appear-leave-to {
     opacity: 0;
diff --git a/resources/vue/components/responsive/ResponsiveSkipLinks.vue b/resources/vue/components/responsive/ResponsiveSkipLinks.vue
index 7668191d0951627f0208e0b6df12e6dfebf091f0..18ccc8d790ae8066afe91bcc72ce756e799f2e4c 100644
--- a/resources/vue/components/responsive/ResponsiveSkipLinks.vue
+++ b/resources/vue/components/responsive/ResponsiveSkipLinks.vue
@@ -1,8 +1,8 @@
 <template>
     <div>
-        <MountingPortal mount-to="#skiplink_list" append>
+        <Teleport to="#skiplink_list" append>
             <portal-target name="additional-skiplinks"></portal-target>
-        </MountingPortal>
+        </Teleport>
         <portal to="additional-skiplinks">
             <li v-for="(link) in links" :key="link.url">
                 <button class="skiplink" role="link" @click.prevent="goto(link.url)">
@@ -39,7 +39,7 @@ export default {
             });
         });
     },
-    beforeDestroy() {
+    beforeUnmount() {
         const buttons = document.querySelectorAll('button.skiplink:not([data-in-fullscreen="1"])');
         buttons.forEach(button => {
             button.style.display = null;
diff --git a/resources/vue/components/stock-images/ActionsWidget.vue b/resources/vue/components/stock-images/ActionsWidget.vue
index ee2d6cfb7e07dcd378600b2c3cdd4d74fafd2050..4553249648c008f4539ed28f2a604fd4ff5cac4a 100644
--- a/resources/vue/components/stock-images/ActionsWidget.vue
+++ b/resources/vue/components/stock-images/ActionsWidget.vue
@@ -18,6 +18,7 @@
 import SidebarWidget from '../SidebarWidget.vue';
 
 export default {
+    emits: ['initiateUpload', 'initiateZipUpload'],
     components: {
         SidebarWidget,
     },
diff --git a/resources/vue/components/stock-images/AttributesFieldset.vue b/resources/vue/components/stock-images/AttributesFieldset.vue
index a88501c13b71690f66417a57d24f63719d00d489..bdf3875cd55292568dab0c4430aeb754084b40d1 100644
--- a/resources/vue/components/stock-images/AttributesFieldset.vue
+++ b/resources/vue/components/stock-images/AttributesFieldset.vue
@@ -18,7 +18,7 @@
         </label>
         <label>
             {{ $gettext('Tags') }}
-            <TagsInput v-model="tags" :suggestions="suggestedTags" />
+            <TagsInput v-model:tags="tags" :suggestions="suggestedTags" />
         </label>
     </div>
 </template>
@@ -28,6 +28,7 @@ import TagsInput from './TagsInput.vue';
 export default {
     props: ['metadata', 'suggestedTags'],
     components: { TagsInput },
+    emits: ['change'],
     computed: {
         author: {
             get() {
diff --git a/resources/vue/components/stock-images/ColorFilterWidget.vue b/resources/vue/components/stock-images/ColorFilterWidget.vue
index b816bdb33a6b63bc9c61c1c28b2734d7327be8be..79d403881cb3c79974c8d3fbcf02e608f0346fe0 100644
--- a/resources/vue/components/stock-images/ColorFilterWidget.vue
+++ b/resources/vue/components/stock-images/ColorFilterWidget.vue
@@ -26,10 +26,7 @@ import SidebarWidget from '../SidebarWidget.vue';
 import { orientations } from './filters.js';
 
 export default {
-    model: {
-        prop: 'filters',
-        event: 'change',
-    },
+    emits: ['update:filters'],
     props: {
         filters: {
             type: Object,
@@ -48,7 +45,7 @@ export default {
     methods: {
         onVueSelectInput(selectedColors) {
             const colors = selectedColors.map(({ hex }) => hex);
-            this.$emit('change', { ...this.filters, colors });
+            this.$emit('update:filters', { ...this.filters, colors });
         },
     },
     mounted() {
diff --git a/resources/vue/components/stock-images/EditDialog.vue b/resources/vue/components/stock-images/EditDialog.vue
index 68c71e2331033c5e063568f9817c13ee21dc54aa..c3e6cb9f850d989b17374383414a60d6f4b54360 100644
--- a/resources/vue/components/stock-images/EditDialog.vue
+++ b/resources/vue/components/stock-images/EditDialog.vue
@@ -42,6 +42,7 @@ import AttributesFieldset from './AttributesFieldset.vue';
 export default {
     props: ['stockImage', 'suggestedTags'],
     components: { AttributesFieldset, ThumbnailCard },
+    emits: ['cancel', 'confirm'],
     data: () => ({
         metadata: {},
     }),
diff --git a/resources/vue/components/stock-images/ImagesList.vue b/resources/vue/components/stock-images/ImagesList.vue
index 7d2c0db9b73f198f7bececea425cd9cfbaae7b98..41a3d6f893cdec4c5122a1254b7ae10a7b861c75 100644
--- a/resources/vue/components/stock-images/ImagesList.vue
+++ b/resources/vue/components/stock-images/ImagesList.vue
@@ -89,6 +89,7 @@ import ImagesListItem from './ImagesListItem.vue';
 import { mapActions } from 'vuex';
 
 export default {
+    emits: ['checked', 'open-page', 'search', 'select'],
     props: {
         checkedImages: {
             type: Array,
@@ -120,11 +121,16 @@ export default {
     }),
     computed: {
         allChecked() {
-            return this.paged.length && this.paged.every(({ id }) => this.checkedImages.includes(id));
+            return this.paged.length && this.paged.every(({ id }) => this.checkedImages.includes(id)) ? true : null;
         },
         caption() {
             const n = this.stockImages.length;
-            return this.$gettextInterpolate(this.$ngettext('%{ n } Bild gefunden', '%{ n } Bilder gefunden', n), { n });
+            return this.$ngettext(
+                '%{ n } Bild gefunden',
+                '%{ n } Bilder gefunden',
+                n,
+                { n }
+            );
         },
         paged() {
             return this.stockImages.slice((this.page - 1) * this.perPage, this.page * this.perPage);
@@ -163,9 +169,12 @@ export default {
         },
     },
     watch: {
-        checkedImages({ length }) {
-            this.$refs.checkAll.indeterminate = 0 < length && length < this.paged.length;
-        },
+        checkedImages: {
+            handler({length}) {
+                this.$refs.checkAll.indeterminate = 0 < length && length < this.paged.length;
+            },
+            deep: true
+        }
     },
 };
 </script>
diff --git a/resources/vue/components/stock-images/ImagesListItem.vue b/resources/vue/components/stock-images/ImagesListItem.vue
index 1e1dda29f698c041acb3ffb7cb1906ace94b6c45..6143a22d77667e46f4b91a6cc2951a2e27e29c48 100644
--- a/resources/vue/components/stock-images/ImagesListItem.vue
+++ b/resources/vue/components/stock-images/ImagesListItem.vue
@@ -2,9 +2,9 @@
     <tr @click="onSelect">
         <td>
             <label>
-                <input type="checkbox" :checked="isChecked" @change="onCheckboxChange" />
+                <input type="checkbox" :checked="isChecked ? true : null" @change="onCheckboxChange" />
                 <span class="sr-only">{{
-                    $gettextInterpolate($gettext('%{context} auswählen'), { context: stockImage.attributes.title })
+                    $gettext('%{context} auswählen', { context: stockImage.attributes.title })
                 }}</span>
             </label>
         </td>
@@ -58,6 +58,7 @@ import Thumbnail from './Thumbnail.vue';
 import { getFormat } from './format.js';
 
 export default {
+    emits: ['checked', 'search', 'select'],
     props: {
         stockImage: {
             type: Object,
diff --git a/resources/vue/components/stock-images/ImagesPagination.vue b/resources/vue/components/stock-images/ImagesPagination.vue
index 16dd96ab0a7728ea983fc3f7ed0c80267daf852b..2d6a013512180c00925db22b33b907d0706812aa 100644
--- a/resources/vue/components/stock-images/ImagesPagination.vue
+++ b/resources/vue/components/stock-images/ImagesPagination.vue
@@ -23,10 +23,7 @@ import StudipPagination from '../StudipPagination.vue';
 
 export default {
     components: { StudipPagination },
-    model: {
-        prop: 'page',
-        event: 'change',
-    },
+    emits: ['update:page'],
     props: {
         stockImages: {
             type: Array,
@@ -51,7 +48,7 @@ export default {
     },
     methods: {
         onUpdateOffset(offset) {
-            this.$emit('change', offset + 1);
+            this.$emit('update:page', offset + 1);
         },
     },
 };
diff --git a/resources/vue/components/stock-images/MetadataBox.vue b/resources/vue/components/stock-images/MetadataBox.vue
index ac819de4c7446025ca0127970db056d2a7235144..7395b768ce83fc8f6f9b22450556a69299911c0c 100644
--- a/resources/vue/components/stock-images/MetadataBox.vue
+++ b/resources/vue/components/stock-images/MetadataBox.vue
@@ -11,7 +11,7 @@
             />
         </div>
         <div>
-            <AttributesFieldset :metadata="metadata" :suggested-tags="suggestedTags" @change="onChange" />
+            <AttributesFieldset :metadata="metadata" :suggested-tags="suggestedTags" @change="metadata => onChange(metadata)" />
         </div>
     </div>
 </template>
@@ -26,6 +26,8 @@ export default {
 
     components: { AttributesFieldset, ThumbnailCard },
 
+    emits: ['change'],
+
     data: () => ({
         fileURL: null,
         height: null,
@@ -39,7 +41,7 @@ export default {
                 return this.metadata.tags;
             },
             set(tags) {
-                this.$set(this.metadata, 'tags', tags);
+                this.$emit('change', { ...this.metadata, tags });
             },
         },
     },
@@ -58,10 +60,10 @@ export default {
             this.width = target.width;
         };
         this.image.src = this.fileURL;
-        this.$set(this.metadata, 'title', this.file.name);
+        this.$emit('change', { ...this.metadata, title: this.file.name });
     },
 
-    beforeDestroy() {
+    beforeUnmount() {
         if (this.fileURL) {
             URL.revokeObjectURL(this.fileURL);
         }
diff --git a/resources/vue/components/stock-images/OrientationFilterWidget.vue b/resources/vue/components/stock-images/OrientationFilterWidget.vue
index 5ae4a2c4dee06154927e5425f8ba84bbd7ab0040..933b66a4b5acaa7bf3f8518846ade2dbc34aa761 100644
--- a/resources/vue/components/stock-images/OrientationFilterWidget.vue
+++ b/resources/vue/components/stock-images/OrientationFilterWidget.vue
@@ -17,10 +17,7 @@ import SidebarWidget from '../SidebarWidget.vue';
 import { orientations } from './filters.js';
 
 export default {
-    model: {
-        prop: 'filters',
-        event: 'change',
-    },
+    emits: ['update:filters'],
     props: {
         filters: {
             type: Object,
@@ -36,7 +33,7 @@ export default {
                 return this.filters.orientation;
             },
             set(orientation) {
-                this.$emit('change', { ...this.filters, orientation });
+                this.$emit('update:filters', { ...this.filters, orientation });
             }
         },
         orientations() {
diff --git a/resources/vue/components/stock-images/Page.vue b/resources/vue/components/stock-images/Page.vue
index 3f06042b54fa5b1630636e2fe6f5fd2c7b2ba490..b27cc52645793979492f3bc61d4d9f4561a11186 100644
--- a/resources/vue/components/stock-images/Page.vue
+++ b/resources/vue/components/stock-images/Page.vue
@@ -7,7 +7,7 @@
             v-show="!showUploadIndicator"
             :per-page="perPage"
             :stock-images="filteredStockImages"
-            v-model="page"
+            v-model:page="page"
         >
             <ImagesList
                 :checked-images="checkedImages"
@@ -26,12 +26,12 @@
             :description="$gettext('Bilder werden hochgeladen...')"
         >
         </studip-progress-indicator>
-        <MountingPortal mountTo="#stock-images-widget" name="sidebar-stock-images">
+        <Teleport to="#stock-images-widget" name="sidebar-stock-images">
             <SearchWidget :query="query" @search="onSearch" />
-            <OrientationFilterWidget v-model="filters" />
-            <ColorFilterWidget v-model="filters" />
+            <OrientationFilterWidget v-model:filters="filters" />
+            <ColorFilterWidget v-model:filters="filters" />
             <ActionsWidget @initiateUpload="onUploadDialogShow" @initiateZipUpload="onZipUploadDialogShow" />
-        </MountingPortal>
+        </Teleport>
         <EditDialog
             :stock-image="selectedImage"
             :suggested-tags="suggestedTags"
@@ -150,15 +150,11 @@ export default {
                     this.showUploadIndicator = false;
                     this.showZipUploadMessage = true;
                     this.zipUploadMessageType = 'success';
-                    this.zipUploadMessage = this.$gettextInterpolate(
-                        this.$ngettext(
-                            '%{length} Bild wurde hinzugefügt',
-                            '%{length} Bilder wurden hinzugefügt',
-                            resp.data['image-count']
-                        ),
-                        {
-                            length: resp.data['image-count'],
-                        }
+                    this.zipUploadMessage = this.$ngettext(
+                        '%{length} Bild wurde hinzugefügt',
+                        '%{length} Bilder wurden hinzugefügt',
+                        resp.data['image-count'],
+                        { length: resp.data['image-count'] }
                     );
                     this.$nextTick(() => {
                         this.fetchStockImages();
diff --git a/resources/vue/components/stock-images/SearchWidget.vue b/resources/vue/components/stock-images/SearchWidget.vue
index 53e0aa715a34144b62cab9a158ff52be35a19b13..7c09973dccc0337928c8220a14dde54d8198749e 100644
--- a/resources/vue/components/stock-images/SearchWidget.vue
+++ b/resources/vue/components/stock-images/SearchWidget.vue
@@ -39,6 +39,7 @@
 import SidebarWidget from '../SidebarWidget.vue';
 
 export default {
+    emits: ['search'],
     props: {
         query: {
             type: String,
diff --git a/resources/vue/components/stock-images/Selector.vue b/resources/vue/components/stock-images/Selector.vue
index 4e34f9f6f3a33806b74a2677c59f568809ae9d45..bba39d2475667c7383802047e381d1e7a900d750 100644
--- a/resources/vue/components/stock-images/Selector.vue
+++ b/resources/vue/components/stock-images/Selector.vue
@@ -10,7 +10,7 @@
         </div>
         <ul>
             <li v-for="stockImage in filteredStockImages" :key="stockImage.id">
-                <SelectableImageCard :stock-image="stockImage" @click.native="onSelectImage(stockImage)" @keyup.enter.native="onSelectImage(stockImage)" />
+                <SelectableImageCard :stock-image="stockImage" @click="onSelectImage(stockImage)" @keyup.enter="onSelectImage(stockImage)" />
             </li>
         </ul>
     </div>
@@ -22,6 +22,7 @@ import SelectableImageCard from './SelectableImageCard.vue';
 import { searchFilterAndSortImages } from './filters.js';
 
 export default {
+    emits: ['select'],
     props: {
         stockImages: {
             type: Array,
diff --git a/resources/vue/components/stock-images/SelectorDialog.vue b/resources/vue/components/stock-images/SelectorDialog.vue
index c6c46f8f88de0334e3645187d569c31d83d27a07..fa9743e72ca4980cdd4a227838d003ad50f4a88e 100644
--- a/resources/vue/components/stock-images/SelectorDialog.vue
+++ b/resources/vue/components/stock-images/SelectorDialog.vue
@@ -17,6 +17,7 @@ import { mapActions, mapGetters } from 'vuex';
 import Selector from './Selector.vue';
 
 export default {
+    emits: ['close', 'select'],
     data: () => ({
         query: '',
         selectedImage: null,
diff --git a/resources/vue/components/stock-images/SelectorSearch.vue b/resources/vue/components/stock-images/SelectorSearch.vue
index 849e91541a821b7672eaf70a8ba216e04cd6f025..f9b138fb00a8aacc2d8d5983c9b809f3068fadf6 100644
--- a/resources/vue/components/stock-images/SelectorSearch.vue
+++ b/resources/vue/components/stock-images/SelectorSearch.vue
@@ -12,7 +12,7 @@
             <ActiveFilter
                 v-for="color in selectedColors"
                 :key="color.hex"
-                :name="$gettextInterpolate($gettext('Farbe %{color}'), { color: color.name })"
+                :name="$gettext('Farbe %{color}', { color: color.name })"
                 @remove="onRemoveColorFilter(color)"
             >
                 <label>
@@ -68,6 +68,7 @@ import { colors as selectableColors } from './colors.js';
 import { orientations, similarColors } from './filters.js';
 
 export default {
+    emits: ['search', 'update-active-filters'],
     props: {
         activeFilters: {
             type: Object,
diff --git a/resources/vue/components/stock-images/TagsInput.vue b/resources/vue/components/stock-images/TagsInput.vue
index 7e7f11587767c1965e8a0d4a370b50a2e6aed875..bcdca9d16f126d7cc730de10c6086f666ca5f131 100644
--- a/resources/vue/components/stock-images/TagsInput.vue
+++ b/resources/vue/components/stock-images/TagsInput.vue
@@ -3,7 +3,7 @@
         <span class="sr-only">{{
             $gettext('Um einen Tag zu erstellen, schließen Sie Ihre Eingabe mit der Eingabetaste ab.')
         }}</span>
-        <TagsInput
+        <VueTagsInput
             v-model="tag"
             :add-on-key="[13, ';']"
             :autocomplete-items="filteredItems"
@@ -31,20 +31,17 @@
                     :title="$gettext('Tag entfernen')"
                 />
             </template>
-        </TagsInput>
+        </VueTagsInput>
     </div>
 </template>
 <script>
-import TagsInput from '@johmun/vue-tags-input';
+import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
 
 const fromSimpleTags = (array) => array.map((text) => ({ text }));
 const toSimpleTags = (tags) => tags.map(({ text }) => text);
 
 export default {
-    model: {
-        prop: 'tags',
-        event: 'change',
-    },
+    emits: ['update:tags'],
     props: {
         tags: {
             type: Array,
@@ -56,7 +53,7 @@ export default {
         },
     },
 
-    components: { TagsInput },
+    components: { VueTagsInput },
 
     data: () => ({
         tag: '',
@@ -77,7 +74,7 @@ export default {
 
     methods: {
         onTagsChanged(newTags) {
-            this.$emit('change', toSimpleTags(newTags));
+            this.$emit('update:tags', toSimpleTags(newTags));
         },
     },
 
diff --git a/resources/vue/components/stock-images/UploadBox.vue b/resources/vue/components/stock-images/UploadBox.vue
index 7eeb8631eeec512b36c091a35283ab1e368d11f2..eace13cf9f3e43865b58da03d1b4668fa41376dd 100644
--- a/resources/vue/components/stock-images/UploadBox.vue
+++ b/resources/vue/components/stock-images/UploadBox.vue
@@ -20,6 +20,7 @@
 
 <script>
 export default {
+    emits: ['upload'],
     props: {
         type: {
             type: String,
diff --git a/resources/vue/components/stock-images/UploadDialog.vue b/resources/vue/components/stock-images/UploadDialog.vue
index 51040b265c9f6de44ac03741f429d71956aa5a63..7ac86a329285e5dafea80b346e7dd0ba5cec138e 100644
--- a/resources/vue/components/stock-images/UploadDialog.vue
+++ b/resources/vue/components/stock-images/UploadDialog.vue
@@ -21,7 +21,7 @@
                     :file="file"
                     :metadata="metadata"
                     :suggested-tags="suggestedTags"
-                    @change="onChangeMetadata"
+                    @change="metadata => onChangeMetadata(metadata)"
                 />
             </form>
         </template>
@@ -48,6 +48,7 @@ const STATES = { IDLE: 'idle', UPLOADED: 'uploaded' };
 export default {
     props: ['show', 'suggestedTags'],
     components: { MetadataBox, UploadBox },
+    emits: ['cancel', 'confirm'],
     data: () => ({
         file: null,
         metadata: {
diff --git a/resources/vue/components/stock-images/ZipUploadDialog.vue b/resources/vue/components/stock-images/ZipUploadDialog.vue
index e2818343f4fc55392d7d69242ee9f13b3f594f25..102be161592ce55dba313b713a95ef0e6a9ea1fa 100644
--- a/resources/vue/components/stock-images/ZipUploadDialog.vue
+++ b/resources/vue/components/stock-images/ZipUploadDialog.vue
@@ -27,6 +27,7 @@
 <script>
 export default {
     name: 'ZipUploadDialog',
+    emits: ['cancel', 'confirm'],
     props: {
         show: {
             type: Boolean,
diff --git a/resources/vue/components/stock-images/colors.js b/resources/vue/components/stock-images/colors.js
index 910b1435d308de8fa209e5c3d0d618a166f4e2e7..923b9a15ee767da5cf4a56df05790012bc6567ee 100644
--- a/resources/vue/components/stock-images/colors.js
+++ b/resources/vue/components/stock-images/colors.js
@@ -1,4 +1,6 @@
-import { $gettext } from '@/assets/javascripts/lib/gettext';
+import gettext from '@/assets/javascripts/lib/gettext';
+
+const { $gettext } = gettext;
 
 const colors = [
     { name: $gettext('Schwarz'), hex: '#000000' },
diff --git a/resources/vue/components/stock-images/filters.js b/resources/vue/components/stock-images/filters.js
index 42de27dc6fc346f817271d885b8ab65d6165c170..c450f8f61e937420faa3914b35ffe9d9ec2dc5b4 100644
--- a/resources/vue/components/stock-images/filters.js
+++ b/resources/vue/components/stock-images/filters.js
@@ -1,4 +1,5 @@
 import { $gettext } from '@/assets/javascripts/lib/gettext';
+
 import { fromHex, rgbToCIELab, cie94 } from 'colorpare';
 
 const SQUARE_DELTA = 1.1;
diff --git a/resources/vue/components/tree/AssignLinkWidget.vue b/resources/vue/components/tree/AssignLinkWidget.vue
index fec478f43b994f4c5572980bd82a6b013acb46c2..7ca99ff3242c7b898dbd9b02790c2876c47bd8c1 100644
--- a/resources/vue/components/tree/AssignLinkWidget.vue
+++ b/resources/vue/components/tree/AssignLinkWidget.vue
@@ -21,7 +21,7 @@ export default {
     mixins: [ TreeMixin ],
     props: {
         node: {
-            type: String,
+            type: Object,
             required: true
         },
         courses: {
diff --git a/resources/vue/components/tree/StudipTree.vue b/resources/vue/components/tree/StudipTree.vue
index fc6dc41dd57ad267163aa6265a1f037ab6c1b41f..999840e150bc6a0ea78c1a60eeb6a689c5252960 100644
--- a/resources/vue/components/tree/StudipTree.vue
+++ b/resources/vue/components/tree/StudipTree.vue
@@ -30,14 +30,14 @@
         <div v-else class="studip-tree">
             <tree-search-result :search-config="searchConfig"></tree-search-result>
         </div>
-        <MountingPortal v-if="withSearch" mountTo="#search-widget" name="sidebar-search">
+        <Teleport v-if="withSearch" to="#search-widget" name="sidebar-search">
             <search-widget v-if="currentNode" :min-length="3" ref="searchWidget"></search-widget>
-        </MountingPortal>
-        <MountingPortal v-if="!editable && !isSearching && !isLoading && currentNode"
-                        mountTo="#views-widget"
+        </Teleport>
+        <Teleport v-if="!editable && !isSearching && !isLoading && currentNode"
+                        to="#views-widget"
                         name="sidebar-views">
             <studip-tree-view-widget :config="viewConfig" />
-        </MountingPortal>
+        </Teleport>
     </div>
 </template>
 
diff --git a/resources/vue/components/tree/StudipTreeList.vue b/resources/vue/components/tree/StudipTreeList.vue
index c57c31f3ac7e042722b1196362e0cb8efd7846b3..a5db1722f672cc47a8b18e31e5a141ba40418a39 100644
--- a/resources/vue/components/tree/StudipTreeList.vue
+++ b/resources/vue/components/tree/StudipTreeList.vue
@@ -15,8 +15,7 @@
                 <a v-if="editable && currentNode.attributes.id !== 'root'"
                    :href="editUrl + '/' + currentNode.attributes.id"
                    @click.prevent="editNode(editUrl, currentNode.id)" data-dialog="size=medium"
-                   :title="$gettextInterpolate($gettext('%{name} bearbeiten'), {name: currentNode.attributes.name}, true)"
-                >
+                   :title="$gettext('%{name} bearbeiten', {name: currentNode.attributes.name}, true)">
                     <studip-icon shape="edit"></studip-icon>
                 </a>
             </h1>
@@ -31,19 +30,26 @@
             <h1>
                 {{ $gettext('Unterebenen') }}
             </h1>
-            <draggable v-model="children" handle=".drag-handle" :animation="300" tag="ul"
-                       class="studip-tree-children" @end="dropChild">
-                <li v-for="(child, index) in children" :key="index" class="studip-tree-child">
-                    <a v-if="editable && children.length > 1" class="drag-link"
-                       tabindex="0"
-                       :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name}, true)"
-                       @keydown="keyHandler($event, index)"
-                       :ref="'draghandle-' + index">
-                        <span class="drag-handle"></span>
-                    </a>
-                    <tree-node-tile :node="child" :semester="withCourses ? semester : 'all'" :sem-class="semClass"
-                                    :url="nodeUrl(child.id, semester !== 'all' ? semester : null)"></tree-node-tile>
-                </li>
+            <draggable v-model="children"
+                       handle=".drag-handle"
+                       :animation="300"
+                       tag="ul"
+                       class="studip-tree-children"
+                       item-key="id"
+                       @end="dropChild">
+                <template #item="{element, index}">
+                    <li class="studip-tree-child">
+                        <a v-if="editable && children.length > 1" class="drag-link"
+                           tabindex="0"
+                           :title="$gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {node: child.attributes.name}, true)"
+                           @keydown="keyHandler($event, index)"
+                           :ref="'draghandle-' + index">
+                            <span class="drag-handle"></span>
+                        </a>
+                        <tree-node-tile :node="element" :semester="withCourses ? semester : 'all'" :sem-class="semClass"
+                                        :url="nodeUrl(element.id, semester !== 'all' ? semester : null)"></tree-node-tile>
+                    </li>
+                </template>
             </draggable>
         </nav>
         <section v-else-if="withChildren && !currentNode.attributes['has-children']"  class="studip-tree-node-no-children">
@@ -92,8 +98,8 @@
                 <tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course">
                     <td>
                         <a :href="courseUrl(course.id)" tabindex="0"
-                           :title="$gettextInterpolate(
-                               $gettext('Zur Veranstaltung %{ title }'),
+                           :title="$gettext(
+                               'Zur Veranstaltung %{ title }',
                                { title: course.attributes.title },
                                true
                            )">
@@ -122,14 +128,14 @@
                 </tr>
             </tfoot>
         </table>
-        <MountingPortal v-if="showExport" mountTo="#export-widget" name="sidebar-export">
+        <Teleport v-if="showExport" to="#export-widget" name="sidebar-export">
             <tree-export-widget v-if="courses.length > 0"
                                 :title="$gettext('Veranstaltungen exportieren')" :url="exportUrl()"
                                 :export-data="courses"></tree-export-widget>
-        </MountingPortal>
-        <MountingPortal v-if="withCourseAssign" mountTo="#assign-widget" name="sidebar-assign-courses">
+        </Teleport>
+        <Teleport v-if="withCourseAssign" to="#assign-widget" name="sidebar-assign-courses">
             <assign-link-widget v-if="courses.length > 0" :node="currentNode" :courses="courses"></assign-link-widget>
-        </MountingPortal>
+        </Teleport>
     </article>
 </template>
 
@@ -151,6 +157,7 @@ export default {
         AssignLinkWidget, StudipPagination
     },
     mixins: [ TreeMixin ],
+    emits: ['change-current-node'],
     props: {
         node: {
             type: Object,
@@ -278,8 +285,8 @@ export default {
                     this.decreasePosition(index);
                     this.$nextTick(() => {
                         this.$refs['draghandle-' + (index - 1)][0].focus();
-                        this.assistiveLive = this.$gettextInterpolate(
-                            this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
+                        this.assistiveLive = this.$gettext(
+                            'Aktuelle Position in der Liste: %{pos} von %{listLength}.',
                             { pos: index, listLength: this.children.length }
                         );
                     });
@@ -289,8 +296,8 @@ export default {
                     this.increasePosition(index);
                     this.$nextTick(function () {
                         this.$refs['draghandle-' + (index + 1)][0].focus();
-                        this.assistiveLive = this.$gettextInterpolate(
-                            this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
+                        this.assistiveLive = this.$gettext(
+                            'Aktuelle Position in der Liste: %{pos} von %{listLength}.',
                             { pos: index + 2, listLength: this.children.length }
                         );
                     });
@@ -384,7 +391,7 @@ export default {
             semesterForm.appendChild(nodeField);
         });
     },
-    beforeDestroy() {
+    beforeUnmount() {
         STUDIP.eventBus.off('open-tree-node');
         STUDIP.eventBus.off('load-tree-node');
         STUDIP.eventBus.off('sort-tree-children');
diff --git a/resources/vue/components/tree/StudipTreeNode.vue b/resources/vue/components/tree/StudipTreeNode.vue
index 8eb13fa0f0c1b9e17fdd2752fb55b96e06b886ff..78691c890a00fda5e80d46eb5331e9bbd3b408aa 100644
--- a/resources/vue/components/tree/StudipTreeNode.vue
+++ b/resources/vue/components/tree/StudipTreeNode.vue
@@ -272,7 +272,7 @@ export default {
             }
         });
     },
-    beforeDestroy() {
+    beforeUnmount() {
         STUDIP.eventBus.off('sort-tree-children');
     }
 }
diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue
index 89e85df40d69fc4265538a9f4e67bda94c5ce878..acbb1045388f5895be355eb2ed7b2fcd0de9c92c 100644
--- a/resources/vue/components/tree/StudipTreeTable.vue
+++ b/resources/vue/components/tree/StudipTreeTable.vue
@@ -41,13 +41,10 @@
             </span>
         </section>
 
-        <table v-if="currentNode.attributes['has-children'] || courses.length > 0" class="default">
+        <table v-if="currentNode.attributes['has-children']" class="default">
             <caption class="studip-tree-node-info">
                 <span v-if="withChildren && children.length > 0">
-                    {{ $gettextInterpolate($gettext('%{ count } Unterebenen'), { count: children.length }) }}
-                </span>
-                <span v-if="withChildren && children.length > 0 && withCourses && courses.length > 0">
-                    ,
+                    {{ $gettext('%{ count } Unterebenen', { count: children.length }) }}
                 </span>
             </caption>
             <colgroup>
@@ -57,15 +54,6 @@
                 <col style="width: 40%">
             </colgroup>
             <thead>
-                <tr v-if="totalCourseCount > limit">
-                    <td colspan="4">
-                        <studip-pagination :items-per-page="limit"
-                                           :total-items="totalCourseCount"
-                                           :current-offset="offset"
-                                           @updateOffset="updateOffset"
-                        />
-                    </td>
-                </tr>
                 <tr>
                     <th></th>
                     <th>{{ $gettext('Typ') }}</th>
@@ -73,43 +61,53 @@
                     <th>{{ $gettext('Information') }}</th>
                 </tr>
             </thead>
-            <draggable v-model="children" handle=".drag-handle" :animation="300"
-                       @end="dropChild" tag="tbody" role="listbox">
-                <tr v-for="(child, index) in children" :key="index" class="studip-tree-child">
-                    <td>
-                        <a v-if="editable && children.length > 1" class="drag-link" role="option"
-                           tabindex="0"
-                           :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name}, true)"
-                           @keydown="keyHandler($event, index)"
-                           :ref="'draghandle-' + index">
-                            <span class="drag-handle"></span>
-                        </a>
-                    </td>
-                    <td>
-                        <a :href="nodeUrl(child.id, semester !== 'all' ? semester : null)" tabindex="0"
-                           @click.prevent="openNode(child)"
-                           :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'),
-                                { node: node.attributes.name }, true)">
-                            <studip-icon :shape="child.attributes['has-children'] ? 'folder-full' : 'folder-empty'"
-                                         :size="26"></studip-icon>
-                        </a>
-                    </td>
-                    <td>
-                        <a :href="nodeUrl(child.id, semester !== 'all' ? semester : null)" tabindex="0"
-                           @click.prevent="openNode(child)"
-                           :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'),
-                                { node: node.attributes.name }, true)">
-                            {{ child.attributes.name }}
-                        </a>
-                    </td>
-                    <td>
-                        <tree-node-course-info v-if="node.attributes.ancestors.length > 2"
-                                               :node="child"
-                                               :semester="semester"
-                                               :sem-class="semClass"
-                        ></tree-node-course-info>
-                    </td>
-                </tr>
+            <draggable v-model="children"
+                       handle=".drag-handle"
+                       :animation="300"
+                       @end="dropChild"
+                       tag="tbody"
+                       item-key="id"
+                       role="listbox"
+            >
+                <template #item="{element, index}">
+                    <StudipTreeTableRows :element="element"
+                                         :editable="editable"
+                                         :children="children"
+                                         :index="index"
+                                         :semester="semester"
+                                         :sem-class="semClass"
+                                         :node="node"
+                                         @open:node="element => openNode(element)"
+                    ></StudipTreeTableRows>
+                </template>
+            </draggable>
+        </table>
+
+        <table v-if="courses.length > 0" class="default">
+            <colgroup>
+                <col style="width: 20px">
+                <col style="width: 30px">
+                <col>
+                <col style="width: 40%">
+            </colgroup>
+            <thead>
+            <tr v-if="totalCourseCount > limit">
+                <td colspan="4">
+                    <studip-pagination :items-per-page="limit"
+                                       :total-items="totalCourseCount"
+                                       :current-offset="offset"
+                                       @updateOffset="updateOffset"
+                    />
+                </td>
+            </tr>
+            <tr>
+                <th></th>
+                <th>{{ $gettext('Typ') }}</th>
+                <th>{{ $gettext('Name') }}</th>
+                <th>{{ $gettext('Information') }}</th>
+            </tr>
+            </thead>
+            <tbody role="listbox">
                 <tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course">
                     <td></td>
                     <td>
@@ -117,11 +115,12 @@
                     </td>
                     <td>
                         <a :href="courseUrl(course.id)" tabindex="0"
-                           :title="$gettextInterpolate(
-                               $gettext('Zur Veranstaltung %{ title }'),
+                           :title="$gettext(
+                               'Zur Veranstaltung %{ title }',
                                { title: course.attributes.title },
                                true
-                           )">
+                           )"
+                        >
                             <template v-if="course.attributes['course-number']">
                                 {{ course.attributes['course-number'] }}
                             </template>
@@ -133,7 +132,7 @@
                         <tree-course-details :course="course.id"></tree-course-details>
                     </td>
                 </tr>
-            </draggable>
+            </tbody>
             <tfoot v-if="totalCourseCount > limit">
                 <tr>
                     <td colspan="4">
@@ -146,13 +145,14 @@
                 </tr>
             </tfoot>
         </table>
-        <MountingPortal v-if="showExport" mountTo="#export-widget" name="sidebar-export">
+
+        <Teleport v-if="showExport" to="#export-widget" name="sidebar-export">
             <tree-export-widget v-if="courses.length > 0" :title="$gettext('Download des Ergebnisses')" :url="exportUrl()"
                                 :export-data="courses"></tree-export-widget>
-        </MountingPortal>
-        <MountingPortal v-if="withCourseAssign" mountTo="#assign-widget" name="sidebar-assign-courses">
+        </Teleport>
+        <Teleport v-if="withCourseAssign" to="#assign-widget" name="sidebar-assign-courses">
             <assign-link-widget v-if="courses.length > 0" :node="currentNode" :courses="courses"></assign-link-widget>
-        </MountingPortal>
+        </Teleport>
     </article>
 </template>
 
@@ -162,18 +162,23 @@ import { TreeMixin } from '../../mixins/TreeMixin';
 import TreeExportWidget from './TreeExportWidget.vue';
 import TreeBreadcrumb from './TreeBreadcrumb.vue';
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-import StudipIcon from '../StudipIcon.vue';
-import TreeNodeCourseInfo from './TreeNodeCourseInfo.vue';
-import TreeCourseDetails from "./TreeCourseDetails.vue";
 import AssignLinkWidget from "./AssignLinkWidget.vue";
+import StudipPagination from "../StudipPagination.vue";
+import StudipTreeTableRows from "./StudipTreeTableRows.vue";
+import TreeCourseDetails from "./TreeCourseDetails.vue";
+import StudipIcon from "../StudipIcon.vue";
 
 export default {
     name: 'StudipTreeTable',
     components: {
-        draggable, TreeExportWidget, TreeCourseDetails, StudipIcon, StudipProgressIndicator, TreeBreadcrumb,
-        TreeNodeCourseInfo, AssignLinkWidget
+        StudipIcon, TreeCourseDetails,
+        StudipTreeTableRows,
+        StudipPagination,
+        draggable, TreeExportWidget, StudipProgressIndicator, TreeBreadcrumb,
+        AssignLinkWidget
     },
     mixins: [ TreeMixin ],
+    emits: ['change-current-node'],
     props: {
         node: {
             type: Object,
@@ -302,8 +307,8 @@ export default {
                     this.decreasePosition(index);
                     this.$nextTick(() => {
                         this.$refs['draghandle-' + (index - 1)][0].focus();
-                        this.assistiveLive = this.$gettextInterpolate(
-                            this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
+                        this.assistiveLive = this.$gettext(
+                            'Aktuelle Position in der Liste: %{pos} von %{listLength}.',
                             { pos: index, listLength: this.children.length }
                         );
                     });
@@ -313,8 +318,8 @@ export default {
                     this.increasePosition(index);
                     this.$nextTick(function () {
                         this.$refs['draghandle-' + (index + 1)][0].focus();
-                        this.assistiveLive = this.$gettextInterpolate(
-                            this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
+                        this.assistiveLive = this.$gettext(
+                            'Aktuelle Position in der Liste: %{pos} von %{listLength}.',
                             { pos: index + 2, listLength: this.children.length }
                         );
                     });
@@ -410,7 +415,7 @@ export default {
             semesterForm.appendChild(nodeField);
         });
     },
-    beforeDestroy() {
+    beforeUnmount() {
         STUDIP.eventBus.off('open-tree-node');
         STUDIP.eventBus.off('load-tree-node');
         STUDIP.eventBus.off('sort-tree-children');
diff --git a/resources/vue/components/tree/StudipTreeTableRows.vue b/resources/vue/components/tree/StudipTreeTableRows.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e13b97d82fe4e2c08baf5ae66310f47d2ae6c9e6
--- /dev/null
+++ b/resources/vue/components/tree/StudipTreeTableRows.vue
@@ -0,0 +1,66 @@
+<template>
+    <tr v-bind="$attrs" class="studip-tree-child">
+        <td>
+            <a v-if="editable && children.length > 1" class="drag-link" role="option"
+               tabindex="0"
+               :title="$gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {node: element.attributes.name}, true)"
+               @keydown="keyHandler($event, index)"
+               :ref="'draghandle-' + index">
+                <span class="drag-handle"></span>
+            </a>
+        </td>
+        <td>
+            <a :href="nodeUrl(element.id, semester !== 'all' ? semester : null)" tabindex="0"
+               @click.prevent="$emit('open:node', element)"
+               :title="$gettext(
+                   'Unterebene %{ node } öffnen',
+                   { node: node.attributes.name },
+                   true
+               )"
+            >
+                <studip-icon :shape="element.attributes['has-children'] ? 'folder-full' : 'folder-empty'"
+                             :size="26"></studip-icon>
+            </a>
+        </td>
+        <td>
+            <a :href="nodeUrl(element.id, semester !== 'all' ? semester : null)" tabindex="0"
+               @click.prevent="$emit('open:node', element)"
+               :title="$gettext(
+                   'Unterebene %{ node } öffnen',
+                   { node: node.attributes.name },
+                   true
+               )"
+            >
+                {{ element.attributes.name }}
+            </a>
+        </td>
+        <td>
+            <tree-node-course-info v-if="node.attributes.ancestors.length > 2"
+                                   :node="element"
+                                   :semester="semester"
+                                   :sem-class="semClass"
+            ></tree-node-course-info>
+        </td>
+    </tr>
+</template>
+<script>
+import StudipIcon from "../StudipIcon.vue";
+import TreeNodeCourseInfo from "./TreeNodeCourseInfo.vue";
+import {TreeMixin} from "../../mixins/TreeMixin";
+
+export default {
+    name: 'studip-tree-table-rows',
+    components: { StudipIcon, TreeNodeCourseInfo},
+    emits: ['open:node'],
+    mixins: [ TreeMixin ],
+    props: {
+        children: Array,
+        editable: Boolean,
+        element: Object,
+        index: Number,
+        semester: String,
+        semClass: Number,
+        node: Object,
+    }
+};
+</script>
diff --git a/resources/vue/components/tree/TreeBreadcrumb.vue b/resources/vue/components/tree/TreeBreadcrumb.vue
index 4ba4664d07eb8a13998d78fe59cfd8d27981e66c..00486dfd9bc9c4b4861ffdbf457e6f1b5e30f1d2 100644
--- a/resources/vue/components/tree/TreeBreadcrumb.vue
+++ b/resources/vue/components/tree/TreeBreadcrumb.vue
@@ -10,7 +10,7 @@
                     <a :href="nodeUrl(ancestor.classname + '_' + ancestor.id)" :ref="ancestor.id"
                        @click.prevent="openNode(ancestor.id, ancestor.classname)" tabindex="0"
                        :id="'tree-breadcrumb-' + ancestor.id"
-                       :title="$gettextInterpolate($gettext('%{ node } öffnen'), { node: ancestor.name}, true)">
+                       :title="$gettext('%{ node } öffnen', { node: ancestor.name}, true)">
                         {{ ancestor.name }}
                     </a>
                     <template v-if="index !== node.attributes.ancestors.length - 1">
@@ -157,7 +157,9 @@ export default {
             STUDIP.Dialog.fromURL(this.editUrl + '/' + node.id, { data: { from: this.nodeUrl(node.id) }});
         },
         deleteNode(node) {
-            let text = this.$gettext('Sind sie sicher, dass der Eintrag "%{ node }" gelöscht werden soll?');
+            let text = this.$gettext(
+                'Sind sie sicher, dass der Eintrag "%{ node }" gelöscht werden soll?',
+            );
             let context = {
                 node: node.attributes.name
             };
diff --git a/resources/vue/components/tree/TreeCourseDetails.vue b/resources/vue/components/tree/TreeCourseDetails.vue
index 020cc330c0d9246622e0a991354ad89b1f1c5936..a6e08b42a48ba66684d07d9191eb34d4ecc14136 100644
--- a/resources/vue/components/tree/TreeCourseDetails.vue
+++ b/resources/vue/components/tree/TreeCourseDetails.vue
@@ -11,16 +11,19 @@
         <div class="course-lecturers">
             <span v-for="(lecturer, index) in details.lecturers" :key="index">
                 <a :href="profileUrl(lecturer.username)"
-                   :title="$gettextInterpolate($gettext('Zum Profil von %{ user }'),
-                        { user: lecturer.name }, true)"
+                   :title="$gettext(
+                       'Zum Profil von %{ user }',
+                       { user: lecturer.name },
+                       true
+                   )"
                    tabindex="0">
                     {{ lecturer.name }}
                 </a><template v-if="details.lecturers.length > 1 && index < details.lecturers.length - 1">, </template>
             </span>
         </div>
-        <MountingPortal :mountTo="'#course-dates-' + course" :append="true">
+        <Teleport :to="'#course-dates-' + course" :append="true">
             <span v-html="details.dates"></span>
-        </MountingPortal>
+        </Teleport>
     </div>
 </template>
 
diff --git a/resources/vue/components/tree/TreeNodeCourseInfo.vue b/resources/vue/components/tree/TreeNodeCourseInfo.vue
index 859429e76529fb33362866a7010b4ae6b2fc78db..b3a58dad1e8c73f76ef29ddb14decd074638ed6b 100644
--- a/resources/vue/components/tree/TreeNodeCourseInfo.vue
+++ b/resources/vue/components/tree/TreeNodeCourseInfo.vue
@@ -1,21 +1,27 @@
 <template>
     <div class="studip-tree-child-description">
         <studip-loading-skeleton v-if="isLoading" />
-        <div v-else v-translate="{ count: courseCount }" :translate-n="courseCount"
-             translate-plural="<strong>%{count}</strong> Veranstaltungen">
-            <strong>Eine</strong> Veranstaltung
-        </div>
+        <div v-else
+             v-html="$ngettext(
+                 '<strong>Eine</strong> Veranstaltung',
+                 '<strong>%{count}</strong> Veranstaltungen',
+                 courseCount,
+                 { count: courseCount }
+             )"
+        ></div>
     </div>
 </template>
 
 <script>
 import { TreeMixin } from '../../mixins/TreeMixin';
 import StudipLoadingSkeleton from '../StudipLoadingSkeleton.vue';
+import {$ngettext} from "../../../assets/javascripts/lib/gettext";
 
 export default {
     name: 'TreeNodeCourseInfo',
     components: { StudipLoadingSkeleton },
     mixins: [ TreeMixin ],
+    emits: ['showAllCourses'],
     props: {
         node: {
             type: Object,
@@ -42,6 +48,7 @@ export default {
         }
     },
     methods: {
+        $ngettext,
         showAllCourses(state) {
             this.showingAllCourses = state;
             this.$emit('showAllCourses', state);
diff --git a/resources/vue/components/tree/TreeNodeTile.vue b/resources/vue/components/tree/TreeNodeTile.vue
index 698cc25c024845440f6d9d0d12a731ad34192929..fb1edd354df1cc3e12a6feda9b1ed4e69e4fada9 100644
--- a/resources/vue/components/tree/TreeNodeTile.vue
+++ b/resources/vue/components/tree/TreeNodeTile.vue
@@ -1,11 +1,17 @@
 <template>
-    <a :href="url" @click.prevent="openNode" :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'),
-                                { node: node.attributes.name }, true)">
+    <a :href="url"
+       @click.prevent="openNode"
+       :title="$gettext(
+           'Unterebene %{ node } öffnen',
+           { node: node.attributes.name },
+           true
+       )"
+    >
         <p class="studip-tree-child-title">
             {{ node.attributes.name }}
         </p>
 
-        <tree-node-course-info v-if="node.attributes.ancestors.length > 2" 
+        <tree-node-course-info v-if="node.attributes.ancestors.length > 2"
                                :node="node"
                                :semester="semester"
                                :sem-class="semClass"
diff --git a/resources/vue/components/tree/TreeSearchResult.vue b/resources/vue/components/tree/TreeSearchResult.vue
index 20a73ca392e5ec52f9ad3941f08f26e0bd44aa99..0f615b41c6ed104fd9bf70f9ad9f8144b432c4b5 100644
--- a/resources/vue/components/tree/TreeSearchResult.vue
+++ b/resources/vue/components/tree/TreeSearchResult.vue
@@ -8,9 +8,15 @@
         <table v-if="courses.length > 0" class="default studip-tree-table">
             <caption>
                 <studip-icon shape="search"></studip-icon>
-                {{ $gettextInterpolate($ngettext('Ein Eintrag für den Begriff "%{searchterm}" gefunden',
-                    '%{count} Einträge für den Begriff "%{searchterm}" gefunden', courses.length),
-                    { count: courses.length, searchterm: searchConfig.searchterm}) }}
+                {{ $ngettext(
+                    'Ein Eintrag für den Begriff "%{searchterm}" gefunden',
+                    '%{count} Einträge für den Begriff "%{searchterm}" gefunden',
+                    courses.length,
+                    {
+                        count: courses.length,
+                        searchterm: searchConfig.searchterm
+                    }
+                ) }}
             </caption>
             <colgroup>
                 <col style="width: 30px">
@@ -31,7 +37,7 @@
                     </td>
                     <td>
                         <a :href="courseUrl(course.id)"
-                           :title="$gettextInterpolate($gettext('Zur Veranstaltung %{title}'), {title: course.attributes.title}, true)"
+                           :title="$gettext('Zur Veranstaltung %{title}', {title: course.attributes.title}, true)"
                            tabindex="0">
                             <template v-if="course.attributes['course-number']">
                                 {{ course.attributes['course-number'] }}
@@ -49,8 +55,10 @@
             </tbody>
         </table>
         <studip-message-box v-else type="info">
-            {{ $gettextInterpolate($gettext('Es wurden keine Ergebnisse zu Ihrem Suchbegriff "%{term}" gefunden.'),
-                { term: searchConfig.searchterm }) }}
+            {{ $gettext(
+                'Es wurden keine Ergebnisse zu Ihrem Suchbegriff "%{term}" gefunden.',
+                { term: searchConfig.searchterm }
+            ) }}
         </studip-message-box>
     </article>
 </template>
@@ -84,9 +92,9 @@ export default {
     },
     computed: {
         searchDescription() {
-            return this.$gettextInterpolate(
-                this.$gettext('Suche nach dem Begriff "%{ term }"'),
-                {term: this.searchConfig.searchterm}
+            return this.$gettext(
+                'Suche nach dem Begriff "%{ term }"',
+                { term: this.searchConfig.searchterm }
             );
         }
     },
diff --git a/resources/vue/courseware-activities-app.js b/resources/vue/courseware-activities-app.js
index 3122238c5a47c70bd86d26257cf5c97e41733196..858082faf771c3c0348fed924dea2f179c9b161a 100644
--- a/resources/vue/courseware-activities-app.js
+++ b/resources/vue/courseware-activities-app.js
@@ -1,12 +1,11 @@
 import ActivitiesApp from './components/courseware/ActivitiesApp.vue';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
-import Vuex from 'vuex';
 import CoursewareModule from './store/courseware/courseware.module';
 import CoursewareActivitiesModule from './store/courseware/courseware-activities.module';
 import CoursewareStructureModule from './store/courseware/structure.module';
 import axios from 'axios';
+import { h } from "vue";
 
-const mountApp = async (STUDIP, createApp, element) => {
+const mountApp = async (STUDIP, createApp, store, element) => {
     const getHttpClient = () =>
         axios.create({
             baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
@@ -17,43 +16,10 @@ const mountApp = async (STUDIP, createApp, element) => {
 
     const httpClient = getHttpClient();
 
-    const store = new Vuex.Store({
-        modules: {
-            courseware: CoursewareModule,
-            'courseware-structure': CoursewareStructureModule,
-            'courseware-activities': CoursewareActivitiesModule,
-            ...mapResourceModules({
-                names: [
-                    'activities',
-                    'users',
-                    'courses',
-                    'course-memberships',
-                    'courseware-blocks',
-                    'courseware-block-comments',
-                    'courseware-block-feedback',
-                    'courseware-containers',
-                    'courseware-instances',
-                    'courseware-structural-elements',
-                    'courseware-task-feedback',
-                    'courseware-task-groups',
-                    'courseware-tasks',
-                    'courseware-units',
-                    'courseware-user-data-fields',
-                    'courseware-user-progresses',
-                    'files',
-                    'file-refs',
-                    'folders',
-                    'users',
-                    'institutes',
-                    'semesters',
-                    'sem-classes',
-                    'sem-types',
-                    'status-groups',
-                ],
-                httpClient,
-            }),
-        },
-    });
+    store.registerModule('courseware', CoursewareModule);
+    store.registerModule('courseware-structure', CoursewareStructureModule);
+    store.registerModule('courseware-activities', CoursewareActivitiesModule);
+
     let entry_id = null;
     let entry_type = null;
     let elem;
@@ -80,11 +46,9 @@ const mountApp = async (STUDIP, createApp, element) => {
     await store.dispatch('loadCourseUnits', entry_id);
 
     const app = createApp({
-        render: (h) => h(ActivitiesApp),
-        store,
+        render: () => h(ActivitiesApp),
     });
-
-    app.$mount(element);
+    app.mount(element);
 
     return app;
 };
diff --git a/resources/vue/courseware-admin-app.js b/resources/vue/courseware-admin-app.js
index 2ca74549a14517467f13584954beaa54370ed5c2..57164a34e8343b786dde5a54b13befcebdfb6ad4 100644
--- a/resources/vue/courseware-admin-app.js
+++ b/resources/vue/courseware-admin-app.js
@@ -1,42 +1,18 @@
 import AdminApp from './components/courseware/AdminApp.vue';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
-import Vuex from 'vuex';
 import CoursewareAdminModule from './store/courseware/courseware-admin.module';
-import axios from 'axios';
+import { h } from "vue";
 
-const mountApp = (STUDIP, createApp, element) => {
-    const getHttpClient = () =>
-    axios.create({
-        baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
-        headers: {
-            'Content-Type': 'application/vnd.api+json',
-        },
-    });
-
-    const httpClient = getHttpClient();
-
-    const store = new Vuex.Store({
-        modules: {
-            courseware: CoursewareAdminModule,
-            ...mapResourceModules({
-                names: [
-                    'courseware-templates',
-                ],
-                httpClient,
-            }),
-        },
-    });
+const mountApp = (STUDIP, createApp, store, element) => {
+    store.registerModule('courseware', CoursewareAdminModule);
 
     store.dispatch('courseware-templates/loadAll');
 
     const app = createApp({
-        render: (h) => h(AdminApp),
-        store
+        render: () => h(AdminApp),
     });
-
-    app.$mount(element);
+    app.mount(element);
 
     return app;
 }
 
-export default mountApp;
\ No newline at end of file
+export default mountApp;
diff --git a/resources/vue/courseware-comments-app.js b/resources/vue/courseware-comments-app.js
index 0e9a4ba430c0403a76b8e6831bf29677848fbf3a..370cc28fb8ecef35349ec8794eb5c69ef4254a0a 100644
--- a/resources/vue/courseware-comments-app.js
+++ b/resources/vue/courseware-comments-app.js
@@ -1,10 +1,9 @@
 import CoursewareCommentsModule from './store/courseware/courseware-comments.module';
 import CommentsApp from './components/courseware/CommentsApp.vue';
-import Vuex from 'vuex';
 import axios from 'axios';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
+import { h } from "vue";
 
-const mountApp = async (STUDIP, createApp, element) => {
+const mountApp = async (STUDIP, createApp, store, element) => {
     const getHttpClient = () =>
         axios.create({
             baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
@@ -30,28 +29,8 @@ const mountApp = async (STUDIP, createApp, element) => {
 
     const httpClient = getHttpClient();
 
-    const store = new Vuex.Store({
-        modules: {
-            'courseware-comments': CoursewareCommentsModule,
-            ...mapResourceModules({
-                names: [
-                    'courseware-blocks',
-                    'courseware-block-comments',
-                    'courseware-block-feedback',
-                    'courseware-containers',
-                    'courseware-units',
-                    'courseware-structural-elements',
-                    'courseware-structural-element-comments',
-                    'courseware-structural-element-feedback',
-                    'users',
-                    'course-memberships',
-                    'institutes',
-                    'institute-memberships',
-                ],
-                httpClient,
-            }),
-        },
-    });
+    store.registerModule('courseware-comments', CoursewareCommentsModule);
+
     store.dispatch('setHttpClient', httpClient);
     store.dispatch('setContext', {
         id: entry_id,
@@ -104,11 +83,9 @@ const mountApp = async (STUDIP, createApp, element) => {
     );
 
     const app = createApp({
-        render: (h) => h(CommentsApp),
-        store,
+        render: () => h(CommentsApp),
     });
-
-    app.$mount(element);
+    app.mount(element);
 };
 
 export default mountApp;
diff --git a/resources/vue/courseware-content-bookmark-app.js b/resources/vue/courseware-content-bookmark-app.js
index 6d7eed95e32170664abc8107a2948fdaae9259c8..7a05033af69466409a9eb97165fefc7a623912b3 100644
--- a/resources/vue/courseware-content-bookmark-app.js
+++ b/resources/vue/courseware-content-bookmark-app.js
@@ -1,10 +1,9 @@
 import ContentBookmarkApp from './components/courseware/ContentBookmarkApp.vue';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
-import Vuex from 'vuex';
 import CoursewareModule from './store/courseware/courseware.module';
 import axios from 'axios';
+import { h } from "vue";
 
-const mountApp = (STUDIP, createApp, element) => {
+const mountApp = (STUDIP, createApp, store, element) => {
     const getHttpClient = () =>
     axios.create({
         baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
@@ -15,35 +14,8 @@ const mountApp = (STUDIP, createApp, element) => {
 
     const httpClient = getHttpClient();
 
-    const store = new Vuex.Store({
-        modules: {
-            courseware: CoursewareModule,
-            ...mapResourceModules({
-                names: [
-                    'activities',
-                    'file-refs',
-                    'courses',
-                    'course-memberships',
-                    'courseware-blocks',
-                    'courseware-block-comments',
-                    'courseware-block-feedback',
-                    'courseware-containers',
-                    'courseware-instances',
-                    'courseware-structural-elements',
-                    'courseware-units',
-                    'courseware-user-data-fields',
-                    'courseware-user-progresses',
-                    'institutes',
-                    'semesters',
-                    'sem-classes',
-                    'sem-types',
-                    'status-groups',
-                    'users',
-                ],
-                httpClient,
-            }),
-        },
-    });
+    store.registerModule('courseware', CoursewareModule);
+
     let entry_id = null;
     let entry_type = null;
     let elem;
@@ -70,11 +42,9 @@ const mountApp = (STUDIP, createApp, element) => {
     });
 
     const app = createApp({
-        render: (h) => h(ContentBookmarkApp),
-        store
+        render: () => h(ContentBookmarkApp),
     });
-
-    app.$mount(element);
+    app.mount(element);
 
     return app;
 }
diff --git a/resources/vue/courseware-content-releases-app.js b/resources/vue/courseware-content-releases-app.js
index 79d56cced2aceb6f4177046ce50912c9b0581471..27784c30e806869b42d8f02d489cd4ed5814b6cb 100644
--- a/resources/vue/courseware-content-releases-app.js
+++ b/resources/vue/courseware-content-releases-app.js
@@ -1,36 +1,24 @@
 import ContentReleasesApp from './components/courseware/ContentReleasesApp.vue';
 import CoursewareModule from './store/courseware/courseware.module';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
-import Vuex from 'vuex';
-import axios from 'axios';
+import { h } from "vue";
+import {mapResourceModules} from "../assets/javascripts/lib/reststate-vuex";
+import axios from "axios";
 
-const mountApp = (STUDIP, createApp, element) => {
+const mountApp = (STUDIP, createApp, store, element) => {
     const getHttpClient = () =>
-    axios.create({
-        baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
-        headers: {
-            'Content-Type': 'application/vnd.api+json',
-        },
-    });
-
-    const httpClient = getHttpClient();
+        axios.create({
+            baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
+            headers: {
+                'Content-Type': 'application/vnd.api+json',
+            },
+        });
+
+    store.registerModule('courseware', CoursewareModule);
+    Object.entries(mapResourceModules({
+        names: ['courseware-structural-elements-released'],
+        httpClient: getHttpClient()
+    })).forEach(([name, module]) => store.registerModule(name, module));
 
-    const store = new Vuex.Store({
-        modules: {
-            courseware: CoursewareModule,
-            ...mapResourceModules({
-                names: [
-                    'courseware-containers',
-                    'courseware-public-links',
-                    'courseware-structural-elements',
-                    'courseware-structural-elements-released',
-                    'file-refs',
-                    'users',
-                ],
-                httpClient,
-            }),
-        },
-    });
     let entry_id = null;
     let entry_type = null;
     let elem = document.getElementById(element.substring(1));
@@ -61,11 +49,9 @@ const mountApp = (STUDIP, createApp, element) => {
     store.dispatch('courseware-structural-elements-released/loadAll', {});
 
     const app = createApp({
-        render: (h) => h(ContentReleasesApp),
-        store
+        render: () => h(ContentReleasesApp),
     });
-
-    app.$mount(element);
+    app.mount(element);
 
     return app;
 }
diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js
index a82dc9888a210b2cd29c9a837377253cebf203ff..79679fb5d977bf44677bd6196653c821a3ef64e7 100644
--- a/resources/vue/courseware-index-app.js
+++ b/resources/vue/courseware-index-app.js
@@ -5,22 +5,11 @@ import CoursewareStructuralElement from './components/courseware/structural-elem
 import CoursewareTasksModule from './store/courseware/courseware-tasks.module';
 import IndexApp from './components/courseware/IndexApp.vue';
 import PluginManager from './components/courseware/plugin-manager.js';
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-import Vuex from 'vuex';
-import axios from 'axios';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
 import { StockImagesPlugin } from './plugins/stock-images.js';
+import { createRouter, createWebHashHistory } from 'vue-router';
+import { h } from "vue";
 
-const mountApp = async (STUDIP, createApp, element) => {
-    const getHttpClient = () =>
-        axios.create({
-            baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
-            headers: {
-                'Content-Type': 'application/vnd.api+json',
-            },
-        });
-
+const mountApp = async (STUDIP, c, store, element) => {
     // get id of parent structural element
     let elem_id = null;
     let entry_id = null;
@@ -58,6 +47,18 @@ const mountApp = async (STUDIP, createApp, element) => {
             }
         }
     }
+
+    const { createApp, httpClient } = await STUDIP.Vue.load();
+
+    let base = new URL(
+        STUDIP.URLHelper.parameters.cid
+            ? STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + unit_id, { cid: STUDIP.URLHelper.parameters.cid }, true)
+            : STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/courseware/' + unit_id)
+    );
+    if (entry_type === 'courses') {
+        base.search += '&';
+    }
+
     const routes = [
         {
             path: '/',
@@ -70,77 +71,23 @@ const mountApp = async (STUDIP, createApp, element) => {
         },
     ];
 
-    let base = new URL(
-        STUDIP.URLHelper.parameters.cid
-            ? STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + unit_id, { cid: STUDIP.URLHelper.parameters.cid }, true)
-            : STUDIP.URLHelper.getURL('dispatch.php/contents/courseware/courseware/' + unit_id)
-    );
-    if (entry_type === 'courses') {
-        base.search += '&';
-    }
-    const router = new VueRouter({
-        base: `${base.pathname}${base.search}`,
-        routes,
-    });
 
-    const httpClient = getHttpClient();
-
-    const store = new Vuex.Store({
-        modules: {
-            courseware: CoursewareModule,
-            'courseware-structure': CoursewareStructureModule,
-            'file-chooser': FileChooserStore,
-            'tasks': CoursewareTasksModule,
-            ...mapResourceModules({
-                names: [
-                    'courses',
-                    'course-memberships',
-                    'courseware-blocks',
-                    'courseware-block-comments',
-                    'courseware-block-feedback',
-                    'courseware-clipboards',
-                    'courseware-containers',
-                    'courseware-instances',
-                    'courseware-public-links',
-                    'courseware-structural-elements',
-                    'courseware-structural-element-comments',
-                    'courseware-structural-element-feedback',
-                    'courseware-task-feedback',
-                    'courseware-task-groups',
-                    'courseware-tasks',
-                    'courseware-templates',
-                    'courseware-user-data-fields',
-                    'courseware-user-progresses',
-                    'courseware-units',
-                    'feedback-elements',
-                    'feedback-entries',
-                    'files',
-                    'file-refs',
-                    'folders',
-                    'lti-tools',
-                    'status-groups',
-                    'users',
-                    'institutes',
-                    'institute-memberships',
-                    'semesters',
-                    'sem-classes',
-                    'sem-types',
-                    'terms-of-use',
-                    'user-data-field',
-                    'studip-properties'
-                ],
-                httpClient,
-            }),
-        },
+    const router = createRouter({
+        history: createWebHashHistory(base.toString()),
+        routes
     });
 
-    axios.get(
-        STUDIP.URLHelper.getURL('jsonapi.php/v1/studip/properties', {}, true)
-    ).then(response => {
-        response.data.data.forEach(prop => {
-            store.dispatch('studip-properties/storeRecord', prop);
+    store.registerModule('courseware', CoursewareModule);
+    store.registerModule('courseware-structure', CoursewareStructureModule);
+    store.registerModule('file-chooser', FileChooserStore);
+    store.registerModule('tasks', CoursewareTasksModule);
+
+    httpClient.get('studip/properties')
+        .then(response => {
+            response.data.data.forEach(prop => {
+                store.dispatch('studip-properties/storeRecord', prop);
+            });
         });
-    });
 
     store.dispatch('setUrlHelper', STUDIP.URLHelper);
     store.dispatch('setUserId', STUDIP.USER_ID);
@@ -177,14 +124,12 @@ const mountApp = async (STUDIP, createApp, element) => {
     );
 
     const app = createApp({
-        render: (h) => h(IndexApp),
-        router,
-        store,
+        render: () => h(IndexApp),
     });
 
-    Vue.use(StockImagesPlugin, { store });
-
-    app.$mount(element);
+    app.use(router);
+    app.use(StockImagesPlugin, { store });
+    app.mount(element);
 
     return app;
 };
diff --git a/resources/vue/courseware-public-app.js b/resources/vue/courseware-public-app.js
index 1d50b8ace89ccb393f50845904b645ff71b4c592..c4585224cc02a4a703bd018c5a6cda9d1b507725 100644
--- a/resources/vue/courseware-public-app.js
+++ b/resources/vue/courseware-public-app.js
@@ -3,13 +3,10 @@ import CoursewarePublicModule from './store/courseware/courseware-public.module'
 import PublicCoursewareStructuralElement from './components/courseware/structural-element/PublicCoursewareStructuralElement.vue';
 import CoursewarePublicStructureModule from './store/courseware/public-structure.module';
 import PluginManager from './components/courseware/plugin-manager.js';
-import VueRouter from 'vue-router';
-import Vuex from 'vuex';
-import axios from 'axios';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
-import _ from 'lodash';
+import { createRouter } from 'vue-router';
+import { h } from "vue";
 
-const mountApp = (STUDIP, createApp, element) => {
+const mountApp = (STUDIP, createApp, store, element) => {
 
     let elem_id = null;
     let link_id = null;
@@ -37,43 +34,12 @@ const mountApp = (STUDIP, createApp, element) => {
         }
     }
 
-    const getHttpClient = () =>
-    axios.create({
-        baseURL: STUDIP.URLHelper.getURL('jsonapi.php/v1/public/courseware/'  + link_id, {}, true),
-        headers: {
-            'Content-Type': 'application/vnd.api+json',
-        },
-    });
-
     let base = new URL(
         STUDIP.URLHelper.getURL('dispatch.php/courseware/public', { link: link_id }, true)
     );
 
-    const httpClient = getHttpClient();
-
-    const store = new Vuex.Store({
-        modules: {
-            // courseware: CoursewareModule,
-            'courseware-public': CoursewarePublicModule,
-            'courseware-structure': CoursewarePublicStructureModule,
-            ...mapResourceModules({
-                names: [
-                    'courseware-blocks',
-                    'courseware-containers',
-                    'courseware-instances',
-                    'courseware-structural-elements',
-                    'courseware-user-data-fields',
-                    'courseware-user-progresses',
-                    'files',
-                    'file-refs',
-                    'folders',
-                    'users',
-                ],
-                httpClient,
-            }),
-        },
-    });
-
+    store.registerModule('courseware-public', CoursewarePublicModule);
+    store.registerModule('courseware-structure', CoursewarePublicStructureModule);
     store.dispatch('setContext', {
         id: link_id,
         type: entry_type,
@@ -108,18 +74,16 @@ const mountApp = (STUDIP, createApp, element) => {
         },
     ];
 
-    const router = new VueRouter({
+    const router = createRouter({
         base: `${base.pathname}${base.search}`,
         routes,
     });
 
     const app = createApp({
-        render: (h) => h(PublicApp),
+        render: () => h(PublicApp),
         router,
-        store
     });
-
-    app.$mount(element);
+    app.mount(element);
 
     return app;
 }
diff --git a/resources/vue/courseware-shelf-app.js b/resources/vue/courseware-shelf-app.js
index 35369c1c6d3c5b8ecc8b5f0cfc6791c33caedc71..efbf0ca9cef9743c02c822c91143c2e91fc20c42 100644
--- a/resources/vue/courseware-shelf-app.js
+++ b/resources/vue/courseware-shelf-app.js
@@ -1,12 +1,10 @@
 import CoursewareShelfModule from './store/courseware/courseware-shelf.module';
 import ShelfApp from './components/courseware/ShelfApp.vue';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import axios from 'axios';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
+import { resourceModule } from '@/assets/javascripts/lib/reststate-vuex.js';
 import { StockImagesPlugin } from './plugins/stock-images.js';
+import { h } from 'vue';
 
-const mountApp = async (STUDIP, createApp, element) => {
+const mountApp = async (STUDIP, c, store, element) => {
     // handle studip 5.0 to 5.2 urls
     const elemId = window.location.hash.match(/structural_element\/(\d+)/);
 
@@ -18,14 +16,6 @@ const mountApp = async (STUDIP, createApp, element) => {
         return false;
     }
 
-    const getHttpClient = () =>
-        axios.create({
-            baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
-            headers: {
-                'Content-Type': 'application/vnd.api+json',
-            },
-        });
-
     let elem;
     let entry_id = null;
     let entry_type = null;
@@ -55,42 +45,17 @@ const mountApp = async (STUDIP, createApp, element) => {
         }
     }
 
-    const httpClient = getHttpClient();
+    const { createApp, httpClient } = await STUDIP.Vue.load();
+    store.registerModule('courseware-shelf', CoursewareShelfModule);
+
+    store.registerModule(
+        'courseware-structural-elements-shared',
+        resourceModule({
+            name: 'courseware-structural-elements-shared',
+            httpClient
+        })
+    );
 
-    const store = new Vuex.Store({
-        modules: {
-            'courseware-shelf': CoursewareShelfModule,
-            ...mapResourceModules({
-                names: [
-                    'courses',
-                    'course-memberships',
-                    'courseware-blocks',
-                    'courseware-containers',
-                    'courseware-instances',
-                    'courseware-units',
-                    'courseware-user-data-fields',
-                    'courseware-user-progresses',
-                    'courseware-structural-elements',
-                    'courseware-structural-elements-shared',
-                    'feedback-elements',
-                    'feedback-entries',
-                    'files',
-                    'file-refs',
-                    'folders',
-                    'users',
-                    'institutes',
-                    'institute-memberships',
-                    'semesters',
-                    'sem-classes',
-                    'sem-types',
-                    'stock-images',
-                    'status-groups',
-                    'terms-of-use'
-                ],
-                httpClient,
-            }),
-        },
-    });
     store.dispatch('setUrlHelper', STUDIP.URLHelper);
     store.dispatch('setHttpClient', httpClient);
     store.dispatch('setLicenses', licenses);
@@ -109,14 +74,11 @@ const mountApp = async (STUDIP, createApp, element) => {
         await store.dispatch('courseware-structural-elements-shared/loadAll', { options: { include: 'owner' } });
     }
 
-    Vue.use(StockImagesPlugin, { store });
-
     const app = createApp({
-        render: (h) => h(ShelfApp),
-        store,
+        render: () => h(ShelfApp),
     });
-
-    app.$mount(element);
+    app.use(StockImagesPlugin, { store });
+    app.mount(element);
 
 };
 
diff --git a/resources/vue/courseware-tasks-app.js b/resources/vue/courseware-tasks-app.js
index fc7bf655f3bc5a71807e31ccd5bab3168ca4f26c..df610fa972d745ab6fe4471362382a45edb30445 100644
--- a/resources/vue/courseware-tasks-app.js
+++ b/resources/vue/courseware-tasks-app.js
@@ -1,14 +1,13 @@
 import TaskGroupsIndex from './components/courseware/tasks/PagesTaskGroupsIndex.vue';
 import TaskGroupsShow from './components/courseware/tasks/PagesTaskGroupsShow.vue';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
-import VueRouter, { RouterView } from 'vue-router';
-import Vuex from 'vuex';
+import { createRouter, RouterView, createWebHashHistory } from 'vue-router';
 import CoursewareModule from './store/courseware/courseware.module';
 import CoursewareTasksModule from './store/courseware/courseware-tasks.module';
 import CoursewareStructureModule from './store/courseware/structure.module';
 import axios from 'axios';
+import {h} from "vue";
 
-const mountApp = async (STUDIP, createApp, element) => {
+const mountApp = async (STUDIP, createApp, store, element) => {
     const getHttpClient = () =>
         axios.create({
             baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
@@ -40,56 +39,22 @@ const mountApp = async (STUDIP, createApp, element) => {
             true
         )
     );
-    const router = new VueRouter({
-        base: base.pathname,
-        mode: 'history',
+    const router = createRouter({
+        history: createWebHashHistory(),
         routes,
     });
     router.beforeEach((to, from, next) => {
-        if ('cid' in to?.query) {
+        if (to?.query?.cid !== undefined) {
             next();
         } else {
             next({ ...to, query: { ...to.query, cid: window.STUDIP.URLHelper.parameters.cid } });
         }
     });
 
-    const store = new Vuex.Store({
-        modules: {
-            courseware: CoursewareModule,
-            tasks: CoursewareTasksModule,
-            'courseware-structure': CoursewareStructureModule,
-            ...mapResourceModules({
-                names: [
-                    'activities',
-                    'users',
-                    'courses',
-                    'course-memberships',
-                    'courseware-blocks',
-                    'courseware-block-comments',
-                    'courseware-block-feedback',
-                    'courseware-containers',
-                    'courseware-instances',
-                    'courseware-structural-elements',
-                    'courseware-task-feedback',
-                    'courseware-task-groups',
-                    'courseware-tasks',
-                    'courseware-units',
-                    'courseware-user-data-fields',
-                    'courseware-user-progresses',
-                    'files',
-                    'file-refs',
-                    'folders',
-                    'users',
-                    'institutes',
-                    'semesters',
-                    'sem-classes',
-                    'sem-types',
-                    'status-groups',
-                ],
-                httpClient,
-            }),
-        },
-    });
+    store.registerModule('courseware', CoursewareModule);
+    store.registerModule('tasks', CoursewareTasksModule);
+    store.registerModule('courseware-structure', CoursewareStructureModule);
+
     let entry_id = null;
     let entry_type = null;
     let isTeacher = false;
@@ -122,12 +87,10 @@ const mountApp = async (STUDIP, createApp, element) => {
     await store.dispatch('tasks/loadTasksOfCourse', { cid: entry_id });
 
     const app = createApp({
-        render: (h) => h(RouterView),
-        router,
-        store,
+        render: () => h(RouterView),
     });
-
-    app.$mount(element);
+    app.use(router);
+    app.mount(element);
 
     return app;
 };
diff --git a/resources/vue/mixins/ContentModulesMixin.js b/resources/vue/mixins/ContentModulesMixin.js
index b02561d9ee1060e19208c910f19ee247d7629add..0650342388284fec6c02d5c884c258d30036e4b4 100644
--- a/resources/vue/mixins/ContentModulesMixin.js
+++ b/resources/vue/mixins/ContentModulesMixin.js
@@ -19,8 +19,13 @@ export default {
             'modules',
             'view',
         ]),
-        activeModules() {
-            return this.sortedModules.filter(module => module.active);
+        activeModules: {
+            get() {
+                return this.sortedModules.filter(module => module.active);
+            },
+            set(modules) {
+                this.sortedModules = modules;
+            }
         },
         inactiveModules() {
             return this.sortedModules.filter(module => !module.active);
diff --git a/resources/vue/mixins/QuestionnaireComponent.js b/resources/vue/mixins/QuestionnaireComponent.js
index 277f21c87dde745db62eee0063a2896be1d5ce27..62f8e2d2dd949a2827c84b5e8a942cad614fc160 100644
--- a/resources/vue/mixins/QuestionnaireComponent.js
+++ b/resources/vue/mixins/QuestionnaireComponent.js
@@ -1,23 +1,26 @@
 export const QuestionnaireComponent = {
+    emits: ['update:modelValue'],
     props: {
-        value: Object
+        modelValue: Object
     },
     data () {
-        return {val_clone: this.value};
+        return {
+            val_clone: {...this.modelValue}
+        };
     },
     methods: {
         setDefaultValues(value) {
-            this.val_clone = Object.assign(value, this.value);
+            this.val_clone = Object.assign(value, this.modelValue);
         }
     },
     watch: {
         val_clone: {
             handler(current) {
-                this.$emit('input', current);
+                this.$emit('update:modelValue', current);
             },
             deep: true
         },
-        value (new_val) {
+        modelValue(new_val) {
             this.val_clone = new_val;
         }
     }
diff --git a/resources/vue/mixins/courseware/container.js b/resources/vue/mixins/courseware/container.js
index eb5fcaa106dc9ef140bce737bb4d6ca8ed24f0c7..df3ab09b86e44d7c7e473ce783708751febfa9f8 100644
--- a/resources/vue/mixins/courseware/container.js
+++ b/resources/vue/mixins/courseware/container.js
@@ -16,6 +16,7 @@ const containerMixin = {
     created: function () {
         this.pluginManager.registerComponentsLocally(this);
     },
+    emits: ['select'],
     methods: {
         ...mapActions({
             updateBlock: 'updateBlock',
@@ -38,15 +39,15 @@ const containerMixin = {
         dropBlock(e) {
             this.isDragging = false; // implemented by each container type
             let data = {};
-            data.originContainerId = e.from.__vue__.$attrs.containerId;
-            data.targetContainerId = e.to.__vue__.$attrs.containerId;
+            data.originContainerId = e.from.__vnode.ctx.attrs.containerId;
+            data.targetContainerId = e.to.__vnode.ctx.attrs.containerId;
             if (data.originContainerId === data.targetContainerId) {
                 this.storeSort(); // implemented by each container type
             } else {
-                data.originSectionId = e.from.__vue__.$attrs.sectionId;
-                data.originSectionBlockList = e.from.__vue__.$children.map(b => { return b.$attrs.blockId; });
-                data.targetSectionId = e.to.__vue__.$attrs.sectionId;
-                data.targetSectionBlockList = e.to.__vue__.$children.map(b => { return b.$attrs.blockId; });
+                data.originSectionId = e.from.__vnode.ctx.attrs.sectionId;
+                data.originSectionBlockList = e.from.__vnode.children.map(b => { return b.ctx.attrs.blockId; });
+                data.targetSectionId = e.to.__vnode.ctx.attrs.sectionId;
+                data.targetSectionBlockList = e.to.__vnode.children.map(b => { return b.ctx.attrs.blockId; });
                 data.blockId = e.item._underlying_vm_.id;
                 data.newPos = e.newIndex;
                 const indexInBlockList = data.targetSectionBlockList.findIndex(b => b === data.blockId);
@@ -76,7 +77,7 @@ const containerMixin = {
                 targetContainer,
             );
             await this.unlockObject({ id: data.targetContainerId, type: 'courseware-containers' });
-         
+
             // update block container id
             let block = this.blockById({id: data.blockId });
             block.relationships.container.data.id = data.targetContainerId;
@@ -112,7 +113,7 @@ const containerMixin = {
                         console.log(error);
                     }
                 }
-                
+
                 await this.createBlock({
                     container: targetContainer,
                     section: section,
diff --git a/resources/vue/plugins/blubber.js b/resources/vue/plugins/blubber.js
index 5e732aa513069962f79a3f85a9eb0743c6609899..1f41767409fc136693d3e916f57351183512e560 100644
--- a/resources/vue/plugins/blubber.js
+++ b/resources/vue/plugins/blubber.js
@@ -1,5 +1,5 @@
 import axios from 'axios';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
+import { mapResourceModules } from '@/assets/javascripts/lib/reststate-vuex.js';
 import JSUpdater from '@/assets/javascripts/lib/jsupdater.js';
 import blubberModule from '../store/blubber.js';
 import * as components from '../components/blubber/components.js';
@@ -7,24 +7,24 @@ import * as components from '../components/blubber/components.js';
 const JSONAPI_PATH = 'jsonapi.php/v1';
 
 export const BlubberPlugin = {
-    install(Vue, options = {}) {
+    install(app, options = {}) {
         if (!('store' in options)) {
             throw new Error('You must provide the vuex store via the options argument');
         }
 
         this.enhanceStore(options.store);
-        this.registerComponents(Vue);
+        this.registerComponents(app);
         this.registerUpdater(options.store);
     },
     enhanceStore(store) {
         const httpClient = getHttpClient(window.STUDIP.URLHelper.getURL(JSONAPI_PATH, {}, true));
         initializeStore(store, httpClient);
     },
-    registerComponents(Vue) {
+    registerComponents(app) {
         Object.entries(components).forEach(([name, component]) => {
-            const exists = Vue.component(name);
+            const exists = app.component(name);
             if (!exists) {
-                Vue.component(name, component);
+                app.component(name, component);
             }
         });
     },
diff --git a/resources/vue/plugins/stock-images.js b/resources/vue/plugins/stock-images.js
index d99dbb385af16d906dea189099d963c53c01ea0a..63f3c8e1e2ce293daae9a61956473842c51ac938 100644
--- a/resources/vue/plugins/stock-images.js
+++ b/resources/vue/plugins/stock-images.js
@@ -1,5 +1,5 @@
 import axios from 'axios';
-import { mapResourceModules } from '@elan-ev/reststate-vuex';
+import { mapResourceModules } from '@/assets/javascripts/lib/reststate-vuex.js';
 import stockImagesModule from '../store/stock-images.js';
 import * as components from '../components/stock-images/components.js';
 
diff --git a/templates/forms/form.php b/templates/forms/form.php
index 00e4c7df6d42b04c22c266a7aa3a544689de4fad..93dd49154fe3b04b7f3bd26b126005a72ce0b3fc 100644
--- a/templates/forms/form.php
+++ b/templates/forms/form.php
@@ -19,75 +19,80 @@ foreach ($allinputs as $input) {
     }
 }
 $form_id = md5(uniqid());
-?><form v-cloak
-      method="post"
-      <? if (!$form->isAutoStoring()) : ?>
-          action="<?= htmlReady($form->getURL()) ?>"
-      <? else : ?>
-          data-autosave="<?= htmlReady($_SERVER['REQUEST_URI']) ?>"
-          data-url="<?= htmlReady($form->getURL()) ?>"
-      <? endif ?>
-      @submit="submit"
-      @cancel=""
-      novalidate
-      <?= $form->getDataSecure() ? 'data-secure' : '' ?>
-      id="<?= htmlReady($form_id) ?>"
-      data-inputs="<?= htmlReady(json_encode($inputs)) ?>"
-      data-debugmode="<?= htmlReady(json_encode($form->getDebugMode())) ?>"
-      data-required="<?= htmlReady(json_encode($required_inputs)) ?>"
-      data-server_validation="<?= $server_validation ? 1 : 0?>"
-      data-validation_url="<?= htmlReady($_SERVER['REQUEST_URI']) ?>"
-      <?= $form->hasFileInput() ? ' enctype="application/x-www-form-urlencoded"' : '' ?>
-      class="default studipform<?= $form->isCollapsable() ? ' collapsable' : '' ?>">
-
-    <?= CSRFProtection::tokenTag(['ref' => 'securityToken']) ?>
+?>
+<div v-cloak
+     class="studipform"
+     data-inputs="<?= htmlReady(json_encode($inputs)) ?>"
+     data-debugmode="<?= htmlReady(json_encode($form->getDebugMode())) ?>"
+     data-required="<?= htmlReady(json_encode($required_inputs)) ?>"
+     data-server_validation="<?= $server_validation ? 1 : 0 ?>"
+     data-validation_url="<?= htmlReady($_SERVER['REQUEST_URI']) ?>"
+<? if ($form->isAutoStoring()) : ?>
+     data-autosave="<?= htmlReady($_SERVER['REQUEST_URI']) ?>"
+     data-url="<?= htmlReady($form->getURL()) ?>"
+ <? endif; ?>
+>
+    <form method="post"
+    <? if (!$form->isAutoStoring()) : ?>
+        action="<?= htmlReady($form->getURL()) ?>"
+    <? endif ?>
+        @submit="submit"
+        @cancel=""
+        novalidate
+    <? if ($form->getDataSecure()): ?>
+        data-secure
+    <? endif; ?>
+        id="<?= htmlReady($form_id) ?>"
+        class="default <?= $form->isCollapsable() ? ' collapsable' : '' ?>"
+    <? if ($form->hasFileInput()): ?>
+        enctype="multipart/form-data"
+    <? endif; ?>
+    >
+        <?= CSRFProtection::tokenTag(['ref' => 'securityToken']) ?>
 
-    <article aria-live="assertive"
-             class="validation_notes studip"
-             v-if="STUDIPFORM_REQUIRED.length > 0 || STUDIPFORM_VALIDATIONNOTES.length > 0">
-        <header>
-            <h1>
-                <?= Icon::create('info-circle', Icon::ROLE_INFO)->asImg(['class' => 'text-bottom validation_notes_icon']) ?>
-                <?= _('Hinweise zum Ausfüllen des Formulars') ?>
-            </h1>
-        </header>
-        <div class="required_note" v-if="STUDIPFORM_REQUIRED.length > 0">
-            <div aria-hidden="true">
-                <?= _('Pflichtfelder sind mit Sternchen gekennzeichnet.') ?>
+        <article aria-live="assertive"
+                 class="validation_notes studip"
+                 v-if="STUDIPFORM_REQUIRED.length > 0 || STUDIPFORM_VALIDATIONNOTES.length > 0">
+            <header>
+                <h1>
+                    <?= Icon::create('info-circle', Icon::ROLE_INFO)->asImg(['class' => 'text-bottom validation_notes_icon']) ?>
+                    <?= _('Hinweise zum Ausfüllen des Formulars') ?>
+                </h1>
+            </header>
+            <div class="required_note" v-if="STUDIPFORM_REQUIRED.length > 0">
+                <div aria-hidden="true">
+                    <?= _('Pflichtfelder sind mit Sternchen gekennzeichnet.') ?>
+                </div>
+                <div class="sr-only">
+                    <?= _('Dieses Formular enthält Pflichtfelder.') ?>
+                </div>
             </div>
-            <div class="sr-only">
-                <?= _('Dieses Formular enthält Pflichtfelder.') ?>
+            <div v-if="STUDIPFORM_DISPLAYVALIDATION && (STUDIPFORM_VALIDATIONNOTES.length > 0)">
+                <?= _('Folgende Angaben müssen korrigiert werden, um das Formular abschicken zu können:') ?>
+                <ul>
+                    <li v-for="note in ordererValidationNotes" :aria-describedby="note.describedby">
+                        {{ note.label.trim() + ": " + note.description }}
+                    </li>
+                </ul>
             </div>
+        </article>
 
+        <div aria-live="polite">
+            <?
+            foreach ($form->getParts() as $part) : ?>
+                <?= $part->renderWithCondition() ?>
+            <?
+            endforeach ?>
         </div>
-        <div v-if="STUDIPFORM_DISPLAYVALIDATION && (STUDIPFORM_VALIDATIONNOTES.length > 0)">
-            <?= _('Folgende Angaben müssen korrigiert werden, um das Formular abschicken zu können:') ?>
-            <ul>
-                <li v-for="note in ordererValidationNotes" :aria-describedby="note.describedby">{{ note.label.trim() + ": " + note.description }}</li>
-            </ul>
-        </div>
-    </article>
-
-    <div aria-live="polite">
-    <? foreach ($form->getParts() as $part) : ?>
-        <?= $part->renderWithCondition() ?>
-    <? endforeach ?>
-    </div>
-    <? if (!Request::isDialog()) : ?>
-        <footer>
-            <?= \Studip\Button::createAccept($form->getSaveButtonText(), $form->getSaveButtonName(), ['form' => $form_id]) ?>
-            <?= \Studip\LinkButton::createCancel($form->getCancelButtonText(), $form->getCancelButtonName()) ?>
+        <footer data-dialog-button>
+            <?= \Studip\Button::create($form->getSaveButtonText(), $form->getSaveButtonName(), ['form' => $form_id]) ?>
+            <? foreach ($form->getButtons() as $button): ?>
+                <?
+                $button->attributes['form'] = $form_id;
+                echo $button;
+                ?>
+            <? endforeach ?>
+            <?= \Studip\LinkButton::createCancel($form->getCancelButtonText(), Request::url()) ?>
         </footer>
-    <? endif ?>
-</form>
-<? if (Request::isDialog()) : ?>
-    <footer data-dialog-button>
-        <?= \Studip\Button::create($form->getSaveButtonText(), $form->getSaveButtonName(), ['form' => $form_id]) ?>
-        <? foreach ($form->getButtons() as $button) : ?>
-            <?
-            $button->attributes['form'] = $form_id;
-            echo $button;
-            ?>
-        <? endforeach ?>
-    </footer>
-<? endif ?>
+    </form>
+</div>
diff --git a/templates/layouts/base.php b/templates/layouts/base.php
index b2b665094ccb911977e4408595cc60f8cf923a83..19440b6144fbd0db356d6276782528fa7add0ad7 100644
--- a/templates/layouts/base.php
+++ b/templates/layouts/base.php
@@ -12,6 +12,17 @@ $getInstalledLanguages = function () {
     return $languages;
 };
 
+$getJsonApiSchemas = function () {
+    return array_values(
+        array_unique(
+            array_map(
+                fn($class) => $class::TYPE,
+                app('json-api-integration-schemas')
+            )
+        )
+    );
+};
+
 $lang_attr = str_replace('_', '-', $_SESSION['_language']);
 ?>
 <!DOCTYPE html>
@@ -57,6 +68,7 @@ $lang_attr = str_replace('_', '-', $_SESSION['_language']);
                 'PERSONAL_NOTIFICATIONS_AUDIO_DEACTIVATED' =>
                     (bool) User::findCurrent()?->getConfiguration()->PERSONAL_NOTIFICATIONS_AUDIO_DEACTIVATED,
             ]) ?>,
+            jsonapi_schemas: <?= json_encode($getJsonApiSchemas()) ?>,
         }
     </script>
 
diff --git a/webpack.common.js b/webpack.common.js
index 4c5b16c981ddb9e9fa88ba3ee208b2a2e0ceec20..9fa316d50be0b961c20d704f30f2dc15a78690d2 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -1,7 +1,6 @@
-const webpack = require("webpack");
 const path = require("path");
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
-const VueLoaderPlugin = require('vue-loader/lib/plugin');
+const { VueLoaderPlugin } = require('vue-loader');
 const ESLintPlugin = require('eslint-webpack-plugin');
 const { CKEditorTranslationsPlugin } = require( '@ckeditor/ckeditor5-dev-translations' );
 
@@ -98,9 +97,6 @@ module.exports = {
             {
                 test: /\.vue$/,
                 loader: 'vue-loader',
-                options: {
-                    compiler: require('vue-template-babel-compiler')
-                }
             }
         ]
     },
@@ -125,7 +121,6 @@ module.exports = {
     ],
     resolve: {
         alias: {
-            'vue$': 'vue/dist/vue.esm.js',
             'jquery-ui/data': 'jquery-ui/ui/data',
             'jquery-ui/disable-selection': 'jquery-ui/ui/disable-selection',
             'jquery-ui/focusable': 'jquery-ui/ui/focusable',
@@ -146,7 +141,7 @@ module.exports = {
             'jquery-ui/widgets/draggable': 'jquery-ui/ui/widgets/draggable',
             'jquery-ui/widgets/droppable': 'jquery-ui/ui/widgets/droppable',
             'jquery-ui/widgets/resizable': 'jquery-ui/ui/widgets/resizable',
-            '@': path.resolve(__dirname, 'resources')
+            '@': path.resolve(__dirname, 'resources'),
         },
         extensions: ['.ts', '.vue', '.js'],
         fallback: {
diff --git a/webpack.dev.js b/webpack.dev.js
index 4049f54b28a6a9cd5fb2c3746597b739de813ee1..6d5da00d94b3d7c80e9f2cd8fafb0af053c5274b 100644
--- a/webpack.dev.js
+++ b/webpack.dev.js
@@ -18,6 +18,11 @@ module.exports = merge(common, {
                 /\.d\.[cm]ts$/
             ]
         }),
+        new webpack.DefinePlugin({
+            __VUE_OPTIONS_API__: 'true',
+            __VUE_PROD_DEVTOOLS__: 'true',
+            __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true'
+        }),
         new WebpackNotifierPlugin({
             appID: 'Stud.IP Webpack',
             title: function (params) {
diff --git a/webpack.prod.js b/webpack.prod.js
index 92d6803622c45d5718e4655c299a128f6a3167ed..6378db502d7fa0b1b28e2074bc31830066ef1001 100644
--- a/webpack.prod.js
+++ b/webpack.prod.js
@@ -11,6 +11,11 @@ module.exports = merge(common, {
     optimization: {
         minimize: true,
         minimizer: [
+            new webpack.DefinePlugin({
+                __VUE_OPTIONS_API__: 'true',
+                __VUE_PROD_DEVTOOLS__: 'false',
+                __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
+            }),
             new TerserPlugin(
                 {
                     extractComments: false