diff --git a/app/controllers/course/files.php b/app/controllers/course/files.php
index e40ac45631abcf3473ed3ca54709f2d46a884617..56de8aeb177b5009b6f4edbbbcc2701ba06179e8 100644
--- a/app/controllers/course/files.php
+++ b/app/controllers/course/files.php
@@ -37,7 +37,7 @@ class Course_FilesController extends AuthenticatedController
         if (is_object($GLOBALS['user']) && $GLOBALS['user']->id !== 'nobody') {
             $constraints = FileManager::getUploadTypeConfig($this->course->id);
 
-            PageLayout::addHeadElement('script', ['type' => 'text/javascript'], sprintf(
+            PageLayout::addHeadElement('script', ['type' => 'module'], sprintf(
                 'STUDIP.Files.setUploadConstraints(%s);',
                 json_encode($constraints)
             ));
diff --git a/app/controllers/files.php b/app/controllers/files.php
index b8ccf175909d8caba1d741dc77adecf08f77d491..ed0521558222ce2741623c41c4105e4e8d72e3fa 100644
--- a/app/controllers/files.php
+++ b/app/controllers/files.php
@@ -48,7 +48,7 @@ class FilesController extends AuthenticatedController
 
         $constraints = FileManager::getUploadTypeConfig($this->user->id);
 
-        PageLayout::addHeadElement('script', ['type' => 'text/javascript'], sprintf(
+        PageLayout::addHeadElement('script', ['type' => 'module'], sprintf(
             'STUDIP.Files.setUploadConstraints(%s);',
             json_encode($constraints)
         ));
diff --git a/app/controllers/institute/files.php b/app/controllers/institute/files.php
index ca46dc8973918a964f965e730aca37fbd2a23108..4ed8bc6f92da79e0f7ddf51abe9b04699c7da9d0 100644
--- a/app/controllers/institute/files.php
+++ b/app/controllers/institute/files.php
@@ -39,7 +39,7 @@ class Institute_FilesController extends AuthenticatedController
         if (is_object($GLOBALS['user']) && $GLOBALS['user']->id !== 'nobody') {
             $constraints = FileManager::getUploadTypeConfig($this->institute->id);
 
-            PageLayout::addHeadElement('script', ['type' => 'text/javascript'], sprintf(
+            PageLayout::addHeadElement('script', ['type' => 'module'], sprintf(
                 'STUDIP.Files.setUploadConstraints(%s);',
                 json_encode($constraints)
             ));
diff --git a/app/views/messages/write.php b/app/views/messages/write.php
index 8bf05f611903bec03794bad08e338c8e61b5794d..5492dbd69db67debaa0a7c7f44a1b80b95ac0003 100644
--- a/app/views/messages/write.php
+++ b/app/views/messages/write.php
@@ -69,7 +69,7 @@
         ?>
         </div>
         <script>
-            STUDIP.MultiPersonSearch.init();
+            STUDIP.ready(() => STUDIP.MultiPersonSearch.init());
         </script>
     </div>
     <div>
diff --git a/lib/classes/PageLayout.php b/lib/classes/PageLayout.php
index ed5e1aefdff89282a38a4513edcbc7aa694ece46..c4f73ce61af268406c18a85af423e205e7b757a2 100644
--- a/lib/classes/PageLayout.php
+++ b/lib/classes/PageLayout.php
@@ -134,9 +134,22 @@ class PageLayout
             'title' => _('Hilfe zur Textformatierung')
         ]);
 
-        self::addStylesheet('studip-base.css?v=' . $v, ['media' => 'screen']);
-        self::addScript('studip-base.js?v=' . $v);
-        self::addScript('studip-wysiwyg.js?v=' . $v);
+        self::addStylesheet('studip-bootstrap.css?v=' . $v, ['media' => 'screen']);
+
+        self::addScript('jquery.min.js?v=' . $v);
+        self::addScript('jquery-ui.min.js?v=' . $v);
+        self::addScript('select2.full.min.js?v=' . $v);
+        self::addScript('jquery.tablesorter.combined.min.js?v=' . $v);
+        self::addScript('jquery-ui-timepicker-addon.min.js?v=' . $v);
+        self::addScript('jquery.scrollTo.min.js?v=' . $v);
+        self::addScript('jquery.qrcode.min.js?v=' . $v);
+        self::addScript('jquery.ui.touch-punch.min.js?v=' . $v);
+        self::addScript('lodash.min.js?v=' . $v);
+        self::addStylesheet('jquery-ui-timepicker-addon.min.css?v=' . $v);
+
+        self::addScript('studip-legacy-libs.js?v=' . $v, ['type' => 'module']);
+        self::addScript('studip-lib.js?v=' . $v, ['type' => 'module']);
+        self::addScript('studip-bootstrap.js?v=' . $v, ['type' => 'module']);
 
         self::addStylesheet('print.css?v=' . $v, ['media' => 'print']);
 
diff --git a/package.json b/package.json
index e52d011741106e6725a26a0e491934c4367718a9..5e400ad7a2fd9d8567798b848f1566b5e09dde13 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
         "@types/jquery": "^3.5.16",
         "@types/jqueryui": "^1.12.16",
         "@types/lodash": "^4.14.191",
+        "@vitejs/plugin-vue": "^5.2.1",
         "@vue/compiler-sfc": "^3.5.13",
         "@vue/eslint-config-typescript": "^13.0.0",
         "altcha": "^0.3.2",
@@ -140,6 +141,8 @@
         "ts-loader": "^9.5.1",
         "typescript": "^5.7.2",
         "typescript-eslint": "^8.17.0",
+        "vite": "^6.2.1",
+        "vite-plugin-static-copy": "^2.3.0",
         "vue": "^3.5.13",
         "vue-dragscroll": "^4.0.6",
         "vue-loader": "^17.4.2",
diff --git a/resources/assets/javascripts/bootstrap/admission.js b/resources/assets/javascripts/bootstrap/admission.js
index 2fff6cfd22e600402132759cd1517c6ec57cccae..ba017db792a89343350d57af7c399b91a201c813 100644
--- a/resources/assets/javascripts/bootstrap/admission.js
+++ b/resources/assets/javascripts/bootstrap/admission.js
@@ -16,7 +16,7 @@ STUDIP.ready(function () {
 
         if (STUDIP.Admission.availableRules[ruleType] !== undefined) {
 
-            import('@/vue/components/admission/' + STUDIP.Admission.availableRules[ruleType])
+            import(`@/vue/components/admission/${STUDIP.Admission.availableRules[ruleType]}.vue`)
                 .then(result => {
                     const components = {};
                     components[ruleType] = result.default;
diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js
index 74c59657807e69a9168958c1df9325b6316cbafe..b91570949d5b5a4ad10ebde2b34ba26898a3382a 100644
--- a/resources/assets/javascripts/bootstrap/vue.js
+++ b/resources/assets/javascripts/bootstrap/vue.js
@@ -1,11 +1,10 @@
 import { defineAsyncComponent } from 'vue';
-import { isFunction } from "lodash";
 
 function attachComponents(app, configuredComponents) {
     configuredComponents.forEach(component => {
         const name = component.split('/').reverse()[0];
         app.component(name, defineAsyncComponent(() => {
-            const temp = import(`../../../vue/components/${component}.vue`);
+            const temp = importComponent(component);
             temp.then(({default: c}) => {
                 const mounted = c.mounted ?? null;
                 c.mounted = function (...args) {
@@ -29,6 +28,15 @@ function attachComponents(app, configuredComponents) {
     });
 }
 
+function importComponent(component) {
+    if (!component.includes("/")) {
+        return import(`../../../vue/components/${component}.vue`);
+    }
+
+    const [directory, file] = component.split("/");
+    return import(`../../../vue/components/${directory}/${file}.vue`);
+}
+
 STUDIP.ready(() => {
     document.querySelectorAll('[data-vue-app]:not([data-vue-app-created])').forEach(async (node) => {
         node.dataset.vueAppCreated = 'true';
@@ -50,7 +58,7 @@ STUDIP.ready(() => {
         // Set up vuex stores
         for (const [index, name] of Object.entries(config.vuexStores)) {
             promises.push(
-                import(`../../../vue/store/${name}`).then(storeConfig => {
+                import(`../../../vue/store/${name}.js`).then(storeConfig => {
                     if (!store.hasModule(index)) {
                         store.registerModule(index, storeConfig.default);
                     }
@@ -71,14 +79,14 @@ STUDIP.ready(() => {
         // Set up pinia stores
         for (const [name, command] of Object.entries(config.stores)) {
             promises.push(
-                import(`../../../vue/store/pinia/${name}`).then((storeConfig) => {
+                import(`../../../vue/store/pinia/${name}.js`).then((storeConfig) => {
                     const piniaStore = storeConfig[command]();
 
                     const dataElement = document.getElementById(`vue-store-data-${name}`);
                     if (dataElement) {
                         const data = JSON.parse(dataElement.innerText);
                         Object.keys(data).forEach(command => {
-                            if (isFunction(piniaStore[command])) {
+                            if (_.isFunction(piniaStore[command])) {
                                 piniaStore[command](data[command]);
                             } else {
                                 piniaStore[command] = data[command];
diff --git a/resources/assets/javascripts/chunk-loader.js b/resources/assets/javascripts/chunk-loader.js
index 8cb692cf87eba19430c2c4c8419aea3854929545..e5fc4d575ac54fe7ed95c14fc58dc6539f144cfa 100644
--- a/resources/assets/javascripts/chunk-loader.js
+++ b/resources/assets/javascripts/chunk-loader.js
@@ -24,41 +24,26 @@ export const loadChunk = function (chunk, { silent = false } = {}) {
         case 'courseware':
             promise = Promise.all([
                 STUDIP.loadChunk('vue'),
-                import(
-                    /* webpackChunkName: "courseware" */
-                    './chunks/courseware'
-                ),
+                import('./chunks/courseware'),
             ]).then(([Vue]) => Vue);
             break;
 
         case 'code-highlight':
-            promise = import(
-                /* webpackChunkName: "code-highlight" */
-                './chunks/code-highlight'
-            ).then(({default: hljs}) => {
+            promise = import('./chunks/code-highlight').then(({default: hljs}) => {
                 return hljs;
             });
             break;
 
         case 'chartist':
-            promise = import(
-                /* webpackChunkName: "chartist" */
-                './chunks/chartist'
-            ).then(({ default: Chartist }) => Chartist);
+            promise = import('./chunks/chartist').then(({ default: Chartist }) => Chartist);
             break;
 
         case 'fullcalendar':
-            promise = import(
-                /* webpackChunkName: "fullcalendar" */
-                './chunks/fullcalendar'
-            );
+            promise = import('./chunks/fullcalendar');
             break;
 
         case 'tablesorter':
-            promise = import(
-                /* webpackChunkName: "tablesorter" */
-                './chunks/tablesorter'
-            );
+            promise = import('./chunks/tablesorter');
             break;
 
         case 'mathjax':
@@ -81,17 +66,11 @@ export const loadChunk = function (chunk, { silent = false } = {}) {
             break;
 
         case 'vue':
-            promise = import(
-                /* webpackChunkName: "vue.js" */
-                './chunks/vue'
-            );
+            promise = import('./chunks/vue');
             break;
 
         case 'wysiwyg':
-            promise = import(
-                /* webpackChunkName: "wysiwyg.js" */
-                './chunks/wysiwyg'
-            );
+            promise = import('./chunks/wysiwyg');
             break;
 
         default:
diff --git a/resources/assets/javascripts/chunks/tablesorter.js b/resources/assets/javascripts/chunks/tablesorter.js
index ed77c026b7609467d70d282f68dad0a424a0dcb4..c58c7088928b274f9778b72e27a2deb5a792eafe 100644
--- a/resources/assets/javascripts/chunks/tablesorter.js
+++ b/resources/assets/javascripts/chunks/tablesorter.js
@@ -1,9 +1,5 @@
 import { $gettext } from '../lib/gettext'
 
-import "tablesorter/dist/js/jquery.tablesorter"
-import "tablesorter/dist/js/extras/jquery.tablesorter.pager.min.js"
-import "tablesorter/dist/js/jquery.tablesorter.widgets.js"
-
 jQuery.tablesorter.addParser({
     id: 'htmldata',
     is(s, table, cell) {
diff --git a/resources/assets/javascripts/chunks/vue.js b/resources/assets/javascripts/chunks/vue.js
index eafade2ab7b316c537d5d6b94be7d69f81762e94..d7620c6da251d9319102bdc9a31a21bd00fdd0fd 100644
--- a/resources/assets/javascripts/chunks/vue.js
+++ b/resources/assets/javascripts/chunks/vue.js
@@ -70,6 +70,9 @@ function createApp(options = {}, ...args) {
     registerGlobalComponents(app);
     registerGlobalDirectives(app);
 
+    // Make the current state of "focus mode" (fullscreen) available to Vue components.
+    eventBus.on('switch-focus-mode', (mode) => store.commit('studip/switchConsumeMode', mode));
+
     if (options.el) {
         app.mount(options.el);
     }
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-bootstrap.js
similarity index 91%
rename from resources/assets/javascripts/entry-base.js
rename to resources/assets/javascripts/entry-bootstrap.js
index db3d1f621aefbef1110a374950447e98ede49739..714a9a9edb780452082df71fa001fc5f2820c37d 100644
--- a/resources/assets/javascripts/entry-base.js
+++ b/resources/assets/javascripts/entry-bootstrap.js
@@ -1,18 +1,6 @@
-import './public-path.js'
-
-// promise polyfill needed for IE11 to load tablesorter
-import 'es6-promise/auto'
-
 import "../stylesheets/studip-jquery-ui.scss"
-// Basic scss support
 import "../stylesheets/studip.scss"
 
-import lodash from "lodash"
-window._ = lodash
-
-import "./jquery-bundle.js"
-
-import "./init.js"
 import "./bootstrap/responsive.js"
 import "./bootstrap/vue.js"
 
@@ -79,6 +67,7 @@ import "./bootstrap/courseware.js"
 import "./bootstrap/external_pages.js"
 import "./bootstrap/vips.js"
 import "./bootstrap/admission.js"
+import "./bootstrap/wysiwyg.js"
 
 import "./mvv_course_wizard.js"
 import "./mvv.js"
diff --git a/resources/assets/javascripts/entry-legacy-libs.js b/resources/assets/javascripts/entry-legacy-libs.js
new file mode 100644
index 0000000000000000000000000000000000000000..3994fc20967c663ebc83915736be59d9486649ce
--- /dev/null
+++ b/resources/assets/javascripts/entry-legacy-libs.js
@@ -0,0 +1,71 @@
+import 'multiselect';
+
+import './studip-jquery-tweaks.js';
+import './studip-jquery.multi-select.tweaks.js';
+import './studip-jquery-selection-helper.js';
+
+import 'blueimp-file-upload';
+import 'blueimp-file-upload/js/jquery.iframe-transport.js';
+
+import './jquery/autoresize.jquery.min.js';
+
+// Create jQuery "plugin" that just reverses the elements' order. This is
+// neccessary since the navigation is built and afterwards, we need to
+// check the navigation's open status in reverse order (from bottom to top)
+jQuery.fn.reverse = [].reverse;
+
+$.fn.extend({
+    showAjaxNotification(position) {
+        position = position || 'left';
+        return this.each(function () {
+            if ($(this).data('ajax_notification')) {
+                return;
+            }
+
+            $(this).wrap('<span class="ajax_notification" />');
+            const thisHeight = $(this).height();
+            const thisPosition = $(this).position();
+            const notification = $('<span class="notification" />').hide().insertBefore(this);
+            const changes = {
+                marginLeft: 0,
+                marginRight: 0,
+            };
+
+            changes[position === 'right' ? 'marginRight' : 'marginLeft'] = notification.outerWidth(true);
+
+            $(this)
+                .data({
+                    ajax_notification: notification,
+                })
+                .parent()
+                .animate(changes, 'fast', function () {
+                    const offset = thisPosition;
+                    const styles = {
+                        left: offset.left - notification.outerWidth(true),
+                        top: offset.top + Math.max(0, Math.floor((thisHeight - notification.outerHeight(true)) / 2)),
+                    };
+                    if (position === 'right') {
+                        styles.left += $(this).outerWidth(true);
+                    }
+                    notification.css(styles).fadeIn('fast');
+                });
+        });
+    },
+    hideAjaxNotification() {
+        return this.each(function () {
+            var $this = $(this).stop(),
+                notification = $this.data('ajax_notification');
+            if (!notification) {
+                return;
+            }
+
+            notification.stop().fadeOut('fast', function () {
+                $this.animate({ marginLeft: 0, marginRight: 0 }, 'fast', function () {
+                    $this.unwrap();
+                });
+                $(this).remove();
+            });
+            $(this).removeData('ajax_notification');
+        });
+    },
+});
diff --git a/resources/assets/javascripts/entry-lib.js b/resources/assets/javascripts/entry-lib.js
new file mode 100644
index 0000000000000000000000000000000000000000..d9d9e760e685535c6b4892615bff4ddab98505e8
--- /dev/null
+++ b/resources/assets/javascripts/entry-lib.js
@@ -0,0 +1,3 @@
+import { STUDIP } from './lib/studip.js';
+
+Object.assign(window.STUDIP, STUDIP);
diff --git a/resources/assets/javascripts/entry-wysiwyg.js b/resources/assets/javascripts/entry-wysiwyg.js
deleted file mode 100644
index 4255f4d6a62dd02382d3441f9fe30cf9b9ebee1c..0000000000000000000000000000000000000000
--- a/resources/assets/javascripts/entry-wysiwyg.js
+++ /dev/null
@@ -1 +0,0 @@
-import "./bootstrap/wysiwyg.js"
diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js
deleted file mode 100644
index 4af6ed9659ddb8be41fdff57b11a66df7b8aa764..0000000000000000000000000000000000000000
--- a/resources/assets/javascripts/init.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import { loadChunk, loadScript, } from './chunk-loader.js';
-import Vue from './lib/studip-vue.js';
-
-import ActionMenu from './lib/actionmenu.js';
-import ActivityFeed from './lib/activityfeed.js';
-import admin_sem_class from './lib/admin_sem_class.js';
-import AdminCourses from './lib/admin-courses.js';
-import Admission from './lib/admission.js';
-import Arbeitsgruppen from './lib/arbeitsgruppen.js';
-import Archive from './lib/archive.js';
-import Avatar from './lib/avatar.js';
-import BigImageHandler from './lib/big_image_handler.js';
-import Blubber from './lib/blubber.js';
-import Browse from './lib/browse.js';
-import Cache from './lib/cache.js';
-import Calendar from './lib/calendar.js';
-import Clipboard from './lib/clipboard.js';
-import Cookie from './lib/cookie.js';
-import CourseWizard from './lib/course_wizard.js';
-import { createURLHelper } from './lib/url_helper';
-import CSS from './lib/css.js';
-import Dates from './lib/dates.js';
-import DateTime from './lib/datetime.js';
-import Dialog from './lib/dialog.js';
-import DragAndDropUpload from './lib/drag_and_drop_upload.js';
-import enrollment from './lib/enrollment.js';
-import eventBus from './lib/event-bus.ts';
-import extractCallback from './lib/extract_callback.js';
-import Files from './lib/files.js';
-import FilesDashboard from './lib/files_dashboard.js';
-import Folders from './lib/folders.js';
-import Forms from './lib/forms.js';
-import Forum from './lib/forum.js';
-import Fullcalendar from './lib/fullcalendar.js';
-import Fullscreen from './lib/fullscreen.js';
-import GlobalSearch from './lib/global_search.js';
-import HeaderMagic from './lib/header_magic.js';
-import i18n from './lib/i18n.js';
-import InlineEditing from './lib/inline-editing.js';
-import JSONAPI, { jsonapi } from './lib/jsonapi.ts';
-import JSUpdater from './lib/jsupdater.js';
-import Lightbox from './lib/lightbox.js';
-import Markup from './lib/markup.js';
-import Members from './lib/members.js';
-import Messages from './lib/messages.js';
-import MultiPersonSearch from './lib/multi_person_search.js';
-import MultiSelect from './lib/multi_select.js';
-import NavigationShrinker from './lib/navigation_shrinker.js';
-import OER from './lib/oer.js';
-import OldUpload from './lib/old_upload.js';
-import Overlapping from './lib/overlapping.js';
-import Overlay from './lib/overlay.js';
-import PageLayout from './lib/page_layout.js';
-import parseOptions from './lib/parse_options.js';
-import PersonalNotifications from './lib/personal_notifications.js';
-import QRCode from './lib/qr_code.js';
-import Questionnaire from './lib/questionnaire.js';
-import QuickSearch from './lib/quick_search.js';
-import QuickSelection from './lib/quick_selection.js';
-import Raumzeit from './lib/raumzeit.js';
-import {ready, domReady, dialogReady} from './lib/ready.js';
-import Report from './lib/report.ts';
-import Resources from './lib/resources.js';
-import Responsive from './lib/responsive.js';
-import Screenreader from './lib/screenreader.js';
-import Scroll from './lib/scroll.js';
-import Search from './lib/search.js';
-import Sidebar from './lib/sidebar.js';
-import SkipLinks from './lib/skip_links.js';
-import startpage from './lib/startpage.js';
-import Statusgroups from './lib/statusgroups.js';
-import study_area_selection from './lib/study_area_selection.js';
-import Table from './lib/table.js';
-import TableOfContents from './lib/table-of-contents.js';
-import Tooltip from './lib/tooltip.js';
-import Tour from './lib/tour.js';
-import * as Gettext from './lib/gettext';
-import UserFilter from './lib/user_filter.js';
-import wysiwyg from './lib/wysiwyg.js';
-import ScrollToTop from './lib/scroll_to_top.js';
-import * as Vips from './lib/vips.js';
-
-const configURLHelper = _.get(window, 'STUDIP.URLHelper', {});
-const URLHelper = createURLHelper(configURLHelper);
-
-window.STUDIP = _.assign(window.STUDIP || {}, {
-    ActionMenu,
-    ActivityFeed,
-    admin_sem_class,
-    AdminCourses,
-    Admission,
-    Arbeitsgruppen,
-    Archive,
-    Avatar,
-    BigImageHandler,
-    Blubber,
-    Browse,
-    Cache,
-    Calendar,
-    Cookie,
-    CourseWizard,
-    CSS,
-    Dates,
-    DateTime,
-    Dialog,
-    DragAndDropUpload,
-    enrollment,
-    eventBus,
-    extractCallback,
-    Files,
-    FilesDashboard,
-    Folders,
-    Forms,
-    Forum,
-    Fullcalendar,
-    Fullscreen,
-    Gettext,
-    GlobalSearch,
-    HeaderMagic,
-    i18n,
-    InlineEditing,
-    jsonapi,
-    JSONAPI,
-    JSUpdater,
-    Lightbox,
-    loadChunk,
-    loadScript,
-    Markup,
-    Members,
-    Messages,
-    MultiPersonSearch,
-    MultiSelect,
-    NavigationShrinker,
-    OER,
-    OldUpload,
-    Overlapping,
-    Overlay,
-    PageLayout,
-    parseOptions,
-    PersonalNotifications,
-    QRCode,
-    Questionnaire,
-    QuickSearch,
-    QuickSelection,
-    Raumzeit,
-    Report,
-    Responsive,
-    Scroll,
-    Screenreader,
-    Search,
-    Sidebar,
-    SkipLinks,
-    startpage,
-    Statusgroups,
-    study_area_selection,
-    Table,
-    TableOfContents,
-    Tooltip,
-    Tour,
-    URLHelper,
-    UserFilter,
-    wysiwyg,
-    Resources,
-    Clipboard,
-    ready,
-    domReady,
-    dialogReady,
-    ScrollToTop,
-    Vips,
-    Vue,
-});
diff --git a/resources/assets/javascripts/jquery-bundle.js b/resources/assets/javascripts/jquery-bundle.js
deleted file mode 100644
index f96bd24e4457e676d53684e737ea49576f073387..0000000000000000000000000000000000000000
--- a/resources/assets/javascripts/jquery-bundle.js
+++ /dev/null
@@ -1,150 +0,0 @@
-import $ from 'expose-loader?exposes=$,jQuery!jquery';
-
- import { setLocale } from './lib/gettext';
-
-import 'jquery-ui/ui/widget.js';
-import 'jquery-ui/ui/position.js';
-import 'jquery-ui/ui/data.js';
-import 'jquery-ui/ui/disable-selection.js';
-import 'jquery-ui/ui/focusable.js';
-import 'jquery-ui/ui/form.js';
-import 'jquery-ui/ui/form-reset-mixin.js';
-import 'jquery-ui/ui/ie.js';
-import 'jquery-ui/ui/keycode.js';
-import 'jquery-ui/ui/labels.js';
-import 'jquery-ui/ui/plugin.js';
-import 'jquery-ui/ui/safe-active-element.js';
-import 'jquery-ui/ui/safe-blur.js';
-import 'jquery-ui/ui/scroll-parent.js';
-import 'jquery-ui/ui/tabbable.js';
-import 'jquery-ui/ui/unique-id.js';
-import 'jquery-ui/ui/version.js';
-import 'jquery-ui/ui/widgets/draggable.js';
-import 'jquery-ui/ui/widgets/droppable.js';
-import 'jquery-ui/ui/widgets/resizable.js';
-import 'jquery-ui/ui/widgets/selectable.js';
-import 'jquery-ui/ui/widgets/sortable.js';
-import 'jquery-ui/ui/widgets/accordion.js';
-import 'jquery-ui/ui/widgets/autocomplete.js';
-import 'jquery-ui/ui/widgets/button.js';
-import 'jquery-ui/ui/widgets/checkboxradio.js';
-import 'jquery-ui/ui/widgets/controlgroup.js';
-import 'jquery-ui/ui/widgets/datepicker.js';
-import 'jquery-ui/ui/widgets/dialog.js';
-import 'jquery-ui/ui/widgets/menu.js';
-import 'jquery-ui/ui/widgets/mouse.js';
-import 'jquery-ui/ui/widgets/progressbar.js';
-import 'jquery-ui/ui/widgets/selectmenu.js';
-import 'jquery-ui/ui/widgets/slider.js';
-import 'jquery-ui/ui/widgets/spinner.js';
-import 'jquery-ui/ui/widgets/tabs.js';
-import 'jquery-ui/ui/widgets/tooltip.js';
-import 'jquery-ui/ui/effect.js';
-import 'jquery-ui/ui/effects/effect-blind.js';
-import 'jquery-ui/ui/effects/effect-bounce.js';
-import 'jquery-ui/ui/effects/effect-clip.js';
-import 'jquery-ui/ui/effects/effect-drop.js';
-import 'jquery-ui/ui/effects/effect-explode.js';
-import 'jquery-ui/ui/effects/effect-fade.js';
-import 'jquery-ui/ui/effects/effect-fold.js';
-import 'jquery-ui/ui/effects/effect-highlight.js';
-import 'jquery-ui/ui/effects/effect-puff.js';
-import 'jquery-ui/ui/effects/effect-pulsate.js';
-import 'jquery-ui/ui/effects/effect-scale.js';
-import 'jquery-ui/ui/effects/effect-shake.js';
-import 'jquery-ui/ui/effects/effect-size.js';
-import 'jquery-ui/ui/effects/effect-slide.js';
-import 'jquery-ui/ui/effects/effect-transfer.js';
-
-import 'jquery-ui-timepicker-addon';
-
-import 'multiselect';
-
-import 'jquery.scrollto';
-import 'jquery.qrcode';
-
-import 'jquery-ui-touch-punch';
-
-import './studip-jquery-tweaks.js';
-import './studip-jquery.multi-select.tweaks.js';
-import './studip-jquery-selection-helper.js';
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import select2 from 'select2/dist/js/select2.full.js';
-
-import 'blueimp-file-upload';
-import 'blueimp-file-upload/js/jquery.iframe-transport.js';
-
-import './jquery/autoresize.jquery.min.js';
-
-// Create jQuery "plugin" that just reverses the elements' order. This is
-// neccessary since the navigation is built and afterwards, we need to
-// check the navigation's open status in reverse order (from bottom to top)
-jQuery.fn.reverse = [].reverse;
-
-$.fn.extend({
-    showAjaxNotification(position) {
-        position = position || 'left';
-        return this.each(function () {
-            if ($(this).data('ajax_notification')) {
-                return;
-            }
-
-            $(this).wrap('<span class="ajax_notification" />');
-            const thisHeight = $(this).height();
-            const thisPosition = $(this).position();
-            const notification = $('<span class="notification" />')
-                .hide()
-                .insertBefore(this);
-            const changes = {
-                marginLeft: 0,
-                marginRight: 0
-            };
-
-            changes[position === 'right' ? 'marginRight' : 'marginLeft'] = notification.outerWidth(true);
-
-            $(this)
-                .data({
-                    ajax_notification: notification
-                })
-                .parent()
-                .animate(changes, 'fast', function () {
-                    const offset = thisPosition;
-                    const styles = {
-                        left: offset.left - notification.outerWidth(true),
-                        top:
-                            offset.top +
-                            Math.max(0, Math.floor((thisHeight - notification.outerHeight(true)) / 2))
-                    };
-                    if (position === 'right') {
-                        styles.left += $(this).outerWidth(true);
-                    }
-                    notification.css(styles).fadeIn('fast');
-                });
-        });
-    },
-    hideAjaxNotification() {
-        return this.each(function () {
-            var $this = $(this).stop(),
-                notification = $this.data('ajax_notification');
-            if (!notification) {
-                return;
-            }
-
-            notification.stop().fadeOut('fast', function () {
-                $this.animate({marginLeft: 0, marginRight: 0}, 'fast', function () {
-                    $this.unwrap();
-                });
-                $(this).remove();
-            });
-            $(this).removeData('ajax_notification');
-        });
-    }
-});
-
-$(document).ready(async () => {
-    await setLocale();
-    STUDIP.ready.trigger('dom');
-}).on('dialog-update', (event, data) => {
-    STUDIP.ready.trigger('dialog', data.dialog);
-});
diff --git a/resources/assets/javascripts/lib/admission.js b/resources/assets/javascripts/lib/admission.js
index c735cb2b79ab67c444b896f7282383a56092122d..01499c76d74a38656f286a279444e430f83c2ebf 100644
--- a/resources/assets/javascripts/lib/admission.js
+++ b/resources/assets/javascripts/lib/admission.js
@@ -9,15 +9,15 @@ const Admission = {
      * All registered rule types with their corresponding Vue components
      */
     availableRules: {
-        ConditionalAdmission: 'ConditionalAdmission.vue',
-        CourseMemberAdmission: 'CourseMemberAdmission.vue',
-        LimitedAdmission: 'LimitedAdmission.vue',
-        LockedAdmission: 'LockedAdmission.vue',
-        ParticipantRestrictedAdmission: 'ParticipantRestrictedAdmission.vue',
-        PasswordAdmission: 'PasswordAdmission.vue',
-        PreferentialAdmission: 'PreferentialAdmission.vue',
-        TermsAdmission: 'TermsAdmission.vue',
-        TimedAdmission: 'TimedAdmission.vue'
+        ConditionalAdmission: 'ConditionalAdmission',
+        CourseMemberAdmission: 'CourseMemberAdmission',
+        LimitedAdmission: 'LimitedAdmission',
+        LockedAdmission: 'LockedAdmission',
+        ParticipantRestrictedAdmission: 'ParticipantRestrictedAdmission',
+        PasswordAdmission: 'PasswordAdmission',
+        PreferentialAdmission: 'PreferentialAdmission',
+        TermsAdmission: 'TermsAdmission',
+        TimedAdmission: 'TimedAdmission'
     },
 
     getCourses: function(targetUrl) {
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js
index f4e2826aed9dad4a8a90fc55f28add00d8373945..e470abe356a7c2439399ba8dba74c420cb729468 100644
--- a/resources/assets/javascripts/lib/fullcalendar.js
+++ b/resources/assets/javascripts/lib/fullcalendar.js
@@ -13,8 +13,6 @@ import resourceCommonPlugin from '@fullcalendar/resource-common';
 import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
 import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
 
-import { jsPDF } from 'jspdf';
-import html2canvas from 'html2canvas';
 import Responsive from "./responsive";
 
 Date.prototype.getWeekNumber = function () {
@@ -178,30 +176,35 @@ class Fullcalendar
 
     static downloadPDF(format = 'landscape', withWeekend = false)
     {
-        $('*[data-fullcalendar="1"]').each(function () {
+        $('*[data-fullcalendar="1"]').each(async function () {
             if (this.calendar != undefined) {
                 $(this).addClass('print-view').toggleClass('without-weekend', !withWeekend);
 
-                var title = $(this).data('title');
-                let print_title = $('<h1>').text(title).prependTo(this);
+                const title = $(this).data('title');
+                const print_title = $('<h1>').text(title).prependTo(this);
 
                 window.scrollTo(0, 0);
 
-                html2canvas(this).then(canvas => {
-                    var imgData = canvas.toDataURL('image/jpeg');
-                    var pdf = new jsPDF({
-                        orientation: format === 'landscape' ? 'landscape' : 'portrait'
-                    });
-                    if (format === 'landscape') {
-                        pdf.addImage(imgData, 'JPEG', 20, 20, 250, 250, 'i1', 'NONE', 0);
-                    } else {
-                        pdf.addImage(imgData, 'JPEG', 25, 20, 160, 190, 'i1', 'NONE', 0);
-                    }
-                    pdf.save(title + '.pdf');
-                });
-
                 print_title.remove();
                 $(this).removeClass('print-view without-weekend');
+
+                const [html2canvas, { jsPDF }] = await Promise.all([
+                    import('html2canvas'),
+                    import('jspdf')
+                ]);
+
+                const canvas = await html2canvas(this);
+
+                var imgData = canvas.toDataURL('image/jpeg');
+                var pdf = new jsPDF({
+                    orientation: format === 'landscape' ? 'landscape' : 'portrait'
+                });
+                if (format === 'landscape') {
+                    pdf.addImage(imgData, 'JPEG', 20, 20, 250, 250, 'i1', 'NONE', 0);
+                } else {
+                    pdf.addImage(imgData, 'JPEG', 25, 20, 160, 190, 'i1', 'NONE', 0);
+                }
+                pdf.save(title + '.pdf');
             }
         });
     }
diff --git a/resources/assets/javascripts/lib/gettext.ts b/resources/assets/javascripts/lib/gettext.ts
index 3ed710ad8642109ad37b99cef0996c1d9c41d0ba..12f037f29653ea12f8d994394c8a36b185abd365 100644
--- a/resources/assets/javascripts/lib/gettext.ts
+++ b/resources/assets/javascripts/lib/gettext.ts
@@ -1,5 +1,4 @@
 import {createGettext, LanguageData} from 'vue3-gettext';
-import * as defaultTranslations from '../../../../locale/de/LC_MESSAGES/js-resources.json';
 import eventBus from './event-bus';
 
 interface StringDict {
@@ -92,7 +91,7 @@ function getAvailableLanguages() {
 
 function getInitialState() {
     const translations: Translations = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => {
-        memo[lang] = lang === DEFAULT_LANG ? defaultTranslations : '';
+        memo[lang] = {};
 
         return memo;
     }, {} as Translations);
diff --git a/resources/assets/javascripts/lib/reststate-vuex.js b/resources/assets/javascripts/lib/reststate-vuex.js
index 47aaaaaec455848fbf722d21eccfa81f19524c05..aabdce18e439e6c14bf80abe305d9b0296c26313 100644
--- a/resources/assets/javascripts/lib/reststate-vuex.js
+++ b/resources/assets/javascripts/lib/reststate-vuex.js
@@ -1,5 +1,4 @@
 import ResourceClient from './reststate-client.js';
-import { isEqual } from 'lodash';
 
 const STATUS_INITIAL = 'INITIAL';
 const STATUS_LOADING = 'LOADING';
@@ -83,7 +82,7 @@ const storeIncluded = ({ dispatch }, result) => {
 };
 
 const matches = criteria => test =>
-    Object.keys(criteria).every(key => isEqual(criteria[key], test[key]));
+    Object.keys(criteria).every(key => _.isEqual(criteria[key], test[key]));
 
 const handleError = commit => errorResponse => {
     commit('SET_STATUS', STATUS_ERROR);
diff --git a/resources/assets/javascripts/lib/studip.js b/resources/assets/javascripts/lib/studip.js
new file mode 100644
index 0000000000000000000000000000000000000000..0a5d92423f290f626ddc374e66485bce3cd654fa
--- /dev/null
+++ b/resources/assets/javascripts/lib/studip.js
@@ -0,0 +1,169 @@
+import { loadChunk, loadScript, } from '../chunk-loader.js';
+import Vue from './studip-vue.js';
+
+import ActionMenu from './actionmenu.js';
+import ActivityFeed from './activityfeed.js';
+import admin_sem_class from './admin_sem_class.js';
+import AdminCourses from './admin-courses.js';
+import Admission from './admission.js';
+import Arbeitsgruppen from './arbeitsgruppen.js';
+import Archive from './archive.js';
+import Avatar from './avatar.js';
+import BigImageHandler from './big_image_handler.js';
+import Blubber from './blubber.js';
+import Browse from './browse.js';
+import Cache from './cache.js';
+import Calendar from './calendar.js';
+import Clipboard from './clipboard.js';
+import Cookie from './cookie.js';
+import CourseWizard from './course_wizard.js';
+import { createURLHelper } from './url_helper';
+import CSS from './css.js';
+import Dates from './dates.js';
+import DateTime from './datetime.js';
+import Dialog from './dialog.js';
+import DragAndDropUpload from './drag_and_drop_upload.js';
+import enrollment from './enrollment.js';
+import eventBus from './event-bus.ts';
+import extractCallback from './extract_callback.js';
+import Files from './files.js';
+import FilesDashboard from './files_dashboard.js';
+import Folders from './folders.js';
+import Forms from './forms.js';
+import Forum from './forum.js';
+import Fullcalendar from './fullcalendar.js';
+import Fullscreen from './fullscreen.js';
+import GlobalSearch from './global_search.js';
+import HeaderMagic from './header_magic.js';
+import i18n from './i18n.js';
+import InlineEditing from './inline-editing.js';
+import JSONAPI, { jsonapi } from './jsonapi.ts';
+import JSUpdater from './jsupdater.js';
+import Lightbox from './lightbox.js';
+import Markup from './markup.js';
+import Members from './members.js';
+import Messages from './messages.js';
+import MultiPersonSearch from './multi_person_search.js';
+import MultiSelect from './multi_select.js';
+import NavigationShrinker from './navigation_shrinker.js';
+import OER from './oer.js';
+import OldUpload from './old_upload.js';
+import Overlapping from './overlapping.js';
+import Overlay from './overlay.js';
+import PageLayout from './page_layout.js';
+import parseOptions from './parse_options.js';
+import PersonalNotifications from './personal_notifications.js';
+import QRCode from './qr_code.js';
+import Questionnaire from './questionnaire.js';
+import QuickSearch from './quick_search.js';
+import QuickSelection from './quick_selection.js';
+import Raumzeit from './raumzeit.js';
+import Report from './report.ts';
+import Resources from './resources.js';
+import Responsive from './responsive.js';
+import Screenreader from './screenreader.js';
+import Scroll from './scroll.js';
+import Search from './search.js';
+import Sidebar from './sidebar.js';
+import SkipLinks from './skip_links.js';
+import startpage from './startpage.js';
+import Statusgroups from './statusgroups.js';
+import study_area_selection from './study_area_selection.js';
+import Table from './table.js';
+import TableOfContents from './table-of-contents.js';
+import Tooltip from './tooltip.js';
+import Tour from './tour.js';
+import * as Gettext from './gettext';
+import UserFilter from './user_filter.js';
+import wysiwyg from './wysiwyg.js';
+import ScrollToTop from './scroll_to_top.js';
+import * as Vips from './vips.js';
+
+const configURLHelper = window?.STUDIP?.URLHelper ?? {};
+const URLHelper = createURLHelper(configURLHelper);
+
+const STUDIP = {
+    ActionMenu,
+    ActivityFeed,
+    admin_sem_class,
+    AdminCourses,
+    Admission,
+    Arbeitsgruppen,
+    Archive,
+    Avatar,
+    BigImageHandler,
+    Blubber,
+    Browse,
+    Cache,
+    Calendar,
+    Cookie,
+    CourseWizard,
+    CSS,
+    Dates,
+    DateTime,
+    Dialog,
+    DragAndDropUpload,
+    enrollment,
+    eventBus,
+    extractCallback,
+    Files,
+    FilesDashboard,
+    Folders,
+    Forms,
+    Forum,
+    Fullcalendar,
+    Fullscreen,
+    Gettext,
+    GlobalSearch,
+    HeaderMagic,
+    i18n,
+    InlineEditing,
+    jsonapi,
+    JSONAPI,
+    JSUpdater,
+    Lightbox,
+    loadChunk,
+    loadScript,
+    Markup,
+    Members,
+    Messages,
+    MultiPersonSearch,
+    MultiSelect,
+    NavigationShrinker,
+    OER,
+    OldUpload,
+    Overlapping,
+    Overlay,
+    PageLayout,
+    parseOptions,
+    PersonalNotifications,
+    QRCode,
+    Questionnaire,
+    QuickSearch,
+    QuickSelection,
+    Raumzeit,
+    Report,
+    Responsive,
+    Scroll,
+    Screenreader,
+    Search,
+    Sidebar,
+    SkipLinks,
+    startpage,
+    Statusgroups,
+    study_area_selection,
+    Table,
+    TableOfContents,
+    Tooltip,
+    Tour,
+    URLHelper,
+    UserFilter,
+    wysiwyg,
+    Resources,
+    Clipboard,
+    ScrollToTop,
+    Vips,
+    Vue,
+};
+
+export { STUDIP };
diff --git a/resources/assets/javascripts/public-path.js b/resources/assets/javascripts/public-path.js
deleted file mode 100644
index a84ad1b63cf0f46944619dc65ea60a26d3a1b36f..0000000000000000000000000000000000000000
--- a/resources/assets/javascripts/public-path.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/* global __webpack_public_path__ : writable */
-__webpack_public_path__ = __webpack_public_path__ || (window.STUDIP && window.STUDIP.ASSETS_URL)
diff --git a/resources/assets/stylesheets/mixins.scss b/resources/assets/stylesheets/mixins.scss
index a7bb714cafa3454c84fd184d7f783e881a9daef4..9c64f9e5ce27746f03ce088d08b90fa85792c1ba 100644
--- a/resources/assets/stylesheets/mixins.scss
+++ b/resources/assets/stylesheets/mixins.scss
@@ -1,4 +1,4 @@
-$image-path: "../images" !default;
+$image-path: "/assets/images" !default;
 $icon-path: "#{$image-path}/icons" !default;
 
 @import "scss/variables";
diff --git a/resources/assets/stylesheets/print.scss b/resources/assets/stylesheets/print.scss
index 070ed4f39ee0e037241b64c69d66d6cc83dd25fe..ce63d5e28e4b74648eb5e080f2294d5414cb8366 100644
--- a/resources/assets/stylesheets/print.scss
+++ b/resources/assets/stylesheets/print.scss
@@ -121,7 +121,7 @@ td.rahmen_table_row_odd {
     border-top: 1pt solid var(--black);
 
     &:after {
-        content: url('../images/logos/logo2b.png');
+        content: url('/assets/images/logos/logo2b.png');
     }
 }
 
@@ -181,7 +181,7 @@ td.quote {
 
 a.link-intern {
     padding-left: 18px;
-    @include background-icon(intern);
+    @include background-icon(link-intern);
     background-repeat: no-repeat;
 }
 
diff --git a/resources/assets/stylesheets/scss/font-face-lato.scss b/resources/assets/stylesheets/scss/font-face-lato.scss
index 2961b5a8ec4205b0c8f1a743e2d1d57637883f8e..fcba87345d258111292ce1e40ee7ce9ca19cf78a 100644
--- a/resources/assets/stylesheets/scss/font-face-lato.scss
+++ b/resources/assets/stylesheets/scss/font-face-lato.scss
@@ -1,10 +1,10 @@
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Thin.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Thin.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Thin.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Thin.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Thin.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Thin.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Thin.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Thin.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Thin.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Thin.ttf') format('truetype');
   font-display: auto;
   font-style: normal;
   font-weight: 100;
@@ -14,11 +14,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-ThinItalic.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-ThinItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-ThinItalic.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-ThinItalic.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-ThinItalic.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.ttf') format('truetype');
   font-display: auto;
   font-style: italic;
   font-weight: 100;
@@ -28,11 +28,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Light.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Light.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Light.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Light.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Light.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Light.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Light.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Light.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Light.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Light.ttf') format('truetype');
   font-display: auto;
   font-style: normal;
   font-weight: 300;
@@ -42,11 +42,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-LightItalic.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-LightItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-LightItalic.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-LightItalic.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-LightItalic.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.ttf') format('truetype');
   font-display: auto;
   font-style: italic;
   font-weight: 300;
@@ -56,11 +56,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Regular.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Regular.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Regular.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Regular.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Regular.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Regular.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Regular.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Regular.ttf') format('truetype');
   font-display: auto;
   font-style: normal;
   font-weight: 400;
@@ -70,11 +70,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Italic.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Italic.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Italic.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Italic.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Italic.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Italic.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Italic.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Italic.ttf') format('truetype');
   font-display: auto;
   font-style: italic;
   font-weight: 400;
@@ -84,11 +84,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Bold.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Bold.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Bold.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Bold.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Bold.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Bold.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Bold.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Bold.ttf') format('truetype');
   font-display: auto;
   font-style: normal;
   font-weight: 700;
@@ -98,11 +98,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-BoldItalic.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-BoldItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-BoldItalic.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-BoldItalic.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-BoldItalic.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.ttf') format('truetype');
   font-display: auto;
   font-style: italic;
   font-weight: 700;
diff --git a/resources/assets/stylesheets/scss/installer.scss b/resources/assets/stylesheets/scss/installer.scss
index 865144f93c997b6c1107b4ec60923eb7d9daab10..dd545dfa39cd9784eb532d0c66fb951b1dab7ae7 100644
--- a/resources/assets/stylesheets/scss/installer.scss
+++ b/resources/assets/stylesheets/scss/installer.scss
@@ -6,8 +6,8 @@ body#install {
 }
 
 .stage {
-    $image-path: '../images/';
-    $icon-path: '#{$image-path}icons/';
+    $image-path: '/assets/images';
+    $icon-path: '#{$image-path}/icons';
 
     background: #fff;
     margin: 0 auto;
@@ -21,7 +21,7 @@ body#install {
         left: auto;
 
         &.ui-widget.ui-widget-content .ui-dialog-titlebar {
-            background-image: url('#{$image-path}logos/studip-logo.svg');
+            background-image: url('#{$image-path}/logos/studip-logo.svg');
             background-position: right 10px top 10px;
             background-repeat: no-repeat;
             background-size: 120px;
@@ -99,19 +99,19 @@ body#install {
 
             &.failed {
                 &::before {
-                    content: url('#{$icon-path}red/decline.svg') ' ';
+                    content: url('#{$icon-path}/red/decline.svg') ' ';
                 }
                 color: var(--red);
             }
             &.success {
                 &::before {
-                    content: url('#{$icon-path}green/accept.svg') ' ';
+                    content: url('#{$icon-path}/green/accept.svg') ' ';
                 }
                 color: var(--green);
             }
             &.notice {
                 &::before {
-                    content: url('#{$icon-path}blue/info-circle.svg') ' ';
+                    content: url('#{$icon-path}/blue/info-circle.svg') ' ';
                 }
                 color: var(--black);
             }
diff --git a/resources/assets/stylesheets/scss/jquery-ui/studip.scss b/resources/assets/stylesheets/scss/jquery-ui/studip.scss
index 002eb1716cfc137152ba1e0ca725232581e68898..14ee9e86936afcc553d875f76324273df2c40d50 100644
--- a/resources/assets/stylesheets/scss/jquery-ui/studip.scss
+++ b/resources/assets/stylesheets/scss/jquery-ui/studip.scss
@@ -157,7 +157,7 @@ textarea.ui-resizable-handle.ui-resizable-s {
 }
 
 .ui-datepicker-header .ui-icon {
-    background-image: url(../images/vendor/jquery-ui/ui-icons_ffffff_256x240.png);
+    background-image: url(/assets/images/vendor/jquery-ui/ui-icons_ffffff_256x240.png);
     height: $icon-size-inline;
     width: $icon-size-inline;
 }
diff --git a/resources/assets/stylesheets/scss/messagebox.scss b/resources/assets/stylesheets/scss/messagebox.scss
index 336eb0da56e4611b003e1a368ab9e572a28abd0a..24567d51d66134c7c876244069d311fd32115701 100644
--- a/resources/assets/stylesheets/scss/messagebox.scss
+++ b/resources/assets/stylesheets/scss/messagebox.scss
@@ -111,7 +111,7 @@ section.contentbox {
         color: #000;
         border-color: var(--yellow);
         background-color: white;
-        background-image: url("@{image-path}/messagebox/question.png");
+        background-image: url("/assets/images/messagebox/question.png");
         background-size: 32px 32px;
         box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
 
diff --git a/resources/assets/stylesheets/scss/selects.scss b/resources/assets/stylesheets/scss/selects.scss
index 3e46dd6722865dafd1832bd5169b9430c1f4a971..2aa653a886cc3a98b6cf295becb61e9fbaa1dc87 100644
--- a/resources/assets/stylesheets/scss/selects.scss
+++ b/resources/assets/stylesheets/scss/selects.scss
@@ -40,7 +40,7 @@ select {
     }
 }
 
-@import "~select2/dist/css/select2";
+@import "select2/dist/css/select2";
 
 // The wrapper is neccessary for the validation error messages to appear
 // at the correct position
diff --git a/resources/assets/stylesheets/scss/table_of_contents.scss b/resources/assets/stylesheets/scss/table_of_contents.scss
index ef8077c43e5194a0b33ba08e222a7b261b354054..9af4baea06837f545e36a8aa31ec29ccf983c238 100644
--- a/resources/assets/stylesheets/scss/table_of_contents.scss
+++ b/resources/assets/stylesheets/scss/table_of_contents.scss
@@ -240,7 +240,7 @@ section > .toc {
 
 
 #toc-button {
-    background-image: url('#{$icon-path}blue/table-of-contents.svg');
+    background-image: url('#{$icon-path}/blue/table-of-contents.svg');
 
     height: 24px;
     width: 24px;
diff --git a/resources/assets/stylesheets/scss/vips.scss b/resources/assets/stylesheets/scss/vips.scss
index f11afda08e495d5fab24923e1e44d1fe1eed95ef..620972a9dd562ca752a4a7d8c6ddb0a5d9527a05 100644
--- a/resources/assets/stylesheets/scss/vips.scss
+++ b/resources/assets/stylesheets/scss/vips.scss
@@ -68,7 +68,7 @@ progress.assignment {
 
 .vips-teaser {
     background-color: var(--content-color-20);
-    background-image: url(../images/icons/blue/vips.svg);
+    background-image: url(/assets/images/icons/blue/vips.svg);
     background-position: 64px 50%;
     background-repeat: no-repeat;
     background-size: 120px;
diff --git a/resources/assets/stylesheets/studip-jquery-ui.scss b/resources/assets/stylesheets/studip-jquery-ui.scss
index d95af13b56deae0ad44f5f392d21d0aa262f8145..c3e47f70dcc672d94620ba445929092dcd681a35 100644
--- a/resources/assets/stylesheets/studip-jquery-ui.scss
+++ b/resources/assets/stylesheets/studip-jquery-ui.scss
@@ -5,8 +5,7 @@ $z-index: 1001;
 @import "jquery-ui.structure.css";
 @import "scss/jquery-ui/custom";
 @import "scss/jquery-ui/studip";
-@import "~jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.css";
-@import "~multiselect/css/multi-select.css";
+@import "multiselect/css/multi-select.css";
 
 // Tweaks/adjustments for multi-select
 .ms-container {
diff --git a/resources/vue/avatar-app.js b/resources/vue/avatar-app.js
index 63db01c5f991a84d1a654636053ad0baf91c398a..49c4f69b74fe3bff0b9bf9d0a0358353e468939b 100644
--- a/resources/vue/avatar-app.js
+++ b/resources/vue/avatar-app.js
@@ -1,5 +1,5 @@
 import AvatarApp from './components/avatar/AvatarApp.vue';
-import AvatarModule from './store/avatar.module';
+import AvatarModule from './store/avatar.module.js';
 import axios from 'axios';
 import { h } from 'vue';
 
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index 25246429e5a944e3e4a5183e68f4093c6f05f563..07d3a3654715dbd2fa7eb731d7d05970c06e0560 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -1,39 +1,20 @@
 import { defineAsyncComponent } from 'vue';
 
+const formInputs = import.meta.glob('./components/form_inputs/*.vue');
+const baseComponents = import.meta.glob('./components/base/*.vue');
+
+function defineComponents(imports) {
+    return Object.fromEntries(
+        Object.entries(imports).map(([path, loader]) => {
+            const name = path.split('/').pop().replace('.vue', '');
+            return [name, defineAsyncComponent(loader)];
+        }),
+    );
+}
+
 const BaseComponents = {
-    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")),
-    Multiquicksearch: defineAsyncComponent(() => import('./components/Multiquicksearch.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')),
+    ...defineComponents(formInputs),
+    ...defineComponents(baseComponents),
 };
 
 export default BaseComponents;
diff --git a/resources/vue/components/CacheAdministration.vue b/resources/vue/components/CacheAdministration.vue
index 5721d40105e5a9ee31d259d57afd89e40ba9940e..4c36e77636f6cb1e1e3104a1484f68212577443b 100644
--- a/resources/vue/components/CacheAdministration.vue
+++ b/resources/vue/components/CacheAdministration.vue
@@ -39,7 +39,7 @@
 import FileCacheConfig from './FileCacheConfig.vue'
 import MemcachedCacheConfig from './MemcachedCacheConfig.vue'
 import RedisCacheConfig from './RedisCacheConfig.vue'
-import StudipMessageBox from './StudipMessageBox.vue';
+import StudipMessageBox from '@/vue/components/base/StudipMessageBox.vue';
 
 export default {
     name: 'CacheAdministration',
diff --git a/resources/vue/components/ConsultationCreator.vue b/resources/vue/components/ConsultationCreator.vue
index 558d1bd61edd5b4a966697e28164d62517503051..6b59bcc006dedbaf8aeebf4857229f97ae6e3046 100644
--- a/resources/vue/components/ConsultationCreator.vue
+++ b/resources/vue/components/ConsultationCreator.vue
@@ -285,11 +285,11 @@
     </form>
 </template>
 <script>
-import StudipTooltipIcon from './StudipTooltipIcon.vue';
-import Datepicker from './Datepicker.vue';
+import StudipTooltipIcon from './base/StudipTooltipIcon.vue';
+import Datepicker from './base/Datepicker.vue';
 
 import moment from 'moment';
-import StudipSelect from './StudipSelect.vue';
+import StudipSelect from './base/StudipSelect.vue';
 import Timepicker from './Timepicker.vue';
 
 export default {
diff --git a/resources/vue/components/ContentBar.vue b/resources/vue/components/ContentBar.vue
index fc1c1e15c5d64b931a9844e057287b0d782c9f59..5dc73e2f3b4a36ab0a223c17271010f016bfff10 100644
--- a/resources/vue/components/ContentBar.vue
+++ b/resources/vue/components/ContentBar.vue
@@ -54,8 +54,7 @@
 import { defineComponent, PropType } from 'vue';
 
 import '../../assets/stylesheets/scss/courseware/layouts/ribbon.scss';
-import StudipIcon from './StudipIcon.vue';
-import { store } from '../../assets/javascripts/chunks/vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import { TOCItem } from './table-of-contents';
 import ContentBarTableOfContents from './ContentBarTableOfContents.vue';
 
@@ -140,11 +139,7 @@ export default defineComponent({
     },
     computed: {
         consumeMode(): boolean {
-            // We have to access the global studipStore over an import rather than
-            // using $store/mapState/mapGetters/etc.,  because this component is
-            // compatible with Courseware, and in the various Courseware apps,
-            // $store does not include the global StudipStore.
-            return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         breadcrumbFallback(): boolean {
             return window.outerWidth < 1200;
diff --git a/resources/vue/components/ContentBarTocItemList.vue b/resources/vue/components/ContentBarTocItemList.vue
index 15101c480ca224c98de0443498514ebb0dd42d45..99c42765fe84dc8933ff1fcf7b6af449bdf00a44 100644
--- a/resources/vue/components/ContentBarTocItemList.vue
+++ b/resources/vue/components/ContentBarTocItemList.vue
@@ -18,7 +18,7 @@
 import { defineComponent, PropType } from 'vue';
 
 import { TOCItem } from './table-of-contents';
-import StudipIcon from './StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default defineComponent({
     name: 'ContentBarTocItemList',
diff --git a/resources/vue/components/SearchWidget.vue b/resources/vue/components/SearchWidget.vue
index 15772168ba2c1fb601c774631f910393af6cd66f..6bec95f8f638e2f193a692cd5061dae5121376bd 100644
--- a/resources/vue/components/SearchWidget.vue
+++ b/resources/vue/components/SearchWidget.vue
@@ -26,8 +26,8 @@
 </template>
 
 <script>
-import SidebarWidget from './SidebarWidget.vue';
-import StudipIcon from './StudipIcon.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'search-widget',
diff --git a/resources/vue/components/SearchWithFilter.vue b/resources/vue/components/SearchWithFilter.vue
index 3c45ea369249e5bf8858a8b66bc36d77c535a0fa..526c9ae53afaf23205de31075aba7c8eb0d20205 100644
--- a/resources/vue/components/SearchWithFilter.vue
+++ b/resources/vue/components/SearchWithFilter.vue
@@ -50,7 +50,7 @@
 </template>
 
 <script>
-import StudipIcon from './StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 let searchIndex = 0;
 
diff --git a/resources/vue/components/StudipArticle.vue b/resources/vue/components/StudipArticle.vue
index 68dffd0aad0e9d72686d307ec36d5d6654030b2e..9719182f9c8dbfa75623ad3b429688f05f147a6f 100644
--- a/resources/vue/components/StudipArticle.vue
+++ b/resources/vue/components/StudipArticle.vue
@@ -20,7 +20,7 @@
 </template>
 
 <script>
-import StudipIcon from './StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     props: {
diff --git a/resources/vue/components/StudipContentBox.vue b/resources/vue/components/StudipContentBox.vue
index ab5bf9584c20ff267e6b0cba472940af5a0b1933..f90f219382853e96389f15c5e0a1a186c732d35e 100644
--- a/resources/vue/components/StudipContentBox.vue
+++ b/resources/vue/components/StudipContentBox.vue
@@ -23,8 +23,8 @@
 </template>
 
 <script>
-import StudipActionMenu from './StudipActionMenu.vue';
-import StudipIcon from './StudipIcon.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     components: { StudipActionMenu, StudipIcon },
diff --git a/resources/vue/components/StudipWizardDialog.vue b/resources/vue/components/StudipWizardDialog.vue
index 5c60a27fac77d72c9831bcd9a647536bde3625f1..6169ba16da6236a73a2f5a741d424840d9fb6817 100644
--- a/resources/vue/components/StudipWizardDialog.vue
+++ b/resources/vue/components/StudipWizardDialog.vue
@@ -98,8 +98,8 @@
 </template>
 
 <script>
-import StudipDialog from './StudipDialog.vue'
-import StudipIcon from './StudipIcon.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue'
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 export default {
     name: 'studip-wizard-dialog',
     emits: ['close', 'confirm'],
diff --git a/resources/vue/components/WikiEditor.vue b/resources/vue/components/WikiEditor.vue
index 8fc2e41292f8d993d4cc33d38c23372fe8ba8be5..1e9fcc16317be916ae25af076aab244b94e8a75d 100644
--- a/resources/vue/components/WikiEditor.vue
+++ b/resources/vue/components/WikiEditor.vue
@@ -79,7 +79,7 @@
 </template>
 <script>
 import WikiEditorOnlineUsers from "./WikiEditorOnlineUsers.vue";
-import StudipDateTime from "./StudipDateTime.vue";
+import StudipDateTime from "@/vue/components/base/StudipDateTime.vue";
 import JSUpdater from "@/assets/javascripts/lib/jsupdater";
 import ContentBar from "./ContentBar.vue";
 import ContentBarBreadcrumbs from "./ContentBarBreadcrumbs.vue";
diff --git a/resources/vue/components/WikiEditorOnlineUsers.vue b/resources/vue/components/WikiEditorOnlineUsers.vue
index 7bee689b83f834a7bcf4e01cd1d4048224117a86..24492f1d5dc8d0c877306e6b77d3d56781adf6d7 100644
--- a/resources/vue/components/WikiEditorOnlineUsers.vue
+++ b/resources/vue/components/WikiEditorOnlineUsers.vue
@@ -20,8 +20,8 @@
     </Teleport>
 </template>
 <script>
-import SidebarWidget from "./SidebarWidget.vue";
-import StudipIcon from "./StudipIcon.vue";
+import SidebarWidget from "@/vue/components/base/SidebarWidget.vue";
+import StudipIcon from "@/vue/components/base/StudipIcon.vue";
 
 export default {
     name: 'WikiEditorOnlineUsers',
diff --git a/resources/vue/components/admission/AdmissionRuleConfig.vue b/resources/vue/components/admission/AdmissionRuleConfig.vue
index d6d6e653f40c7b49eac6f4ea432f1869d7738df2..3104f19bfef3770b13bdbf0ad22c6e6b134a25be 100644
--- a/resources/vue/components/admission/AdmissionRuleConfig.vue
+++ b/resources/vue/components/admission/AdmissionRuleConfig.vue
@@ -94,7 +94,7 @@ export default {
     },
     created() {
         const file = STUDIP.Admission.availableRules[this.type];
-        import(`@/vue/components/admission/${file}`).then((module) => {
+        import(`@/vue/components/admission/rules/${file}.vue`).then((module) => {
             this.component = shallowRef(module.default);
             this.props = {
                 id: this.theRule?.id,
diff --git a/resources/vue/components/admission/AdmissionRuleTypeSelector.vue b/resources/vue/components/admission/AdmissionRuleTypeSelector.vue
index e8bbe8c9fe69adee5f770494dbf07ff6ff122c36..7e75fd95b097ff2d88bf3db1586a0b658c429b3a 100644
--- a/resources/vue/components/admission/AdmissionRuleTypeSelector.vue
+++ b/resources/vue/components/admission/AdmissionRuleTypeSelector.vue
@@ -66,7 +66,7 @@
 
 <script>
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-import StudipDialog from '../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 
 export default {
     name: 'AdmissionRuleTypeSelector',
diff --git a/resources/vue/components/admission/ConfigureCourseSet.vue b/resources/vue/components/admission/ConfigureCourseSet.vue
index 595f5ef2e228719f6ab6cc115d1c2f2c819ba5eb..b4abc5ed5618d5f5dc2dcbc2b661bea9bc6ce760 100644
--- a/resources/vue/components/admission/ConfigureCourseSet.vue
+++ b/resources/vue/components/admission/ConfigureCourseSet.vue
@@ -350,7 +350,7 @@
 </template>
 
 <script>
-import quicksearch from '../Quicksearch.vue';
+import quicksearch from '@/vue/components/base/Quicksearch.vue';
 import AdmissionRuleTypeSelector from './AdmissionRuleTypeSelector.vue';
 import AdmissionRuleConfig from './AdmissionRuleConfig.vue';
 import StudipProgressIndicator from "../StudipProgressIndicator.vue";
diff --git a/resources/vue/components/admission/InstantCourseSet.vue b/resources/vue/components/admission/InstantCourseSet.vue
index ec555c685bfc52ff14c4038ef952ce6be3b7ccb3..39dc7217925c2bc4a3971a23d6a96f9b3c71ddd4 100644
--- a/resources/vue/components/admission/InstantCourseSet.vue
+++ b/resources/vue/components/admission/InstantCourseSet.vue
@@ -23,7 +23,7 @@
 </template>
 
 <script>
-import AdmissionRuleConfig from './AdmissionRuleConfig';
+import AdmissionRuleConfig from './AdmissionRuleConfig.vue';
 
 export default {
     name: 'InstantCourseSet',
diff --git a/resources/vue/components/admission/ValidityTime.vue b/resources/vue/components/admission/ValidityTime.vue
index 32efd04a2397da87aca0000fa453b937029f5b1a..80f154aaa0871dd1f94149bca5a9f7c14f749df4 100644
--- a/resources/vue/components/admission/ValidityTime.vue
+++ b/resources/vue/components/admission/ValidityTime.vue
@@ -29,7 +29,7 @@
 </template>
 
 <script>
-import datetimepicker from '../Datetimepicker.vue';
+import datetimepicker from '@/vue/components/base/Datetimepicker.vue';
 
 export default {
     name: 'ValidityTime',
diff --git a/resources/vue/components/admission/ConditionalAdmission.vue b/resources/vue/components/admission/rules/ConditionalAdmission.vue
similarity index 97%
rename from resources/vue/components/admission/ConditionalAdmission.vue
rename to resources/vue/components/admission/rules/ConditionalAdmission.vue
index a0eee478c4eea329380e7d42614cf89f0cb5e210..65a9e4d79a0cd5e3b440cbe749eb94c1ea6f38b5 100644
--- a/resources/vue/components/admission/ConditionalAdmission.vue
+++ b/resources/vue/components/admission/rules/ConditionalAdmission.vue
@@ -88,9 +88,9 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import ValidityTime from "./ValidityTime.vue";
-import StudipUserFilter from "../StudipUserFilter.vue";
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import ValidityTime from "../ValidityTime.vue";
+import StudipUserFilter from "../../StudipUserFilter.vue";
 
 export default {
     name: 'ConditionalAdmission',
diff --git a/resources/vue/components/admission/CourseMemberAdmission.vue b/resources/vue/components/admission/rules/CourseMemberAdmission.vue
similarity index 95%
rename from resources/vue/components/admission/CourseMemberAdmission.vue
rename to resources/vue/components/admission/rules/CourseMemberAdmission.vue
index 3684eb35e27dc2976e70c4cf5ce4e335646d1db9..6ea8b675728e5a5de6b57e05f3d25304402ef4ca 100644
--- a/resources/vue/components/admission/CourseMemberAdmission.vue
+++ b/resources/vue/components/admission/rules/CourseMemberAdmission.vue
@@ -37,9 +37,9 @@
 </template>
 
 <script>
-import {AdmissionRuleMixin} from '../../mixins/AdmissionRuleMixin';
-import ValidityTime from './ValidityTime.vue';
-import quicksearch from '../Quicksearch.vue';
+import {AdmissionRuleMixin} from '../../../mixins/AdmissionRuleMixin';
+import ValidityTime from '../ValidityTime.vue';
+import quicksearch from '@/vue/components/base/Quicksearch.vue';
 
 export default {
     name: 'CourseMemberAdmission',
diff --git a/resources/vue/components/admission/LimitedAdmission.vue b/resources/vue/components/admission/rules/LimitedAdmission.vue
similarity index 93%
rename from resources/vue/components/admission/LimitedAdmission.vue
rename to resources/vue/components/admission/rules/LimitedAdmission.vue
index bfa57965330cafe7da2e8a3d4a04176d2bd178d7..942931e5c7555f040290edb201edac074547f653 100644
--- a/resources/vue/components/admission/LimitedAdmission.vue
+++ b/resources/vue/components/admission/rules/LimitedAdmission.vue
@@ -19,8 +19,8 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import ValidityTime from './ValidityTime.vue';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import ValidityTime from '../ValidityTime.vue';
 
 export default {
     name: 'LimitedAdmission',
diff --git a/resources/vue/components/admission/LockedAdmission.vue b/resources/vue/components/admission/rules/LockedAdmission.vue
similarity index 92%
rename from resources/vue/components/admission/LockedAdmission.vue
rename to resources/vue/components/admission/rules/LockedAdmission.vue
index 3e050bed6b7d0463eacf4870511b5da13ca24e6b..553467e5302c3c57b445b590b876029924d518ed 100644
--- a/resources/vue/components/admission/LockedAdmission.vue
+++ b/resources/vue/components/admission/rules/LockedAdmission.vue
@@ -10,7 +10,7 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
 
 export default {
     name: 'LockedAdmission',
diff --git a/resources/vue/components/admission/ParticipantRestrictedAdmission.vue b/resources/vue/components/admission/rules/ParticipantRestrictedAdmission.vue
similarity index 93%
rename from resources/vue/components/admission/ParticipantRestrictedAdmission.vue
rename to resources/vue/components/admission/rules/ParticipantRestrictedAdmission.vue
index a7468f17a3e2b7caab75621aa59872229ae874a5..971a05bb8d2a3d340d8d0fa1b25606c57ec07f6c 100644
--- a/resources/vue/components/admission/ParticipantRestrictedAdmission.vue
+++ b/resources/vue/components/admission/rules/ParticipantRestrictedAdmission.vue
@@ -23,9 +23,9 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import datetimepicker from '../Datetimepicker.vue';
-import StudipTooltipIcon from '../StudipTooltipIcon.vue';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import datetimepicker from '@/vue/components/base/Datetimepicker.vue';
+import StudipTooltipIcon from '@/vue/components/base/StudipTooltipIcon.vue';
 
 export default {
     name: 'ParticipantRestrictedAdmission',
diff --git a/resources/vue/components/admission/PasswordAdmission.vue b/resources/vue/components/admission/rules/PasswordAdmission.vue
similarity index 97%
rename from resources/vue/components/admission/PasswordAdmission.vue
rename to resources/vue/components/admission/rules/PasswordAdmission.vue
index 5d87e4d454ddd1c34e447d016ec4281c2e248414..30c3e463c728557a707ac94f22cfcdbff2126b2f 100644
--- a/resources/vue/components/admission/PasswordAdmission.vue
+++ b/resources/vue/components/admission/rules/PasswordAdmission.vue
@@ -29,7 +29,7 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
 
 export default {
     name: 'PasswordAdmission',
diff --git a/resources/vue/components/admission/PreferentialAdmission.vue b/resources/vue/components/admission/rules/PreferentialAdmission.vue
similarity index 96%
rename from resources/vue/components/admission/PreferentialAdmission.vue
rename to resources/vue/components/admission/rules/PreferentialAdmission.vue
index 76841ec57413987d09b0f0cf2bb7493d45f7e7a9..38f4e75ff5b83d9fd3e0dfc57c1ba4877c2b0e73 100644
--- a/resources/vue/components/admission/PreferentialAdmission.vue
+++ b/resources/vue/components/admission/rules/PreferentialAdmission.vue
@@ -46,8 +46,8 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import StudipUserFilter from "../StudipUserFilter.vue";
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import StudipUserFilter from "../../StudipUserFilter.vue";
 
 export default {
     name: 'PreferentialAdmission',
diff --git a/resources/vue/components/admission/TermsAdmission.vue b/resources/vue/components/admission/rules/TermsAdmission.vue
similarity index 95%
rename from resources/vue/components/admission/TermsAdmission.vue
rename to resources/vue/components/admission/rules/TermsAdmission.vue
index 98abcf8fa0b6596dd3e07fd02ba2a442f64ea7a1..2cf6733feabfbf37510354b398865a080e823c9c 100644
--- a/resources/vue/components/admission/TermsAdmission.vue
+++ b/resources/vue/components/admission/rules/TermsAdmission.vue
@@ -13,7 +13,7 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
 
 export default {
     name: 'TermsAdmission',
diff --git a/resources/vue/components/admission/TimedAdmission.vue b/resources/vue/components/admission/rules/TimedAdmission.vue
similarity index 94%
rename from resources/vue/components/admission/TimedAdmission.vue
rename to resources/vue/components/admission/rules/TimedAdmission.vue
index 2437d7ebd94150b6c96910e0a5ed457c43e5cf4d..1569e7e802a5f467c002093a5d2b53fd87c50bdc 100644
--- a/resources/vue/components/admission/TimedAdmission.vue
+++ b/resources/vue/components/admission/rules/TimedAdmission.vue
@@ -22,8 +22,8 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import datetimepicker from "../Datetimepicker.vue";
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import datetimepicker from "@/vue/components/base/Datetimepicker.vue";
 export default {
     name: 'TimedAdmission',
     components: { datetimepicker },
diff --git a/resources/vue/components/avatar/AvatarApp.vue b/resources/vue/components/avatar/AvatarApp.vue
index 8a33e380c0a01b6ac13eb448d4828706916291ae..cdae34538a46b00a5b5c5a45973f7c946b4502f9 100644
--- a/resources/vue/components/avatar/AvatarApp.vue
+++ b/resources/vue/components/avatar/AvatarApp.vue
@@ -142,8 +142,8 @@
 
 <script>
 import axios from 'axios';
-import StudipDialog from '../StudipDialog.vue';
-import StudipMessageBox from '../StudipMessageBox.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
+import StudipMessageBox from '@/vue/components/base/StudipMessageBox.vue';
 import StudipSquareButton from '../StudipSquareButton.vue';
 import StockImageSelector from '../stock-images/SelectorDialog.vue';
 import { mapGetters } from 'vuex';
diff --git a/resources/vue/components/Datepicker.vue b/resources/vue/components/base/Datepicker.vue
similarity index 98%
rename from resources/vue/components/Datepicker.vue
rename to resources/vue/components/base/Datepicker.vue
index 9aeadf12db258bfb5e0aa05334dcd69f335385e5..8ee1ca90cb790c71ac69b00f296d385fd9d952d8 100644
--- a/resources/vue/components/Datepicker.vue
+++ b/resources/vue/components/base/Datepicker.vue
@@ -10,7 +10,7 @@
 </template>
 
 <script>
-import RestrictedDatesHelper from '../../assets/javascripts/lib/RestrictedDatesHelper';
+import RestrictedDatesHelper from '@/assets/javascripts/lib/RestrictedDatesHelper';
 
 export default {
     name: 'Datepicker',
diff --git a/resources/vue/components/Datetimepicker.vue b/resources/vue/components/base/Datetimepicker.vue
similarity index 100%
rename from resources/vue/components/Datetimepicker.vue
rename to resources/vue/components/base/Datetimepicker.vue
diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/base/EditableList.vue
similarity index 100%
rename from resources/vue/components/EditableList.vue
rename to resources/vue/components/base/EditableList.vue
diff --git a/resources/vue/components/I18nTextarea.vue b/resources/vue/components/base/I18nTextarea.vue
similarity index 99%
rename from resources/vue/components/I18nTextarea.vue
rename to resources/vue/components/base/I18nTextarea.vue
index c6d102b913eed5aa39f03583632d2befcf43d368..ecfb5e1e18dc03478c8a1bc57ad0995bb25907c9 100644
--- a/resources/vue/components/I18nTextarea.vue
+++ b/resources/vue/components/base/I18nTextarea.vue
@@ -65,7 +65,7 @@
 </template>
 
 <script>
-import StudipWysiwyg from './StudipWysiwyg.vue';
+import StudipWysiwyg from '@/vue/components/base/StudipWysiwyg.vue';
 export default {
     name: 'i18n-textarea',
     components: {
diff --git a/resources/vue/components/Multiquicksearch.vue b/resources/vue/components/base/Multiquicksearch.vue
similarity index 100%
rename from resources/vue/components/Multiquicksearch.vue
rename to resources/vue/components/base/Multiquicksearch.vue
diff --git a/resources/vue/components/Multiselect.vue b/resources/vue/components/base/Multiselect.vue
similarity index 100%
rename from resources/vue/components/Multiselect.vue
rename to resources/vue/components/base/Multiselect.vue
diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/base/Quicksearch.vue
similarity index 100%
rename from resources/vue/components/Quicksearch.vue
rename to resources/vue/components/base/Quicksearch.vue
diff --git a/resources/vue/components/RangeInput.vue b/resources/vue/components/base/RangeInput.vue
similarity index 100%
rename from resources/vue/components/RangeInput.vue
rename to resources/vue/components/base/RangeInput.vue
diff --git a/resources/vue/components/SidebarWidget.vue b/resources/vue/components/base/SidebarWidget.vue
similarity index 100%
rename from resources/vue/components/SidebarWidget.vue
rename to resources/vue/components/base/SidebarWidget.vue
diff --git a/resources/vue/components/StudipActionMenu.vue b/resources/vue/components/base/StudipActionMenu.vue
similarity index 98%
rename from resources/vue/components/StudipActionMenu.vue
rename to resources/vue/components/base/StudipActionMenu.vue
index bb189f8892ba320bdc22fa29140f4118cdec669c..bfafb5eebecaeb36c4d69c82f6e9e734e7d00ade 100644
--- a/resources/vue/components/StudipActionMenu.vue
+++ b/resources/vue/components/base/StudipActionMenu.vue
@@ -74,7 +74,7 @@
 </template>
 
 <script>
-import { $gettext } from '../../assets/javascripts/lib/gettext';
+import { $gettext } from '@/assets/javascripts/lib/gettext';
 
 export default {
     name: 'studip-action-menu',
diff --git a/resources/vue/components/StudipAssetImg.vue b/resources/vue/components/base/StudipAssetImg.vue
similarity index 100%
rename from resources/vue/components/StudipAssetImg.vue
rename to resources/vue/components/base/StudipAssetImg.vue
diff --git a/resources/vue/components/StudipDateTime.vue b/resources/vue/components/base/StudipDateTime.vue
similarity index 100%
rename from resources/vue/components/StudipDateTime.vue
rename to resources/vue/components/base/StudipDateTime.vue
diff --git a/resources/vue/components/StudipDialog.vue b/resources/vue/components/base/StudipDialog.vue
similarity index 100%
rename from resources/vue/components/StudipDialog.vue
rename to resources/vue/components/base/StudipDialog.vue
diff --git a/resources/vue/components/StudipFileSize.vue b/resources/vue/components/base/StudipFileSize.vue
similarity index 100%
rename from resources/vue/components/StudipFileSize.vue
rename to resources/vue/components/base/StudipFileSize.vue
diff --git a/resources/vue/components/StudipFolderSize.vue b/resources/vue/components/base/StudipFolderSize.vue
similarity index 100%
rename from resources/vue/components/StudipFolderSize.vue
rename to resources/vue/components/base/StudipFolderSize.vue
diff --git a/resources/vue/components/StudipIcon.vue b/resources/vue/components/base/StudipIcon.vue
similarity index 100%
rename from resources/vue/components/StudipIcon.vue
rename to resources/vue/components/base/StudipIcon.vue
diff --git a/resources/vue/components/StudipMessageBox.vue b/resources/vue/components/base/StudipMessageBox.vue
similarity index 100%
rename from resources/vue/components/StudipMessageBox.vue
rename to resources/vue/components/base/StudipMessageBox.vue
diff --git a/resources/vue/components/StudipMultiPersonSearch.vue b/resources/vue/components/base/StudipMultiPersonSearch.vue
similarity index 100%
rename from resources/vue/components/StudipMultiPersonSearch.vue
rename to resources/vue/components/base/StudipMultiPersonSearch.vue
diff --git a/resources/vue/components/StudipProxiedCheckbox.vue b/resources/vue/components/base/StudipProxiedCheckbox.vue
similarity index 100%
rename from resources/vue/components/StudipProxiedCheckbox.vue
rename to resources/vue/components/base/StudipProxiedCheckbox.vue
diff --git a/resources/vue/components/StudipProxyCheckbox.vue b/resources/vue/components/base/StudipProxyCheckbox.vue
similarity index 100%
rename from resources/vue/components/StudipProxyCheckbox.vue
rename to resources/vue/components/base/StudipProxyCheckbox.vue
diff --git a/resources/vue/components/StudipSelect.vue b/resources/vue/components/base/StudipSelect.vue
similarity index 100%
rename from resources/vue/components/StudipSelect.vue
rename to resources/vue/components/base/StudipSelect.vue
diff --git a/resources/vue/components/StudipTooltipIcon.vue b/resources/vue/components/base/StudipTooltipIcon.vue
similarity index 100%
rename from resources/vue/components/StudipTooltipIcon.vue
rename to resources/vue/components/base/StudipTooltipIcon.vue
diff --git a/resources/vue/components/StudipWysiwyg.vue b/resources/vue/components/base/StudipWysiwyg.vue
similarity index 83%
rename from resources/vue/components/StudipWysiwyg.vue
rename to resources/vue/components/base/StudipWysiwyg.vue
index c7de9c7f6e55f37d5c1845ab41f54113998a29be..61c65ab4fda63f1d1d2f62c67b701d9aa865e007 100644
--- a/resources/vue/components/StudipWysiwyg.vue
+++ b/resources/vue/components/base/StudipWysiwyg.vue
@@ -1,6 +1,5 @@
 <script>
-import { ClassicEditor, BalloonEditor } from '../../assets/javascripts/chunks/wysiwyg.js';
-import {h, resolveComponent} from "vue";
+import { defineAsyncComponent, h, resolveComponent} from "vue";
 
 export default {
     name: 'studip-wysiwyg',
@@ -32,9 +31,9 @@ export default {
         editor() {
             switch (this.editorType) {
                 case 'classic':
-                    return ClassicEditor;
+                    return this.ClassicEditor;
                 case 'balloon':
-                    return BalloonEditor;
+                    return this.BalloonEditor;
             }
             throw new Error('Unknown `editorType`');
         },
@@ -62,8 +61,11 @@ export default {
             }
         }
     },
-    created() {
+    async created() {
         STUDIP.loadChunk('mathjax');
+        const { BalloonEditor, ClassicEditor } =  await STUDIP.loadChunk('wysiwyg');
+        this.BalloonEditor = BalloonEditor;
+        this.ClassicEditor = ClassicEditor;
     },
     render() {
         return h(resolveComponent('ckeditor'), {
diff --git a/resources/vue/components/blubber/SearchWidget.vue b/resources/vue/components/blubber/SearchWidget.vue
index 016f5036c91e0e8a86b49e5cb49af756f1edc764..d7e02891c9eb66ea9840f147bcd173dd825157d4 100644
--- a/resources/vue/components/blubber/SearchWidget.vue
+++ b/resources/vue/components/blubber/SearchWidget.vue
@@ -37,7 +37,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 let nextId = 0;
 
diff --git a/resources/vue/components/blubber/ThreadsWidget.vue b/resources/vue/components/blubber/ThreadsWidget.vue
index 72b1bf1a6493bc3c778a21a0dc3cc6ca023f29d6..9bf9996f3e45b9f8240787566c8215dacf4dbcf1 100644
--- a/resources/vue/components/blubber/ThreadsWidget.vue
+++ b/resources/vue/components/blubber/ThreadsWidget.vue
@@ -40,7 +40,7 @@
     </SidebarWidget>
 </template>
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     emits: ['load-more-threads', 'select-thread'],
diff --git a/resources/vue/components/courseware/CoursewareActivityItem.vue b/resources/vue/components/courseware/CoursewareActivityItem.vue
index 9520d36047f073a5a51eb00fd0d249cf6f9e0024..a762b7aef2d03edd62818e68cb5866644d0afd3c 100644
--- a/resources/vue/components/courseware/CoursewareActivityItem.vue
+++ b/resources/vue/components/courseware/CoursewareActivityItem.vue
@@ -16,7 +16,7 @@
 </template>
 
 <script>
-import StudipIcon from './../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 import { mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/CoursewareAdminTemplates.vue b/resources/vue/components/courseware/CoursewareAdminTemplates.vue
index 057aee670a4b232fd9d056f048fe66668a38d2b1..10f7300736ea10132e83d8a1f8d1b922401ad1fc 100644
--- a/resources/vue/components/courseware/CoursewareAdminTemplates.vue
+++ b/resources/vue/components/courseware/CoursewareAdminTemplates.vue
@@ -113,8 +113,8 @@
 </template>
 
 <script>
-import StudipActionMenu from '../StudipActionMenu.vue';
-import StudipDialog from './../StudipDialog.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 
 import JSZip from 'jszip';
 import { mapActions, mapGetters } from 'vuex';
diff --git a/resources/vue/components/courseware/CoursewareContentBookmarks.vue b/resources/vue/components/courseware/CoursewareContentBookmarks.vue
index 83d0927ddf0f3e4b2f69e941ff3e28cde2dfde26..95f734644ac318e0a7ef2ee9f326515046913726 100644
--- a/resources/vue/components/courseware/CoursewareContentBookmarks.vue
+++ b/resources/vue/components/courseware/CoursewareContentBookmarks.vue
@@ -37,7 +37,7 @@
 
 <script>
 import { mapActions, mapGetters } from 'vuex';
-import StudipIcon from '../StudipIcon.vue'
+import StudipIcon from '@/vue/components/base/StudipIcon.vue'
 export default {
     name: 'courseware-content-bookmarks',
     components: {
diff --git a/resources/vue/components/courseware/CoursewareContentLinks.vue b/resources/vue/components/courseware/CoursewareContentLinks.vue
index 407543940dfd7e4bc8a4e7a8ee62ea5c24a14ce9..b8df1d7994232186b51b7691d40e5fdb71ffd83a 100644
--- a/resources/vue/components/courseware/CoursewareContentLinks.vue
+++ b/resources/vue/components/courseware/CoursewareContentLinks.vue
@@ -70,9 +70,9 @@
 </template>
 
 <script>
-import StudipActionMenu from './../StudipActionMenu.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
 import { mapActions, mapGetters } from 'vuex';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'courseware-content-links',
diff --git a/resources/vue/components/courseware/CoursewareContentShared.vue b/resources/vue/components/courseware/CoursewareContentShared.vue
index 993a08bc3a945f15b12953810f95f3c185cf0ae8..b2d944b356033cdf2e24d48b059f378d2d694166 100644
--- a/resources/vue/components/courseware/CoursewareContentShared.vue
+++ b/resources/vue/components/courseware/CoursewareContentShared.vue
@@ -84,9 +84,9 @@
 <script>
 import CoursewareContentPermissions from './CoursewareContentPermissions.vue';
 import { mapActions, mapGetters } from 'vuex';
-import StudipActionMenu from './../StudipActionMenu.vue';
-import StudipDialog from '../StudipDialog.vue';
-import StudipIcon from '../StudipIcon.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'courseware-content-shared',
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue
index e664a9896cd1fb24b3f509f98f3b7bfac623dfae..b55af767a379b59625db095fe0b60b869026b17b 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue
@@ -80,7 +80,7 @@
 <script>
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
-import StudipWysiwyg from '../../StudipWysiwyg.vue';
+import StudipWysiwyg from '@/vue/components/base/StudipWysiwyg.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue
index cba078fdbac8f8e953c9723ca5f4bd609ba83a22..f20416fd609b00e77c8a9690cc2e68ac450abca4 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue
@@ -43,7 +43,7 @@
 <script>
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
-import StudipWysiwyg from '../../StudipWysiwyg.vue';
+import StudipWysiwyg from '@/vue/components/base/StudipWysiwyg.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
index 468809074f87b74b2c323ec160d902ed2a27a1ab..0ca99effc8d544e12a1e3c4ae4908821c660c36a 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
@@ -18,7 +18,7 @@
 </template>
 
 <script>
-import StudipActionMenu from '../../StudipActionMenu.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue b/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue
index 5fc28bd14635a60cf33b5afeb2c1824d5b6c4a6a..6432922965c6e798841812062a5bb9697a67013d 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue
@@ -71,7 +71,7 @@
 </template>
 
 <script>
-import StudipDialog from '../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
index 02e56ac9340ed2d6bec7cdb593d1a6993ad1488f..2651a07a086df8a29d2063a8347233d3f6c6a720 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
@@ -92,8 +92,8 @@ import CoursewareBlockDiscussion from './CoursewareBlockDiscussion.vue';
 import CoursewareBlockEdit from './CoursewareBlockEdit.vue';
 import CoursewareBlockExportOptions from './CoursewareBlockExportOptions.vue';
 import CoursewareBlockInfo from './CoursewareBlockInfo.vue';
-import StudipDialog from '../../StudipDialog.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import blockMixin from '@/vue/mixins/courseware/block.js';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue
index c0003717a13912c609dc7c4d6b81b0f7f272260b..dc376c3acafba75f6c5617b8d9f233bb2c1c8490 100644
--- a/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue
@@ -25,9 +25,11 @@
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
 import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace';
-import { ClassicEditor } from '@/assets/javascripts/chunks/wysiwyg';
+import { defineAsyncComponent } from 'vue';
 import { mapActions } from 'vuex';
 
+const ClassicEditor = defineAsyncComponent(() => STUDIP.loadChunk('wysiwyg').then(({ ClassicEditor }) => ClassicEditor));
+
 export default {
     name: 'courseware-text-block',
     mixins: [blockMixin],
diff --git a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
index b2eb62487261a88ba84eaa169e85404fed2e69d4..abaa019836484a9e287fcd1fb6d141d7b17591fb 100644
--- a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
@@ -135,7 +135,7 @@
 
 <script>
 import CoursewareContainerActions from './CoursewareContainerActions.vue';
-import StudipDialog from '../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { mapGetters, mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/containers/container-components.js b/resources/vue/components/courseware/containers/container-components.js
index c920dae34646965571e3eaae3113393c85c27f01..a32af9445b4f6fd063aabaeae3e8990b14ed7ce3 100644
--- a/resources/vue/components/courseware/containers/container-components.js
+++ b/resources/vue/components/courseware/containers/container-components.js
@@ -34,7 +34,7 @@ import CoursewareTypewriterBlock from '../blocks/CoursewareTypewriterBlock.vue';
 import CoursewareVideoBlock from '../blocks/CoursewareVideoBlock.vue';
 //layout
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
 
 const ContainerComponents = {
diff --git a/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue
index ffe3953fad5b0c7b761898d48777b10a10a80a9e..fc4300c059d9ebe1726e51d70932d96ee621b976 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue
@@ -12,7 +12,7 @@
 </template>
 
 <script>
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'courseware-call-to-action-box',
diff --git a/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
index 7e41a92f16dcc4f3a16e5cf011e40934ecc9891f..182217d57fbf6b270d172e21f1190b92db9a6f4f 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
@@ -13,7 +13,7 @@
 </template>
 
 <script>
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'courseware-collapsible-box',
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
index 5a9e98a7ba83bf3efbc12048d718e7750fb697ac..792be3830c6a5362ed20db0bdef5085b9ccc3260 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
@@ -33,7 +33,6 @@
 import ContentBar from '../../ContentBar.vue';
 import { mapActions, mapGetters } from 'vuex';
 import CoursewareRibbonToolbar from './CoursewareRibbonToolbar.vue';
-import { store } from '../../../../assets/javascripts/chunks/vue';
 import { defineComponent } from "vue";
 
 export default defineComponent({
@@ -70,13 +69,7 @@ export default defineComponent({
             showToolbar: 'showToolbar',
         }),
         consumeMode(): boolean {
-            // TODO ensure that there is only one global StudipStore / 'studip' store module
-            //  across Courseware and chunks/vue.js.
-            // Currently, the 'studip' module of the courseware store is deceivingly named.
-            // It is a completely different store than the one in chunks/vue.js.
-            // It just happens to have a module with the same name, 'studip'.
-            // So, to access the global studipStore, we have to import it and access it like this.
-            return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         strings() {
             return {
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
index 902241b92963efcfd1f7d58b630acc36e0b1884d..3d6e66216d592673a9871e1dbfa2aea34319e491 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
@@ -48,7 +48,6 @@ import CoursewareToolsContents from './CoursewareToolsContents.vue';
 import CoursewareToolsUnits from './CoursewareToolsUnits.vue';
 import { FocusTrap } from 'focus-trap-vue';
 import { mapActions, mapGetters } from 'vuex';
-import { store } from "../../../../assets/javascripts/chunks/vue";
 
 export default {
     name: 'courseware-ribbon-toolbar',
@@ -74,7 +73,7 @@ export default {
     },
     computed: {
         consumeMode() {
-          return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         ...mapGetters({
             userIsTeacher: 'userIsTeacher',
diff --git a/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
index 961fae3f51be23046052cc8b12f821aaa88e9740..9ed5f461bdacad6de1ae5080882ebdc3c386fd98 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
@@ -46,7 +46,7 @@
 
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import { mapActions, mapGetters } from 'vuex';
 import ContentBar from "../../ContentBar.vue";
 
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
index bf2af11afbe568027835b6c63a6582b6e626f0d8..0a02a98a0d947cb5d30d39ab13e5cce0152e1dca 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
@@ -475,17 +475,16 @@ import colorMixin from '@/vue/mixins/courseware/colors.js';
 import wizardMixin from '@/vue/mixins/courseware/wizard.js';
 import CoursewareCallToActionBox from '../layouts/CoursewareCallToActionBox.vue';
 import CoursewareDateInput from '../layouts/CoursewareDateInput.vue';
-import StudipDialog from '../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { FocusTrap } from 'focus-trap-vue';
 import FeedbackDialog from '../../feedback/FeedbackDialog.vue';
 import FeedbackCreateDialog from '../../feedback/FeedbackCreateDialog.vue';
 import StudipFiveStars from '../../feedback/StudipFiveStars.vue';
-import StudipMessageBox from '../../StudipMessageBox.vue';
+import StudipMessageBox from '@/vue/components/base/StudipMessageBox.vue';
 import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
 import draggable from 'vuedraggable';
 import containerMixin from '@/vue/mixins/courseware/container.js';
 import { mapActions, mapGetters } from 'vuex';
-import { store } from "../../../../assets/javascripts/chunks/vue";
 
 export default {
     name: 'courseware-structural-element',
@@ -567,7 +566,7 @@ export default {
 
     computed: {
         consumeMode() {
-            return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         ...mapGetters({
             courseware: 'courseware',
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
index 235fa5219a7212852274c68c179a80ac87765c53..c622daee0b23724ccf63eddb546960099f9fb301 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
@@ -152,7 +152,7 @@
 
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import StudipWizardDialog from '../../StudipWizardDialog.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
 import wizardMixin from '@/vue/mixins/courseware/wizard.js';
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
index 9652bfb64a2085ff10c27bce45611237d3f01747..935add5d64855a5b2a46ff1d04fbce7f225b7782 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
@@ -183,7 +183,7 @@
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import StudipWizardDialog from '../../StudipWizardDialog.vue';
 
 import { mapActions, mapGetters } from 'vuex'
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue
index fe44bee1ebd3bc9dc1198f1adbafdc4f403f2c6d..f18d6acb1a71b48b01039a802fdbaeb24eb81aab 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue
@@ -256,7 +256,7 @@
 
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import Datepicker from './../../Datepicker.vue';
+import Datepicker from '@/vue/components/base/Datepicker.vue';
 import axios from 'axios';
 import { mapActions, mapGetters } from 'vuex';
 export default {
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue
index 37a32228f5c1bef6a284925a47f0d016d44afeef..b5e4572578df30ae821bb6df99bc780ba9b6a087 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue
@@ -24,7 +24,7 @@
     </studip-dialog>
 </template>
 <script>
-import Datepicker from './../../Datepicker.vue';
+import Datepicker from '@/vue/components/base/Datepicker.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue
index 7e5ef49023506bd385c505a1f5f16435b4cbb54c..9f77685c60aa1c9e73be96f1a5d70359852d5a99 100644
--- a/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue
@@ -80,7 +80,6 @@ import CoursewareCompanionOverlay from '../layouts/CoursewareCompanionOverlay.vu
 
 import { mapActions, mapGetters } from 'vuex';
 import ContentBar from "../../ContentBar.vue";
-import { store } from "../../../../assets/javascripts/chunks/vue";
 
 export default {
     name: 'public-courseware-structural-element',
@@ -104,7 +103,7 @@ export default {
 
     computed: {
         consumeMode() {
-          return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         ...mapGetters({
             courseware: 'courseware',
diff --git a/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue b/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue
index f38d908ee4991b8d606ad23f3911a51b4739657d..75549bbf63d55362e59431f6aa8bbb10c131f91a 100644
--- a/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue
@@ -88,14 +88,13 @@
 </template>
 
 <script>
-import _ from 'lodash';
 import { mapActions, mapGetters } from 'vuex';
 import CompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import CoursewareTasksDialogDistribute from './CoursewareTasksDialogDistribute.vue';
 import PeerReviewProcessStatus from './peer-review/ProcessStatus.vue';
-import StudipActionMenu from '../../StudipActionMenu.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
 import StudipDate from '../../StudipDate.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import TaskGroupsAddSolversDialog from './TaskGroupsAddSolversDialog.vue';
 import TaskGroupsDeleteDialog from './TaskGroupsDeleteDialog.vue';
 import TaskGroupsModifyDeadlineDialog from './TaskGroupsModifyDeadlineDialog.vue';
diff --git a/resources/vue/components/courseware/tasks/CoursewareDashboardTasksList.vue b/resources/vue/components/courseware/tasks/CoursewareDashboardTasksList.vue
index 41ac8736bedf148e5489198074127b8b1bab717a..df3a316c0efb4c3de22b2c53d5539841b8add64a 100644
--- a/resources/vue/components/courseware/tasks/CoursewareDashboardTasksList.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareDashboardTasksList.vue
@@ -132,9 +132,9 @@
 </template>
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import StudipIcon from '../../StudipIcon.vue';
-import StudipActionMenu from '../../StudipActionMenu.vue';
-import StudipDialog from '../../StudipDialog.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import taskHelperMixin from '../../../mixins/courseware/task-helper.js';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/tasks/peer-review/AssessmentDialog.vue b/resources/vue/components/courseware/tasks/peer-review/AssessmentDialog.vue
index 1cac7949b71717292e38e322027b33e98412b0b3..17c82c56f5f6bc139169b972b014890e384eca29 100644
--- a/resources/vue/components/courseware/tasks/peer-review/AssessmentDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/AssessmentDialog.vue
@@ -40,7 +40,7 @@ import ResultsTypeFreetext from './assessment-types/results/Freetext.vue';
 import ResultsTypeTable from './assessment-types/results/Table.vue';
 import { getProcessStatus, ProcessStatus } from './definitions.ts';
 import CompanionBox from '../../layouts/CoursewareCompanionBox.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/tasks/peer-review/AssessmentTypeEditorDialog.vue b/resources/vue/components/courseware/tasks/peer-review/AssessmentTypeEditorDialog.vue
index 679e033014055d311d5bc202c024e3756249c170..7e28e25e5699dd9f9d0bdfa987b514a7bf34bacd 100644
--- a/resources/vue/components/courseware/tasks/peer-review/AssessmentTypeEditorDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/AssessmentTypeEditorDialog.vue
@@ -21,7 +21,7 @@
 import { mapGetters } from 'vuex';
 import EditorForm from './assessment-types/editors/EditorForm.vue';
 import EditorTable from './assessment-types/editors/EditorTable.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { ASSESSMENT_TYPES } from './process-configuration';
 
 const getConfiguration = (process) => process?.attributes?.configuration ?? {};
diff --git a/resources/vue/components/courseware/tasks/peer-review/PairingEditor.vue b/resources/vue/components/courseware/tasks/peer-review/PairingEditor.vue
index 09ff0237c66716285c6e42b405f9e635800465b5..823644dca688a6c811d2a87b202eb1eeea485a27 100644
--- a/resources/vue/components/courseware/tasks/peer-review/PairingEditor.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/PairingEditor.vue
@@ -89,9 +89,8 @@
 </template>
 
 <script>
-import _ from 'lodash';
 import { mapGetters } from 'vuex';
-import StudipIcon from '../../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     components: { StudipIcon },
diff --git a/resources/vue/components/courseware/tasks/peer-review/PairingEditorDialog.vue b/resources/vue/components/courseware/tasks/peer-review/PairingEditorDialog.vue
index 29aef1c54341ccb0e8963614094a98e582e520fb..cda5f663128fadbd1fcb6f52f89ffa7c4db865f4 100644
--- a/resources/vue/components/courseware/tasks/peer-review/PairingEditorDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/PairingEditorDialog.vue
@@ -22,7 +22,7 @@
 <script>
 import { mapGetters } from 'vuex';
 import PairingEditor from './PairingEditor.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import ProgressIndicator from '../../../StudipProgressIndicator.vue';
 
 const objId = ({ id, type }) => ({ id, type });
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessConfiguration.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessConfiguration.vue
index e973a5003a2cd56cb4aa4fd7d8a1f0c34b76c2f4..5955781ffbdd89c7643cbb04e411fe020162d25c 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessConfiguration.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessConfiguration.vue
@@ -23,7 +23,7 @@
 </template>
 
 <script>
-import { ProcessConfiguration, ASSESSMENT_TYPES } from './process-configuration';
+import { ASSESSMENT_TYPES } from './process-configuration';
 
 export default {
     props: {
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessCreateDialog.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessCreateDialog.vue
index 1615107cf832a7c296f817eaf8396567416e7f42..2980e5b298a037c04d3c9b081480eb415846dc77 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessCreateDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessCreateDialog.vue
@@ -43,8 +43,8 @@
 import AssessmentTypeEditor from './AssessmentTypeEditor.vue';
 import CompanionBox from '../../layouts/CoursewareCompanionBox.vue';
 import ProcessCreateForm from './ProcessCreateForm.vue';
-import StudipDialog from '../../../StudipDialog.vue';
-import { defaultConfiguration, ProcessConfiguration } from './process-configuration';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
+import { defaultConfiguration } from './process-configuration';
 
 export default {
     components: { AssessmentTypeEditor, CompanionBox, ProcessCreateForm, StudipDialog },
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessCreateForm.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessCreateForm.vue
index 6685baa6963f802683b6d1c7afad78d0b57acb3b..71f28a6349ec5f8384b23b055aa8e9d0af009843 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessCreateForm.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessCreateForm.vue
@@ -123,7 +123,7 @@
 <script>
 import LabelRequired from '../../../forms/LabelRequired.vue';
 import PeerReviewProcessConfiguration from './ProcessConfiguration.vue';
-import { ASSESSMENT_TYPES, CONFIGURATION_SETS, ProcessConfiguration } from './process-configuration';
+import { ASSESSMENT_TYPES, CONFIGURATION_SETS } from './process-configuration';
 
 let nextId = 0;
 
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessDurationDialog.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessDurationDialog.vue
index 46eae8f449e5651d9e913ad674559e09048c3e4d..0cd59178bdbf83892d271f6b34c1b91a1de17780 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessDurationDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessDurationDialog.vue
@@ -40,7 +40,7 @@
 import { mapGetters } from 'vuex';
 import LabelRequired from '../../../forms/LabelRequired.vue';
 import StudipDate from '../../../StudipDate.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 
 const midnight = (_date) => {
     const date = new Date(_date);
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessEditDialog.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessEditDialog.vue
index 751f95236414db90b36c9735458d0a4cf605d125..0f88dcb18618d583f4700dba43260a321bd3088f 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessEditDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessEditDialog.vue
@@ -20,9 +20,9 @@
 <script>
 import { mapGetters } from 'vuex';
 import { $gettext, $gettextInterpolate } from '../../../../../assets/javascripts/lib/gettext';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import ProcessCreateForm from './ProcessCreateForm.vue';
-import { defaultConfiguration, ProcessConfiguration } from './process-configuration';
+import { defaultConfiguration } from './process-configuration';
 
 export default {
     components: { ProcessCreateForm, StudipDialog },
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessStatus.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessStatus.vue
index 666514d8aa7bd10ef1f961ef40cc13904f43b5cc..4388d79f1e18d68eb6464f4ec07f330aacfbc4ff 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessStatus.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessStatus.vue
@@ -11,7 +11,7 @@
     </span>
 </template>
 <script>
-import StudipIcon from '../../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import { getProcessStatus, ProcessStatus } from './definitions';
 
 export default {
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessesList.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessesList.vue
index 59c80ffa769744bfbb4fb9c2994b309dda84face..f7964e6de864efb8b59188739dcc662f90ef2bdc 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessesList.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessesList.vue
@@ -72,7 +72,6 @@
 </template>
 
 <script>
-import _ from 'lodash';
 import { mapActions, mapGetters } from 'vuex';
 import CompanionBox from '../../layouts/CoursewareCompanionBox.vue';
 import ProcessStatusIcon from './ProcessStatus.vue';
diff --git a/resources/vue/components/courseware/tasks/peer-review/ResultDialog.vue b/resources/vue/components/courseware/tasks/peer-review/ResultDialog.vue
index 5dc28df04747df9deffd56995eab84f54c071c94..1186ae960c91ddfb84c98501e3484ae5ec9a54db 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ResultDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ResultDialog.vue
@@ -18,7 +18,7 @@
 import ResultForm from './assessment-types/results/Form.vue';
 import ResultFreetext from './assessment-types/results/Freetext.vue';
 import ResultTable from './assessment-types/results/Table.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorForm.vue b/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorForm.vue
index f6995b1cc1d2bf30fc58fd9cf950d0b6a0a9abf9..2138ecc549232749def61582be526a9050f30078 100644
--- a/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorForm.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorForm.vue
@@ -60,12 +60,11 @@
     </CoursewareTabs>
 </template>
 <script>
-import StudipActionMenu from '../../../../../StudipActionMenu.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
 import StudipArticle from '../../../../../StudipArticle.vue';
 import LabelRequired from '../../../../../forms/LabelRequired.vue';
 import CoursewareTab from '../../../../layouts/CoursewareTab.vue';
 import CoursewareTabs from '../../../../layouts/CoursewareTabs.vue';
-import { EditorFormCriterium, FormAssessmentPayload } from '../../process-configuration';
 
 export default {
     components: { CoursewareTab, CoursewareTabs, LabelRequired, StudipActionMenu, StudipArticle },
diff --git a/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorTable.vue b/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorTable.vue
index 96511ad4f7872956f0b7e2110ab401cad070a6e3..a30034ac8811e6f7c9a834d79a1fb39a76795e12 100644
--- a/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorTable.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorTable.vue
@@ -58,7 +58,6 @@
 import LabelRequired from '../../../../../forms/LabelRequired.vue';
 import CoursewareTab from '../../../../layouts/CoursewareTab.vue';
 import CoursewareTabs from '../../../../layouts/CoursewareTabs.vue';
-import { EditorTableCriterium, TableAssessmentPayload } from '../../process-configuration';
 
 export default {
     components: { CoursewareTab, CoursewareTabs, LabelRequired },
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
index f8f392a7a86fc99b1dccea74cd06bfcc8f330aa2..80955c8c1d5d7d3b2b6265f15290f719ba1f36e9 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
@@ -93,7 +93,7 @@ import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue';
 import containerMixin from '@/vue/mixins/courseware/container.js';
 import clipboardMixin from '@/vue/mixins/courseware/clipboard.js';
 import draggable from 'vuedraggable';
-import StudipDialog from '../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
index 36d4c566c7a62e420de3c039bed985f6b04d077f..b5c5fa6ddaf3f1c4209c29e0c1b1cce70baf796d 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
@@ -149,7 +149,7 @@
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import StockImageSelectableImageCard from '../../stock-images/SelectableImageCard.vue';
 import StockImageSelector from '../../stock-images/SelectorDialog.vue';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import StudipWizardDialog from '../../StudipWizardDialog.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
 import { mapActions, mapGetters } from 'vuex';
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
index 0d17cc1841d6050311b59fc037e35fbeb6dcd11c..56d754107c5652efed3558bc0c05d6ebaa02ddf6 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
@@ -168,7 +168,7 @@
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import StudipWizardDialog from '../../StudipWizardDialog.vue';
 
 import { mapActions, mapGetters } from 'vuex'
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue
index e105c31983781df668cc825ee4a8a0e84a85191a..d5520c8eb2e645be78ae26ff584ccf7084f156d2 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue
@@ -91,7 +91,7 @@
 import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue';
 import StockImageSelectableImageCard from '../../stock-images/SelectableImageCard.vue';
 import StockImageSelector from '../../stock-images/SelectorDialog.vue';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
 
 import { mapActions, mapGetters } from 'vuex';
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue
index 88128f3815ce873b6ffcb602ee23e8d79a16df45..e19bab50746df024c965de2f51a8b1bff2d5565c 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue
@@ -1,586 +1,586 @@
-<template>
-    <studip-dialog
-        :title="dialogTitle"
-        :confirm-text="$gettext('Speichern')"
-        confirm-class="accept"
-        :close-text="$gettext('Schließen')"
-        close-class="cancel"
-        :height="height"
-        :width="width"
-        @close="$emit('close')"
-        @confirm="storePermissions"
-    >
-        <template v-slot:dialogContent>
-            <div class="cw-permissions-form-wrapper">
-                <form class="default cw-permissions-form-radioset" @submit.prevent="">
-                    <div class="cw-radioset-wrapper" role="group" aria-labelledby="permission-type">
-                        <p class="sr-only" id="permission-type">{{ $gettext('Typ') }}</p>
-                        <div class="cw-radioset">
-                            <div class="cw-radioset-box" :class="[permissionType === 'all' ? 'selected' : '']">
-                                <input
-                                    type="radio"
-                                    id="permission-type-all"
-                                    value="all"
-                                    v-model="permissionType"
-                                    @change="updatePermissionType"
-                                />
-                                <label for="permission-type-all">
-                                    <div
-                                        class="label-icon all"
-                                        :class="[permissionType === 'all' ? 'selected' : '']"
-                                    ></div>
-                                    <div class="label-text">
-                                        <span>{{ $gettext('alle Studierenden') }}</span>
-                                    </div>
-                                </label>
-                            </div>
-                            <div class="cw-radioset-box" :class="[permissionType === 'users' ? 'selected' : '']">
-                                <input
-                                    type="radio"
-                                    id="permission-type-users"
-                                    value="users"
-                                    v-model="permissionType"
-                                    @change="updatePermissionType"
-                                />
-                                <label for="permission-type-users">
-                                    <div
-                                        class="label-icon users"
-                                        :class="[permissionType === 'users' ? 'selected' : '']"
-                                    ></div>
-                                    <div class="label-text">
-                                        <span>{{ $gettext('ausgewählte Studierende') }}</span>
-                                    </div>
-                                </label>
-                            </div>
-                            <div class="cw-radioset-box" :class="[permissionType === 'groups' ? 'selected' : '']">
-                                <input
-                                    type="radio"
-                                    id="permission-type-groups"
-                                    value="groups"
-                                    v-model="permissionType"
-                                    @change="updatePermissionType"
-                                />
-                                <label for="permission-type-groups">
-                                    <div
-                                        class="label-icon groups"
-                                        :class="[permissionType === 'groups' ? 'selected' : '']"
-                                    ></div>
-                                    <div class="label-text">
-                                        <span>{{ $gettext('Gruppen') }}</span>
-                                    </div>
-                                </label>
-                            </div>
-                        </div>
-                    </div>
-                </form>
-
-                <form class="default cw-form-selects" @submit.prevent="">
-                    <div class="cw-form-selects-row">
-                        <label>
-                            {{ $gettext('Sichtbar') }}
-                            <select v-model="visible" @change="updateVisibile">
-                                <option value="always">{{ $gettext('Immer') }}</option>
-                                <option value="period">{{ $gettext('Zeitraum') }}</option>
-                                <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option>
-                            </select>
-                        </label>
-                        <template v-if="visible === 'period'">
-                            <label>
-                                {{ $gettext('von') }}
-                                <datepicker v-model="visibleStartDate" :placeholder="$gettext('unbegrenzt')" />
-                            </label>
-                            <label>
-                                {{ $gettext('bis') }}
-                                <datepicker v-model="visibleEndDate" :placeholder="$gettext('unbegrenzt')" />
-                            </label>
-                        </template>
-                    </div>
-                    <div class="cw-form-selects-row">
-                        <label
-                            >{{ $gettext('Bearbeitbar') }}
-                            <select v-model="writable" @change="updateWritable">
-                                <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option>
-                                <option value="always">{{ $gettext('Immer') }}</option>
-                                <option value="period">{{ $gettext('Zeitraum') }}</option>
-                            </select>
-                        </label>
-                        <template v-if="writable === 'period'">
-                            <div>
-                                <label>
-                                    {{ $gettext('von') }}
-                                    <datepicker v-model="writableStartDate" :placeholder="$gettext('unbegrenzt')" />
-                                </label>
-                            </div>
-                            <div>
-                                <label>
-                                    {{ $gettext('bis') }}
-                                    <datepicker v-model="writableEndDate" :placeholder="$gettext('unbegrenzt')" />
-                                </label>
-                            </div>
-                        </template>
-                    </div>
-                </form>
-            </div>
-            <div v-if="permissionType === 'all'" class="cw-contents-overview-teaser">
-                <div class="cw-contents-overview-teaser-content">
-                    <header>{{ $gettext('Rechte und Sichtbarkeit') }}</header>
-                    <p>
-                        {{
-                            $gettext(
-                                'Hier stellen Sie für das gesamte Lernmaterial ein, welche Teilnehmenden aus Ihrer Veranstaltung alle Coursewareseiten dieses Materials sehen bzw. bearbeiten können. Falls Sie für Ihr Lehrszenario eine feinere Einstellungsmöglichkeit benötigen, können Sie direkt im Lernmaterial die „Rechte und Sichtbarkeit“ an den einzelnen Seiten einstellen.'
-                            )
-                        }}
-                    </p>
-                    <p>
-                        {{
-                            $gettext(
-                                'Entscheiden Sie sich zunächst ob „alle Studierende“ die gleichen Rechte erhalten sollen, oder ob „einzelne Studierende“ oder zuvor erstellte „Gruppen“ unterschiedliche Rechte benötigen.  Die Einstellung „einzelne Studierende“ oder „Gruppen“ bietet sich beispielsweise dann an, wenn Sie eine Courseware von einer Kleingruppe bearbeiten lassen wollen. Anschließend können Sie einstellen, in welchem Zeitraum diese Rechte gelten.'
-                            )
-                        }}
-                    </p>
-                </div>
-            </div>
-
-            <table v-if="permissionType === 'users'" class="default permission-table">
-                <caption>
-                    {{ $gettext('Studierende') }}
-                </caption>
-                <thead>
-                    <tr>
-                        <th>{{ $gettext('Name') }}</th>
-                        <th>
-                            {{ $gettext('Sichtbar') }}
-                            <input type="checkbox" v-model="visibleAll" @change="updatewritableAll" />
-                        </th>
-                        <th>
-                            {{ $gettext('Bearbeitbar') }}
-                            <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" />
-                        </th>
-                    </tr>
-                </thead>
-                <tbody>
-                    <tr v-if="autorMembers.length === 0">
-                        <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td>
-                    </tr>
-                    <tr v-for="autor in autorMembers" :key="autor.id">
-                        <td>{{ autor.formattedname }}</td>
-                        <td>
-                            <input
-                                v-if="!visibleAll"
-                                type="checkbox"
-                                :value="autor.id"
-                                v-model="visibleApprovalUsers"
-                                @change="updateUserVisible(autor)"
-                            />
-                            <studip-icon v-else shape="accept" role="info" :size="14" />
-                        </td>
-                        <td>
-                            <input
-                                v-if="!writableAll"
-                                type="checkbox"
-                                :value="autor.id"
-                                v-model="writableApprovalUsers"
-                                @change="updateUserWritable(autor)"
-                            />
-                            <studip-icon v-else shape="accept" role="info" :size="14" />
-                        </td>
-                    </tr>
-                </tbody>
-            </table>
-            <template v-if="permissionType === 'groups'">
-                <table v-if="groups.length > 0" class="default permission-table">
-                    <caption>
-                        {{ $gettext('Gruppen') }}
-                    </caption>
-                    <thead>
-                        <tr>
-                            <th>{{ $gettext('Name') }}</th>
-                            <th>
-                                {{ $gettext('Sichtbar') }}
-                                <input type="checkbox" v-model="visibleAll" :disabled="writableAll" />
-                            </th>
-                            <th>
-                                {{ $gettext('Bearbeitbar') }}
-                                <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" />
-                            </th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <tr v-if="groups.length === 0">
-                            <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td>
-                        </tr>
-                        <tr v-for="group in groups" :key="group.id">
-                            <td>{{ group.name }}</td>
-                            <td>
-                                <input
-                                    v-if="!visibleAll"
-                                    type="checkbox"
-                                    :value="group.id"
-                                    v-model="visibleApprovalGroups"
-                                    @change="updateGroupVisible(group)"
-                                />
-                                <studip-icon v-else shape="accept" role="info" :size="14" />
-                            </td>
-                            <td>
-                                <input
-                                    v-if="!writableAll"
-                                    type="checkbox"
-                                    :value="group.id"
-                                    v-model="writableApprovalGroups"
-                                    @change="updateGroupWritable(group)"
-                                />
-                                <studip-icon v-else shape="accept" role="info" :size="14" />
-                            </td>
-                        </tr>
-                    </tbody>
-                </table>
-                <courseware-companion-box
-                    v-else
-                    :msgCompanion="$gettext('Sie haben noch keine Gruppen erstellt. Mit Gruppen können Sie die Sichtbarkeits- und Bearbeitungsrechte anschließend besonders unkompliziert an Arbeitsgruppen vergeben.')"
-                    mood="pointing"
-                >
-                    <template #companionActions>
-                        <a :href="statusGroupsUrl"><button class="button">{{ $gettext('Zu den Gruppen der Veranstaltung') }}</button></a>
-                    </template>
-                </courseware-companion-box>
-            </template>
-        </template>
-    </studip-dialog>
-</template>
-<script>
-import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import Datepicker from './../../Datepicker.vue';
-import axios from 'axios';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-unit-item-dialog-permissions',
-    emits: ['close'],
-    components: {
-        CoursewareCompanionBox,
-        Datepicker,
-    },
-    props: {
-        unit: {
-            type: Object,
-            required: true,
-        },
-        unitName: {
-            type: String,
-            required: true
-        }
-    },
-    data() {
-        return {
-            permissionType: 'all',
-            visible: 'always',
-            visibleAll: false,
-            visibleApprovalUsers: [],
-            visibleApprovalGroups: [],
-            visibleStartDate: null,
-            visibleEndDate: null,
-            writable: 'never',
-            writableAll: false,
-            writableStartDate: null,
-            writableEndDate: null,
-            writableApprovalUsers: [],
-            writableApprovalGroups: [],
-            height: '680',
-            width: '870',
-            currentSemester: null,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            relatedCourseMemberships: 'course-memberships/related',
-            relatedCourseStatusGroups: 'status-groups/related',
-            relatedUser: 'users/related',
-        }),
-        users() {
-            const parent = { type: 'courses', id: this.context.id };
-            const relationship = 'memberships';
-            const memberships = this.relatedCourseMemberships({ parent, relationship });
-
-            return (
-                memberships?.map((membership) => {
-                    const parent = { type: membership.type, id: membership.id };
-                    const member = this.relatedUser({ parent, relationship: 'user' });
-
-                    return {
-                        id: member.id,
-                        formattedname: member.attributes['formatted-name'],
-                        username: member.attributes['username'],
-                        perm: membership.attributes['permission'],
-                    };
-                }) ?? []
-            );
-        },
-        statusGroupsUrl() {
-            return STUDIP.URLHelper.getURL('dispatch.php/course/statusgroups');
-        },
-        autorMembers() {
-            if (Object.keys(this.users).length === 0 && this.users.constructor === Object) {
-                return [];
-            }
-
-            let members = this.users.filter(function (user) {
-                return user.perm === 'autor';
-            });
-
-            return members;
-        },
-        groups() {
-            const parent = { type: 'courses', id: this.context.id };
-            const relationship = 'status-groups';
-            const statusGroups = this.relatedCourseStatusGroups({ parent, relationship });
-
-            return (
-                statusGroups?.map((statusGroup) => {
-                    return {
-                        id: statusGroup.id,
-                        name: statusGroup.attributes['name'],
-                    };
-                }) ?? []
-            );
-        },
-        periodsValid() {
-            if (this.writable !== 'period' || this.visible !== 'period') {
-                return true;
-            }
-            return this.visibleStartDate <= this.writableStartDate
-                && (
-                     this.visibleEndDate === null
-                     || this.visibleEndDate >= this.writableEndDate
-                );
-        },
-        semesterDates() {
-            const date = Date.now() / 1000;
-            let startDate = date;
-            let endDate = date;
-            if (this.currentSemester) {
-                startDate = new Date(this.currentSemester.attributes.start).getTime() / 1000;
-                endDate = new Date(this.currentSemester.attributes.end).getTime() / 1000;
-            }
-
-            return { start: startDate, end: endDate };
-        },
-        dialogTitle() {
-            return this.$gettext('Rechte und Sichtbarkeit') + ': ' + this.unitName;
-        }
-    },
-    methods: {
-        ...mapActions({
-            loadCourseMemberships: 'course-memberships/loadRelated',
-            loadCourseStatusGroups: 'status-groups/loadRelated',
-            updateUnit: 'courseware-units/update',
-            loadUnit: 'courseware-units/loadById',
-            companionWarning: 'companionWarning',
-        }),
-        setDimensions() {
-            this.height = Math.min((window.innerHeight * 0.8).toFixed(0), 680).toString();
-            this.width = Math.min(window.innerWidth * 0.8, 870).toFixed(0);
-        },
-        initData() {
-            this.permissionType = this.unit.attributes['permission-type'];
-            this.visible = this.unit.attributes['visible'];
-            this.visibleAll = this.unit.attributes['visible-all'];
-            this.visibleStartDate = this.unit.attributes['visible-start-date']
-                ? new Date(this.unit.attributes['visible-start-date']).getTime() / 1000
-                : null;
-            this.visibleEndDate = this.unit.attributes['visible-end-date']
-                ? new Date(this.unit.attributes['visible-end-date']).getTime() / 1000
-                : null;
-            this.writable = this.unit.attributes['writable'];
-            this.writableAll = this.unit.attributes['writable-all'];
-            this.writableStartDate = this.unit.attributes['writable-start-date']
-                ? new Date(this.unit.attributes['writable-start-date']).getTime() / 1000
-                : null;
-            this.writableEndDate = this.unit.attributes['writable-end-date']
-                ? new Date(this.unit.attributes['writable-end-date']).getTime() / 1000
-                : null;
-            if (this.permissionType === 'users') {
-                this.visibleApprovalUsers = this.unit.attributes['visible-approval'];
-                this.writableApprovalUsers = this.unit.attributes['writable-approval'];
-            }
-            if (this.permissionType === 'groups') {
-                this.visibleApprovalGroups = this.unit.attributes['visible-approval'];
-                this.writableApprovalGroups = this.unit.attributes['writable-approval'];
-            }
-
-            axios
-                .get(STUDIP.URLHelper.getURL('jsonapi.php/v1/semesters', { 'filter[current]': true }, true))
-                .then((response) => {
-                    this.currentSemester = response.data.data[0];
-                })
-                .catch(() => {
-                    this.currentSemester = null;
-                });
-        },
-        async storePermissions() {
-            let visibleApproval = [];
-            let writableApproval = [];
-            if (this.permissionType === 'users') {
-                visibleApproval = this.visibleApprovalUsers;
-                writableApproval = this.writableApprovalUsers;
-            }
-            if (this.permissionType === 'groups') {
-                visibleApproval = this.visibleApprovalGroups;
-                writableApproval = this.writableApprovalGroups;
-            }
-
-            if (this.visible === 'period' && this.visibleStartDate === null && this.visibleEndDate === null) {
-                this.visible = 'always';
-            }
-
-            if (this.writable === 'period' && this.writableStartDate === null && this.writableEndDate === null) {
-                this.visible = 'always';
-            }
-
-            if (
-                this.visible === 'period' &&
-                this.visibleStartDate !== null &&
-                this.visibleEndDate !== null &&
-                this.visibleStartDate > this.visibleEndDate
-            ) {
-                this.companionWarning({
-                    info: this.$gettext(
-                        'Das Enddatum des Sichtbarkeitszeitraums darf nicht vor dem Startdatum liegen.'
-                    ),
-                });
-                return false;
-            }
-
-            if (
-                this.writable === 'period' &&
-                this.writableStartDate !== null &&
-                this.writableEndDate !== null &&
-                this.writableStartDate > this.writableEndDate
-            ) {
-                this.companionWarning({
-                    info: this.$gettext('Das Enddatum des Bearbeitungszeitraums darf nicht vor dem Startdatum liegen.'),
-                });
-                return false;
-            }
-
-            if (!this.periodsValid) {
-                this.companionWarning({
-                    info: this.$gettext('Der Bearbeitungszeitraum muss innerhalb des Sichtbarkeitszeitraums liegen.'),
-                });
-                return false;
-            }
-
-            const unit = {
-                id: this.unit.id,
-                type: 'courseware-units',
-                attributes: {
-                    'permission-scope': 'unit',
-                    'permission-type': this.permissionType,
-                    visible: this.visible,
-                    'visible-all': this.visibleAll && this.permissionType !== 'all' ? 1 : 0,
-                    'visible-start-date':
-                        this.visible === 'period' ? new Date(this.visibleStartDate * 1000).toISOString() : null,
-                    'visible-end-date':
-                        this.visible === 'period' ? new Date(this.visibleEndDate * 1000).toISOString() : null,
-                    'visible-approval': JSON.stringify(visibleApproval),
-                    writable: this.writable,
-                    'writable-all': this.writableAll && this.permissionType !== 'all' ? 1 : 0,
-                    'writable-start-date':
-                        this.writable === 'period' ? new Date(this.writableStartDate * 1000).toISOString() : null,
-                    'writable-end-date':
-                        this.writable === 'period' ? new Date(this.writableEndDate * 1000).toISOString() : null,
-                    'writable-approval': JSON.stringify(writableApproval),
-                },
-            };
-            this.$emit('close');
-            await this.updateUnit(unit);
-            await this.loadUnit({ id: this.unit.id });
-        },
-
-        updatePermissionType() {
-            if (this.permissionType !== 'all') {
-                if (this.visible === 'never') {
-                    this.visible = 'always';
-                }
-                if (this.writable === 'never') {
-                    this.writable = 'always';
-                }
-            } else {
-                if (this.writable === 'always') {
-                    this.writable = 'never';
-                }
-            }
-        },
-        updateVisibile() {
-            if (this.visible === 'never' && this.permissionType === 'all') {
-                this.writable = 'never';
-            }
-            if (this.visible === 'period') {
-                if (this.writable === 'always') {
-                    this.writable = 'period';
-                    this.writableStartDate = this.writableStartDate ?? this.semesterDates.start;
-                    this.writableEndDate = this.writableEndDate ?? this.semesterDates.end;
-                }
-
-                this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start;
-                this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end;
-            }
-        },
-        updateWritable() {
-            if (this.writable === 'always') {
-                this.visible = 'always';
-            }
-            if (this.writable === 'period' && this.permissionType === 'all' && this.visible !== 'always') {
-                this.visible = 'period';
-                this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start;
-                this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end;
-            }
-            if (this.writable === 'period') {
-                this.writableStartDate = this.writableStartDate ?? this.semesterDates.start;
-                this.writableEndDate = this.writableEndDate ?? this.semesterDates.end;
-            }
-        },
-        updateUserWritable(user) {
-            if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) {
-                this.visibleApprovalUsers.push(user.id);
-            }
-        },
-        updateUserVisible(user) {
-            if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) {
-                this.writableApprovalUsers = this.writableApprovalUsers.filter((id) => id !== user.id);
-            }
-        },
-        updateGroupWritable(group) {
-            if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) {
-                this.visibleApprovalGroups.push(group.id);
-            }
-        },
-        updateGroupVisible(group) {
-            if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) {
-                this.writableApprovalGroups = this.writableApprovalGroups.filter((id) => id !== group.id);
-            }
-        },
-        updateVisibleAll() {
-            if (this.writableAll) {
-                this.visibleAll = true;
-            }
-        },
-        updatewritableAll() {
-            if (!this.visibleAll) {
-                this.writableAll = false;
-            }
-        },
-    },
-    mounted() {
-        this.setDimensions();
-        this.initData();
-        const parent = { type: 'courses', id: this.context.id };
-        let options = {
-            include: 'user',
-            'page[limit]': 10000,
-        };
-        this.loadCourseMemberships({ parent, relationship: 'memberships', options: options });
-        this.loadCourseStatusGroups({ parent, relationship: 'status-groups' });
-    },
-};
-</script>
+<template>
+    <studip-dialog
+        :title="dialogTitle"
+        :confirm-text="$gettext('Speichern')"
+        confirm-class="accept"
+        :close-text="$gettext('Schließen')"
+        close-class="cancel"
+        :height="height"
+        :width="width"
+        @close="$emit('close')"
+        @confirm="storePermissions"
+    >
+        <template v-slot:dialogContent>
+            <div class="cw-permissions-form-wrapper">
+                <form class="default cw-permissions-form-radioset" @submit.prevent="">
+                    <div class="cw-radioset-wrapper" role="group" aria-labelledby="permission-type">
+                        <p class="sr-only" id="permission-type">{{ $gettext('Typ') }}</p>
+                        <div class="cw-radioset">
+                            <div class="cw-radioset-box" :class="[permissionType === 'all' ? 'selected' : '']">
+                                <input
+                                    type="radio"
+                                    id="permission-type-all"
+                                    value="all"
+                                    v-model="permissionType"
+                                    @change="updatePermissionType"
+                                />
+                                <label for="permission-type-all">
+                                    <div
+                                        class="label-icon all"
+                                        :class="[permissionType === 'all' ? 'selected' : '']"
+                                    ></div>
+                                    <div class="label-text">
+                                        <span>{{ $gettext('alle Studierenden') }}</span>
+                                    </div>
+                                </label>
+                            </div>
+                            <div class="cw-radioset-box" :class="[permissionType === 'users' ? 'selected' : '']">
+                                <input
+                                    type="radio"
+                                    id="permission-type-users"
+                                    value="users"
+                                    v-model="permissionType"
+                                    @change="updatePermissionType"
+                                />
+                                <label for="permission-type-users">
+                                    <div
+                                        class="label-icon users"
+                                        :class="[permissionType === 'users' ? 'selected' : '']"
+                                    ></div>
+                                    <div class="label-text">
+                                        <span>{{ $gettext('ausgewählte Studierende') }}</span>
+                                    </div>
+                                </label>
+                            </div>
+                            <div class="cw-radioset-box" :class="[permissionType === 'groups' ? 'selected' : '']">
+                                <input
+                                    type="radio"
+                                    id="permission-type-groups"
+                                    value="groups"
+                                    v-model="permissionType"
+                                    @change="updatePermissionType"
+                                />
+                                <label for="permission-type-groups">
+                                    <div
+                                        class="label-icon groups"
+                                        :class="[permissionType === 'groups' ? 'selected' : '']"
+                                    ></div>
+                                    <div class="label-text">
+                                        <span>{{ $gettext('Gruppen') }}</span>
+                                    </div>
+                                </label>
+                            </div>
+                        </div>
+                    </div>
+                </form>
+
+                <form class="default cw-form-selects" @submit.prevent="">
+                    <div class="cw-form-selects-row">
+                        <label>
+                            {{ $gettext('Sichtbar') }}
+                            <select v-model="visible" @change="updateVisibile">
+                                <option value="always">{{ $gettext('Immer') }}</option>
+                                <option value="period">{{ $gettext('Zeitraum') }}</option>
+                                <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option>
+                            </select>
+                        </label>
+                        <template v-if="visible === 'period'">
+                            <label>
+                                {{ $gettext('von') }}
+                                <datepicker v-model="visibleStartDate" :placeholder="$gettext('unbegrenzt')" />
+                            </label>
+                            <label>
+                                {{ $gettext('bis') }}
+                                <datepicker v-model="visibleEndDate" :placeholder="$gettext('unbegrenzt')" />
+                            </label>
+                        </template>
+                    </div>
+                    <div class="cw-form-selects-row">
+                        <label
+                            >{{ $gettext('Bearbeitbar') }}
+                            <select v-model="writable" @change="updateWritable">
+                                <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option>
+                                <option value="always">{{ $gettext('Immer') }}</option>
+                                <option value="period">{{ $gettext('Zeitraum') }}</option>
+                            </select>
+                        </label>
+                        <template v-if="writable === 'period'">
+                            <div>
+                                <label>
+                                    {{ $gettext('von') }}
+                                    <datepicker v-model="writableStartDate" :placeholder="$gettext('unbegrenzt')" />
+                                </label>
+                            </div>
+                            <div>
+                                <label>
+                                    {{ $gettext('bis') }}
+                                    <datepicker v-model="writableEndDate" :placeholder="$gettext('unbegrenzt')" />
+                                </label>
+                            </div>
+                        </template>
+                    </div>
+                </form>
+            </div>
+            <div v-if="permissionType === 'all'" class="cw-contents-overview-teaser">
+                <div class="cw-contents-overview-teaser-content">
+                    <header>{{ $gettext('Rechte und Sichtbarkeit') }}</header>
+                    <p>
+                        {{
+                            $gettext(
+                                'Hier stellen Sie für das gesamte Lernmaterial ein, welche Teilnehmenden aus Ihrer Veranstaltung alle Coursewareseiten dieses Materials sehen bzw. bearbeiten können. Falls Sie für Ihr Lehrszenario eine feinere Einstellungsmöglichkeit benötigen, können Sie direkt im Lernmaterial die „Rechte und Sichtbarkeit“ an den einzelnen Seiten einstellen.'
+                            )
+                        }}
+                    </p>
+                    <p>
+                        {{
+                            $gettext(
+                                'Entscheiden Sie sich zunächst ob „alle Studierende“ die gleichen Rechte erhalten sollen, oder ob „einzelne Studierende“ oder zuvor erstellte „Gruppen“ unterschiedliche Rechte benötigen.  Die Einstellung „einzelne Studierende“ oder „Gruppen“ bietet sich beispielsweise dann an, wenn Sie eine Courseware von einer Kleingruppe bearbeiten lassen wollen. Anschließend können Sie einstellen, in welchem Zeitraum diese Rechte gelten.'
+                            )
+                        }}
+                    </p>
+                </div>
+            </div>
+
+            <table v-if="permissionType === 'users'" class="default permission-table">
+                <caption>
+                    {{ $gettext('Studierende') }}
+                </caption>
+                <thead>
+                    <tr>
+                        <th>{{ $gettext('Name') }}</th>
+                        <th>
+                            {{ $gettext('Sichtbar') }}
+                            <input type="checkbox" v-model="visibleAll" @change="updatewritableAll" />
+                        </th>
+                        <th>
+                            {{ $gettext('Bearbeitbar') }}
+                            <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" />
+                        </th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr v-if="autorMembers.length === 0">
+                        <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td>
+                    </tr>
+                    <tr v-for="autor in autorMembers" :key="autor.id">
+                        <td>{{ autor.formattedname }}</td>
+                        <td>
+                            <input
+                                v-if="!visibleAll"
+                                type="checkbox"
+                                :value="autor.id"
+                                v-model="visibleApprovalUsers"
+                                @change="updateUserVisible(autor)"
+                            />
+                            <studip-icon v-else shape="accept" role="info" :size="14" />
+                        </td>
+                        <td>
+                            <input
+                                v-if="!writableAll"
+                                type="checkbox"
+                                :value="autor.id"
+                                v-model="writableApprovalUsers"
+                                @change="updateUserWritable(autor)"
+                            />
+                            <studip-icon v-else shape="accept" role="info" :size="14" />
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+            <template v-if="permissionType === 'groups'">
+                <table v-if="groups.length > 0" class="default permission-table">
+                    <caption>
+                        {{ $gettext('Gruppen') }}
+                    </caption>
+                    <thead>
+                        <tr>
+                            <th>{{ $gettext('Name') }}</th>
+                            <th>
+                                {{ $gettext('Sichtbar') }}
+                                <input type="checkbox" v-model="visibleAll" :disabled="writableAll" />
+                            </th>
+                            <th>
+                                {{ $gettext('Bearbeitbar') }}
+                                <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" />
+                            </th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-if="groups.length === 0">
+                            <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td>
+                        </tr>
+                        <tr v-for="group in groups" :key="group.id">
+                            <td>{{ group.name }}</td>
+                            <td>
+                                <input
+                                    v-if="!visibleAll"
+                                    type="checkbox"
+                                    :value="group.id"
+                                    v-model="visibleApprovalGroups"
+                                    @change="updateGroupVisible(group)"
+                                />
+                                <studip-icon v-else shape="accept" role="info" :size="14" />
+                            </td>
+                            <td>
+                                <input
+                                    v-if="!writableAll"
+                                    type="checkbox"
+                                    :value="group.id"
+                                    v-model="writableApprovalGroups"
+                                    @change="updateGroupWritable(group)"
+                                />
+                                <studip-icon v-else shape="accept" role="info" :size="14" />
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+                <courseware-companion-box
+                    v-else
+                    :msgCompanion="$gettext('Sie haben noch keine Gruppen erstellt. Mit Gruppen können Sie die Sichtbarkeits- und Bearbeitungsrechte anschließend besonders unkompliziert an Arbeitsgruppen vergeben.')"
+                    mood="pointing"
+                >
+                    <template #companionActions>
+                        <a :href="statusGroupsUrl"><button class="button">{{ $gettext('Zu den Gruppen der Veranstaltung') }}</button></a>
+                    </template>
+                </courseware-companion-box>
+            </template>
+        </template>
+    </studip-dialog>
+</template>
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import Datepicker from '@/vue/components/base/Datepicker.vue';
+import axios from 'axios';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-unit-item-dialog-permissions',
+    emits: ['close'],
+    components: {
+        CoursewareCompanionBox,
+        Datepicker,
+    },
+    props: {
+        unit: {
+            type: Object,
+            required: true,
+        },
+        unitName: {
+            type: String,
+            required: true
+        }
+    },
+    data() {
+        return {
+            permissionType: 'all',
+            visible: 'always',
+            visibleAll: false,
+            visibleApprovalUsers: [],
+            visibleApprovalGroups: [],
+            visibleStartDate: null,
+            visibleEndDate: null,
+            writable: 'never',
+            writableAll: false,
+            writableStartDate: null,
+            writableEndDate: null,
+            writableApprovalUsers: [],
+            writableApprovalGroups: [],
+            height: '680',
+            width: '870',
+            currentSemester: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            relatedCourseMemberships: 'course-memberships/related',
+            relatedCourseStatusGroups: 'status-groups/related',
+            relatedUser: 'users/related',
+        }),
+        users() {
+            const parent = { type: 'courses', id: this.context.id };
+            const relationship = 'memberships';
+            const memberships = this.relatedCourseMemberships({ parent, relationship });
+
+            return (
+                memberships?.map((membership) => {
+                    const parent = { type: membership.type, id: membership.id };
+                    const member = this.relatedUser({ parent, relationship: 'user' });
+
+                    return {
+                        id: member.id,
+                        formattedname: member.attributes['formatted-name'],
+                        username: member.attributes['username'],
+                        perm: membership.attributes['permission'],
+                    };
+                }) ?? []
+            );
+        },
+        statusGroupsUrl() {
+            return STUDIP.URLHelper.getURL('dispatch.php/course/statusgroups');
+        },
+        autorMembers() {
+            if (Object.keys(this.users).length === 0 && this.users.constructor === Object) {
+                return [];
+            }
+
+            let members = this.users.filter(function (user) {
+                return user.perm === 'autor';
+            });
+
+            return members;
+        },
+        groups() {
+            const parent = { type: 'courses', id: this.context.id };
+            const relationship = 'status-groups';
+            const statusGroups = this.relatedCourseStatusGroups({ parent, relationship });
+
+            return (
+                statusGroups?.map((statusGroup) => {
+                    return {
+                        id: statusGroup.id,
+                        name: statusGroup.attributes['name'],
+                    };
+                }) ?? []
+            );
+        },
+        periodsValid() {
+            if (this.writable !== 'period' || this.visible !== 'period') {
+                return true;
+            }
+            return this.visibleStartDate <= this.writableStartDate
+                && (
+                     this.visibleEndDate === null
+                     || this.visibleEndDate >= this.writableEndDate
+                );
+        },
+        semesterDates() {
+            const date = Date.now() / 1000;
+            let startDate = date;
+            let endDate = date;
+            if (this.currentSemester) {
+                startDate = new Date(this.currentSemester.attributes.start).getTime() / 1000;
+                endDate = new Date(this.currentSemester.attributes.end).getTime() / 1000;
+            }
+
+            return { start: startDate, end: endDate };
+        },
+        dialogTitle() {
+            return this.$gettext('Rechte und Sichtbarkeit') + ': ' + this.unitName;
+        }
+    },
+    methods: {
+        ...mapActions({
+            loadCourseMemberships: 'course-memberships/loadRelated',
+            loadCourseStatusGroups: 'status-groups/loadRelated',
+            updateUnit: 'courseware-units/update',
+            loadUnit: 'courseware-units/loadById',
+            companionWarning: 'companionWarning',
+        }),
+        setDimensions() {
+            this.height = Math.min((window.innerHeight * 0.8).toFixed(0), 680).toString();
+            this.width = Math.min(window.innerWidth * 0.8, 870).toFixed(0);
+        },
+        initData() {
+            this.permissionType = this.unit.attributes['permission-type'];
+            this.visible = this.unit.attributes['visible'];
+            this.visibleAll = this.unit.attributes['visible-all'];
+            this.visibleStartDate = this.unit.attributes['visible-start-date']
+                ? new Date(this.unit.attributes['visible-start-date']).getTime() / 1000
+                : null;
+            this.visibleEndDate = this.unit.attributes['visible-end-date']
+                ? new Date(this.unit.attributes['visible-end-date']).getTime() / 1000
+                : null;
+            this.writable = this.unit.attributes['writable'];
+            this.writableAll = this.unit.attributes['writable-all'];
+            this.writableStartDate = this.unit.attributes['writable-start-date']
+                ? new Date(this.unit.attributes['writable-start-date']).getTime() / 1000
+                : null;
+            this.writableEndDate = this.unit.attributes['writable-end-date']
+                ? new Date(this.unit.attributes['writable-end-date']).getTime() / 1000
+                : null;
+            if (this.permissionType === 'users') {
+                this.visibleApprovalUsers = this.unit.attributes['visible-approval'];
+                this.writableApprovalUsers = this.unit.attributes['writable-approval'];
+            }
+            if (this.permissionType === 'groups') {
+                this.visibleApprovalGroups = this.unit.attributes['visible-approval'];
+                this.writableApprovalGroups = this.unit.attributes['writable-approval'];
+            }
+
+            axios
+                .get(STUDIP.URLHelper.getURL('jsonapi.php/v1/semesters', { 'filter[current]': true }, true))
+                .then((response) => {
+                    this.currentSemester = response.data.data[0];
+                })
+                .catch(() => {
+                    this.currentSemester = null;
+                });
+        },
+        async storePermissions() {
+            let visibleApproval = [];
+            let writableApproval = [];
+            if (this.permissionType === 'users') {
+                visibleApproval = this.visibleApprovalUsers;
+                writableApproval = this.writableApprovalUsers;
+            }
+            if (this.permissionType === 'groups') {
+                visibleApproval = this.visibleApprovalGroups;
+                writableApproval = this.writableApprovalGroups;
+            }
+
+            if (this.visible === 'period' && this.visibleStartDate === null && this.visibleEndDate === null) {
+                this.visible = 'always';
+            }
+
+            if (this.writable === 'period' && this.writableStartDate === null && this.writableEndDate === null) {
+                this.visible = 'always';
+            }
+
+            if (
+                this.visible === 'period' &&
+                this.visibleStartDate !== null &&
+                this.visibleEndDate !== null &&
+                this.visibleStartDate > this.visibleEndDate
+            ) {
+                this.companionWarning({
+                    info: this.$gettext(
+                        'Das Enddatum des Sichtbarkeitszeitraums darf nicht vor dem Startdatum liegen.'
+                    ),
+                });
+                return false;
+            }
+
+            if (
+                this.writable === 'period' &&
+                this.writableStartDate !== null &&
+                this.writableEndDate !== null &&
+                this.writableStartDate > this.writableEndDate
+            ) {
+                this.companionWarning({
+                    info: this.$gettext('Das Enddatum des Bearbeitungszeitraums darf nicht vor dem Startdatum liegen.'),
+                });
+                return false;
+            }
+
+            if (!this.periodsValid) {
+                this.companionWarning({
+                    info: this.$gettext('Der Bearbeitungszeitraum muss innerhalb des Sichtbarkeitszeitraums liegen.'),
+                });
+                return false;
+            }
+
+            const unit = {
+                id: this.unit.id,
+                type: 'courseware-units',
+                attributes: {
+                    'permission-scope': 'unit',
+                    'permission-type': this.permissionType,
+                    visible: this.visible,
+                    'visible-all': this.visibleAll && this.permissionType !== 'all' ? 1 : 0,
+                    'visible-start-date':
+                        this.visible === 'period' ? new Date(this.visibleStartDate * 1000).toISOString() : null,
+                    'visible-end-date':
+                        this.visible === 'period' ? new Date(this.visibleEndDate * 1000).toISOString() : null,
+                    'visible-approval': JSON.stringify(visibleApproval),
+                    writable: this.writable,
+                    'writable-all': this.writableAll && this.permissionType !== 'all' ? 1 : 0,
+                    'writable-start-date':
+                        this.writable === 'period' ? new Date(this.writableStartDate * 1000).toISOString() : null,
+                    'writable-end-date':
+                        this.writable === 'period' ? new Date(this.writableEndDate * 1000).toISOString() : null,
+                    'writable-approval': JSON.stringify(writableApproval),
+                },
+            };
+            this.$emit('close');
+            await this.updateUnit(unit);
+            await this.loadUnit({ id: this.unit.id });
+        },
+
+        updatePermissionType() {
+            if (this.permissionType !== 'all') {
+                if (this.visible === 'never') {
+                    this.visible = 'always';
+                }
+                if (this.writable === 'never') {
+                    this.writable = 'always';
+                }
+            } else {
+                if (this.writable === 'always') {
+                    this.writable = 'never';
+                }
+            }
+        },
+        updateVisibile() {
+            if (this.visible === 'never' && this.permissionType === 'all') {
+                this.writable = 'never';
+            }
+            if (this.visible === 'period') {
+                if (this.writable === 'always') {
+                    this.writable = 'period';
+                    this.writableStartDate = this.writableStartDate ?? this.semesterDates.start;
+                    this.writableEndDate = this.writableEndDate ?? this.semesterDates.end;
+                }
+
+                this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start;
+                this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end;
+            }
+        },
+        updateWritable() {
+            if (this.writable === 'always') {
+                this.visible = 'always';
+            }
+            if (this.writable === 'period' && this.permissionType === 'all' && this.visible !== 'always') {
+                this.visible = 'period';
+                this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start;
+                this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end;
+            }
+            if (this.writable === 'period') {
+                this.writableStartDate = this.writableStartDate ?? this.semesterDates.start;
+                this.writableEndDate = this.writableEndDate ?? this.semesterDates.end;
+            }
+        },
+        updateUserWritable(user) {
+            if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) {
+                this.visibleApprovalUsers.push(user.id);
+            }
+        },
+        updateUserVisible(user) {
+            if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) {
+                this.writableApprovalUsers = this.writableApprovalUsers.filter((id) => id !== user.id);
+            }
+        },
+        updateGroupWritable(group) {
+            if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) {
+                this.visibleApprovalGroups.push(group.id);
+            }
+        },
+        updateGroupVisible(group) {
+            if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) {
+                this.writableApprovalGroups = this.writableApprovalGroups.filter((id) => id !== group.id);
+            }
+        },
+        updateVisibleAll() {
+            if (this.writableAll) {
+                this.visibleAll = true;
+            }
+        },
+        updatewritableAll() {
+            if (!this.visibleAll) {
+                this.writableAll = false;
+            }
+        },
+    },
+    mounted() {
+        this.setDimensions();
+        this.initData();
+        const parent = { type: 'courses', id: this.context.id };
+        let options = {
+            include: 'user',
+            'page[limit]': 10000,
+        };
+        this.loadCourseMemberships({ parent, relationship: 'memberships', options: options });
+        this.loadCourseStatusGroups({ parent, relationship: 'status-groups' });
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
index 9d21d4cd76baf1b670f256432358abebab76bba1..da19e5207dc8f3dbee52e8111022da2fd88beac2 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
@@ -55,7 +55,7 @@
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import CoursewareUnitProgressItem from './CoursewareUnitProgressItem.vue';
 import CoursewareProgressCircle from './CoursewareProgressCircle.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 import { mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue
index 9a179c3aec33aab9c64296f4a5c2c9fa63849994..78b8d70361360b11f63928cc12fdea2cfe03ce6f 100644
--- a/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue
@@ -18,7 +18,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue
index 09ced68f6c88661c9092b5a5a2ada9761b76e9a6..1a9935437671a9a5284f6dfdd621959d84dd6c5b 100644
--- a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue
@@ -30,7 +30,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 import { mapActions } from 'vuex';
 
diff --git a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue
index dc10e17a0726cb35b36543826293bb7696b5736e..4cc9c65662dec806900185cbfda5162c2b949ce4 100644
--- a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue
@@ -18,7 +18,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue
index 5b0d5af88720da91bf17111f279a195b47588508..ecf82ff7f2e261d1603111a071afc6a5d99fb89b 100644
--- a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue
@@ -21,7 +21,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue
index 4d92294b77ada17b88a15f015c0b585afd1dfc84..f3ff836951173311195bec29ed8743f04b4cca07 100644
--- a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue
@@ -21,7 +21,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue b/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue
index b31ee20b4a024f7e10e0357dac241cf5f9c6c46a..152bff91bf1afc7ee3c53cab85a56c0bcaebc102 100644
--- a/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue
@@ -26,7 +26,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import CoursewareExport from '@/vue/mixins/courseware/export.js';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue b/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue
index c0bfd5ba9e00719848aa012ba7d286a29a871e0a..b720e960d4afe9e7ab805c69b43b1b50509771e5 100644
--- a/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue
@@ -23,7 +23,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue b/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue
index fdc239dd7e5695847893448560a4280f7316c68f..23bd06885072a023022eefc103f19c6cc5d17cb6 100644
--- a/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue
@@ -32,8 +32,8 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 import { mapActions, mapGetters } from 'vuex';
 import axios from 'axios';
diff --git a/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue
index c7232c38078f923fb29a2515b41bf058354c0ceb..7f37aa8e13c25e9b6e626f17f5702c3ba0d80e09 100644
--- a/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue
@@ -13,7 +13,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue b/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue
index fd74bf00f7f1bb3191c1b4e386fe4f3502046810..c1f16abeabb759eb786dbab08294f25bdf99cc8d 100644
--- a/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue
@@ -18,7 +18,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
index 3ba88654ed2a44a870ffa56a753f7d8a9055eab6..8c6e079b20e6c8b993f7fc1726672d170ed24dd6 100644
--- a/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
@@ -36,7 +36,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 import { mapActions } from 'vuex';
 
@@ -64,15 +64,15 @@ export default {
 
 <style scoped>
 .cw-action-widget-task-groups-add-solvers {
-    background-image: url('../images/icons/blue/add.svg');
+    background-image: url('/assets/images/icons/blue/add.svg');
     background-size: 20px;
 }
 .cw-action-widget-task-groups-deadline {
-    background-image: url('../images/icons/blue/date.svg');
+    background-image: url('/assets/images/icons/blue/date.svg');
     background-size: 20px;
 }
 .cw-action-widget-task-groups-delete {
-    background-image: url('../images/icons/blue/trash.svg');
+    background-image: url('/assets/images/icons/blue/trash.svg');
     background-size: 20px;
 }
 </style>
diff --git a/resources/vue/components/feedback/StudipFiveStars.vue b/resources/vue/components/feedback/StudipFiveStars.vue
index 1a4f40b327f3889d896f8354a7922061361e4c22..7f211f01c63ea2686a3996cbe06bf75a8e04c566 100644
--- a/resources/vue/components/feedback/StudipFiveStars.vue
+++ b/resources/vue/components/feedback/StudipFiveStars.vue
@@ -5,7 +5,7 @@
 </template>
 
 <script>
-import StudipIcon from './../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 export default {
     name: 'studip-five-stars',
     components: {
diff --git a/resources/vue/components/feedback/StudipFiveStarsInput.vue b/resources/vue/components/feedback/StudipFiveStarsInput.vue
index 73448528f55a14c5734aa8c7de8953958c8d8917..3c8ab565d21c108622a3c44e4951558ac58bc193 100644
--- a/resources/vue/components/feedback/StudipFiveStarsInput.vue
+++ b/resources/vue/components/feedback/StudipFiveStarsInput.vue
@@ -15,7 +15,7 @@
     </div>
 </template>
 <script>
-import StudipIcon from './../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 export default {
     name: 'studip-five-stars-input',
     components: {
diff --git a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
index e958a02c580df158c3c387d8066a49ce9cad540a..90daf8b7ce56929447c08eba55f80e08e7388222 100644
--- a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
+++ b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
@@ -50,7 +50,7 @@
 </template>
 
 <script>
-import StudipMessageBox from "../StudipMessageBox.vue";
+import StudipMessageBox from "@/vue/components/base/StudipMessageBox.vue";
 
 export default {
     name: "calendar-permissions-table",
diff --git a/resources/vue/components/form_inputs/DateListInput.vue b/resources/vue/components/form_inputs/DateListInput.vue
index c2ad1acfa807ac8acc9f77118c041791b1d7af7d..d0ba2ec33b70aea120f220e1794c8a54231792cb 100644
--- a/resources/vue/components/form_inputs/DateListInput.vue
+++ b/resources/vue/components/form_inputs/DateListInput.vue
@@ -21,7 +21,7 @@
 </template>
 
 <script>
-import StudipDateTime from "../StudipDateTime.vue";
+import StudipDateTime from "@/vue/components/base/StudipDateTime.vue";
 
 export default {
     name: "date-list-input",
diff --git a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
index d8431dd99993d4e49af5a846c1bca786c26f4b06..9798ad4197fa06332b65e41979490b2a8c465767 100644
--- a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
+++ b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
@@ -56,7 +56,7 @@
 </template>
 
 <script>
-import StudipMessageBox from "../StudipMessageBox.vue";
+import StudipMessageBox from "@/vue/components/base/StudipMessageBox.vue";
 
 export default {
     name: 'my-courses-coloured-table',
diff --git a/resources/vue/components/form_inputs/QuicksearchListInput.vue b/resources/vue/components/form_inputs/QuicksearchListInput.vue
index d1f1fede5341508c25c9a12cc606d2cfbf50567d..55f054d060f33b6e6eb0169f47840e6006605510 100644
--- a/resources/vue/components/form_inputs/QuicksearchListInput.vue
+++ b/resources/vue/components/form_inputs/QuicksearchListInput.vue
@@ -27,7 +27,7 @@
 </template>
 
 <script>
-import quicksearch from '../Quicksearch.vue';
+import quicksearch from '@/vue/components/base/Quicksearch.vue';
 
 export default {
     name: 'QuicksearchList',
diff --git a/resources/vue/components/massmail/MassMailMessagesList.vue b/resources/vue/components/massmail/MassMailMessagesList.vue
index 60d5cbe8d1f4e64b106ee80767ad0dc62ffb0266..d693331f414cd38d51e6e96959c0395d3a2a28c5 100644
--- a/resources/vue/components/massmail/MassMailMessagesList.vue
+++ b/resources/vue/components/massmail/MassMailMessagesList.vue
@@ -75,8 +75,8 @@
 
 <script>
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-import StudipActionMenu from '../StudipActionMenu.vue';
-import SidebarWidget from '../SidebarWidget.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     name: 'MassMailMessagesList',
diff --git a/resources/vue/components/massmail/MassMailPermissions.vue b/resources/vue/components/massmail/MassMailPermissions.vue
index b493dc5fd1b4f19639f4b36eac872abd88165ee5..1bd90c3a7dd8214689236284d3b4acb118d6dc37 100644
--- a/resources/vue/components/massmail/MassMailPermissions.vue
+++ b/resources/vue/components/massmail/MassMailPermissions.vue
@@ -52,7 +52,7 @@
 
 <script>
 import StudipProgressIndicator from "../StudipProgressIndicator.vue";
-import StudipActionMenu from "../StudipActionMenu.vue";
+import StudipActionMenu from "@/vue/components/base/StudipActionMenu.vue";
 
 export default {
     name: 'MassMailPermissions',
diff --git a/resources/vue/components/questionnaires/QuestionnaireEditor.vue b/resources/vue/components/questionnaires/QuestionnaireEditor.vue
index 6ea8d642e60781fb6b2cace8beb320d3352fb72e..ac59788e7652b7f225713a1f1278647768065268 100644
--- a/resources/vue/components/questionnaires/QuestionnaireEditor.vue
+++ b/resources/vue/components/questionnaires/QuestionnaireEditor.vue
@@ -167,9 +167,9 @@
 <script>
 import draggable from 'vuedraggable';
 import md5 from 'md5';
-import StudipIcon from '../StudipIcon.vue';
-import StudipActionMenu from '../StudipActionMenu.vue';
-import Datetimepicker from '../Datetimepicker.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import Datetimepicker from '@/vue/components/base/Datetimepicker.vue';
 import {defineAsyncComponent} from 'vue';
 
 const loadedComponents = {};
@@ -209,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] === ''
-                    ? defineAsyncComponent(() => import(`./${componentInfo[0]}.vue`))
-                    : defineAsyncComponent(() => import(/* webpackIgnore: true */componentInfo[1]));
+                    ? defineAsyncComponent(() => import(`./question-types/${componentInfo[0]}.vue`))
+                    : defineAsyncComponent(() => import(/* @vite-ignore */ componentInfo[1]));
             }
 
             return loadedComponents[componentInfo[0]];
diff --git a/resources/vue/components/questionnaires/AutomatedDataEdit.vue b/resources/vue/components/questionnaires/question-types/AutomatedDataEdit.vue
similarity index 96%
rename from resources/vue/components/questionnaires/AutomatedDataEdit.vue
rename to resources/vue/components/questionnaires/question-types/AutomatedDataEdit.vue
index f37bba909f9feda76b945ee730da8ff0f61f3320..b7ecdf72cfbdad522b176fa532e7df7679021d96 100644
--- a/resources/vue/components/questionnaires/AutomatedDataEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/AutomatedDataEdit.vue
@@ -30,7 +30,7 @@
 
 <script>
 import axios from 'axios';
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
 
 export default {
     name: 'automated-data-edit',
diff --git a/resources/vue/components/questionnaires/FreetextEdit.vue b/resources/vue/components/questionnaires/question-types/FreetextEdit.vue
similarity index 81%
rename from resources/vue/components/questionnaires/FreetextEdit.vue
rename to resources/vue/components/questionnaires/question-types/FreetextEdit.vue
index 7b0ad533e2de13675bad8f32cdd8c7c8c9e48002..f47ef4868c660171c6b028c4f7714556bb6b5214 100644
--- a/resources/vue/components/questionnaires/FreetextEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/FreetextEdit.vue
@@ -13,8 +13,8 @@
 </template>
 
 <script>
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 export default {
     extends: QuestionnaireComponent,
diff --git a/resources/vue/components/questionnaires/LikertEdit.vue b/resources/vue/components/questionnaires/question-types/LikertEdit.vue
similarity index 89%
rename from resources/vue/components/questionnaires/LikertEdit.vue
rename to resources/vue/components/questionnaires/question-types/LikertEdit.vue
index c25126c02760c83842461701417e7fe29383b8dd..6383f46942fbfb968cab37985b1bbe6d45c55b66 100644
--- a/resources/vue/components/questionnaires/LikertEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/LikertEdit.vue
@@ -41,10 +41,10 @@
 </template>
 
 <script>
-import { $gettext } from '../../../assets/javascripts/lib/gettext';
-import InputArray from "./InputArray.vue";
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { $gettext } from '../../../../assets/javascripts/lib/gettext';
+import InputArray from "../InputArray.vue";
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 // This is necesssar since $gettext does not seem to work in data() or created()
 const default_values = () => ({
diff --git a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue b/resources/vue/components/questionnaires/question-types/QuestionnaireInfoEdit.vue
similarity index 88%
rename from resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
rename to resources/vue/components/questionnaires/question-types/QuestionnaireInfoEdit.vue
index 828d24e7c392d62b945cc9c630a9ece226ae0fc3..ae547e381f7f3925d27d98f15ff77824b5ccfb8c 100644
--- a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/QuestionnaireInfoEdit.vue
@@ -14,8 +14,8 @@
 </template>
 
 <script>
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 export default {
     name: 'questionnaire-info-edit',
diff --git a/resources/vue/components/questionnaires/RangescaleEdit.vue b/resources/vue/components/questionnaires/question-types/RangescaleEdit.vue
similarity index 93%
rename from resources/vue/components/questionnaires/RangescaleEdit.vue
rename to resources/vue/components/questionnaires/question-types/RangescaleEdit.vue
index b3ff7da61ef1d905909b883d7c50a69f512cc1db..52b7975cd9e5d8b7a4c2d2f78d9d4c6e7e217c2c 100644
--- a/resources/vue/components/questionnaires/RangescaleEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/RangescaleEdit.vue
@@ -50,9 +50,9 @@
 </template>
 
 <script>
-import InputArray from './InputArray.vue';
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import InputArray from '../InputArray.vue';
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 export default {
     name: 'rangescale-edit',
diff --git a/resources/vue/components/questionnaires/VoteEdit.vue b/resources/vue/components/questionnaires/question-types/VoteEdit.vue
similarity index 86%
rename from resources/vue/components/questionnaires/VoteEdit.vue
rename to resources/vue/components/questionnaires/question-types/VoteEdit.vue
index b4855cabf912db47426a1fb6e9f1dec6f188d09b..86dae6ce0e611d369e4076d98b25771dda4ab92b 100644
--- a/resources/vue/components/questionnaires/VoteEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/VoteEdit.vue
@@ -24,9 +24,9 @@
 </template>
 
 <script>
-import InputArray from "./InputArray.vue";
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import InputArray from "../InputArray.vue";
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 export default {
     extends: QuestionnaireComponent,
diff --git a/resources/vue/components/responsive/NavigationItem.vue b/resources/vue/components/responsive/NavigationItem.vue
index 8db20f8d38092ea5612a9142aa836b2b93b9a249..1e9393e0a86bc93ff3be134db0f134a1135a494e 100644
--- a/resources/vue/components/responsive/NavigationItem.vue
+++ b/resources/vue/components/responsive/NavigationItem.vue
@@ -59,7 +59,7 @@
 
 <script lang="ts">
 import { defineComponent } from 'vue';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default defineComponent({
     name: 'NavigationItem',
diff --git a/resources/vue/components/responsive/ResponsiveContentBar.vue b/resources/vue/components/responsive/ResponsiveContentBar.vue
index b9a5d3aa35cd45015dfc7bba962b323994ead554..63938adf5a386d3b73de2f9006101111070b0aa0 100644
--- a/resources/vue/components/responsive/ResponsiveContentBar.vue
+++ b/resources/vue/components/responsive/ResponsiveContentBar.vue
@@ -25,7 +25,7 @@
 </template>
 
 <script>
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'ResponsiveContentBar',
diff --git a/resources/vue/components/responsive/ResponsiveNavigation.vue b/resources/vue/components/responsive/ResponsiveNavigation.vue
index 33b3ccbc246a3e0d7aca51d4a84da2ba8b5f94ce..81350fb51bae0099b922168b3135a76c7c8ad600 100644
--- a/resources/vue/components/responsive/ResponsiveNavigation.vue
+++ b/resources/vue/components/responsive/ResponsiveNavigation.vue
@@ -113,7 +113,7 @@
 
 <script>
 import NavigationItem from './NavigationItem.vue';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import ResponsiveContentBar from './ResponsiveContentBar.vue';
 import ResponsiveSkipLinks from './ResponsiveSkipLinks.vue';
 import { FocusTrap } from 'focus-trap-vue';
diff --git a/resources/vue/components/stock-images/ActionsWidget.vue b/resources/vue/components/stock-images/ActionsWidget.vue
index 4553249648c008f4539ed28f2a604fd4ff5cac4a..b55c5c0ba99c4c18f12f65545af545da2d758f8e 100644
--- a/resources/vue/components/stock-images/ActionsWidget.vue
+++ b/resources/vue/components/stock-images/ActionsWidget.vue
@@ -15,7 +15,7 @@
     </SidebarWidget>
 </template>
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     emits: ['initiateUpload', 'initiateZipUpload'],
diff --git a/resources/vue/components/stock-images/ColorFilterWidget.vue b/resources/vue/components/stock-images/ColorFilterWidget.vue
index d9546fed8220e7e2971c93a9d26950e4b385975c..9febd3f117653c5357cbbdb8df7a560978c8099d 100644
--- a/resources/vue/components/stock-images/ColorFilterWidget.vue
+++ b/resources/vue/components/stock-images/ColorFilterWidget.vue
@@ -22,7 +22,7 @@
 </template>
 <script>
 import { colors as selectableColors } from './colors.js';
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     emits: ['update:filters'],
diff --git a/resources/vue/components/stock-images/OrientationFilterWidget.vue b/resources/vue/components/stock-images/OrientationFilterWidget.vue
index 933b66a4b5acaa7bf3f8518846ade2dbc34aa761..bc18049ce28d7364383057ba6e5840d5f0d21f92 100644
--- a/resources/vue/components/stock-images/OrientationFilterWidget.vue
+++ b/resources/vue/components/stock-images/OrientationFilterWidget.vue
@@ -13,7 +13,7 @@
     </SidebarWidget>
 </template>
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { orientations } from './filters.js';
 
 export default {
diff --git a/resources/vue/components/stock-images/Page.vue b/resources/vue/components/stock-images/Page.vue
index b419121ca45244858994c201cf81470a510e003c..7d04db71d72005893668da890f4cc75b7897e450 100644
--- a/resources/vue/components/stock-images/Page.vue
+++ b/resources/vue/components/stock-images/Page.vue
@@ -63,7 +63,7 @@ import OrientationFilterWidget from './OrientationFilterWidget.vue';
 import SearchWidget from './SearchWidget.vue';
 import UploadDialog from './UploadDialog.vue';
 import ZipUploadDialog from './ZipUploadDialog.vue';
-import StudipMessageBox from '../StudipMessageBox.vue';
+import StudipMessageBox from '@/vue/components/base/StudipMessageBox.vue';
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
 import { searchFilterAndSortImages } from './filters.js';
 
diff --git a/resources/vue/components/stock-images/SearchWidget.vue b/resources/vue/components/stock-images/SearchWidget.vue
index 7c09973dccc0337928c8220a14dde54d8198749e..2a5c4a535ea0ea33676a6217cafa3f715f38269e 100644
--- a/resources/vue/components/stock-images/SearchWidget.vue
+++ b/resources/vue/components/stock-images/SearchWidget.vue
@@ -36,7 +36,7 @@
     </SidebarWidget>
 </template>
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     emits: ['search'],
diff --git a/resources/vue/components/stock-images/SelectableImageCard.vue b/resources/vue/components/stock-images/SelectableImageCard.vue
index e6d4c73a3d8d119a16ce3bda87b83010a6399340..0046ebe62a26f40f7e6d7070e03ea32fc5d56b94 100644
--- a/resources/vue/components/stock-images/SelectableImageCard.vue
+++ b/resources/vue/components/stock-images/SelectableImageCard.vue
@@ -43,6 +43,6 @@ export default {
     -webkit-box-orient: vertical;
 }
 .stock-images-image-card__thumbnail {
-    background-image: url(../images/checkered-background.png);
+    background-image: url(/assets/images/checkered-background.png);
 }
 </style>
diff --git a/resources/vue/components/tree/AssignLinkWidget.vue b/resources/vue/components/tree/AssignLinkWidget.vue
index 7ca99ff3242c7b898dbd9b02790c2876c47bd8c1..a54d6e82112949f9a15e5763ae42d4ec0dcef7d3 100644
--- a/resources/vue/components/tree/AssignLinkWidget.vue
+++ b/resources/vue/components/tree/AssignLinkWidget.vue
@@ -11,8 +11,8 @@
 </template>
 
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
-import StudipIcon from '../StudipIcon.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import { TreeMixin } from '../../mixins/TreeMixin';
 
 export default {
diff --git a/resources/vue/components/tree/StudipTreeNode.vue b/resources/vue/components/tree/StudipTreeNode.vue
index 78691c890a00fda5e80d46eb5331e9bbd3b408aa..02effcccf5a4ad9fef232ff5627a4957fa98a5ec 100644
--- a/resources/vue/components/tree/StudipTreeNode.vue
+++ b/resources/vue/components/tree/StudipTreeNode.vue
@@ -46,10 +46,10 @@
 
 <script>
 import { TreeMixin } from '../../mixins/TreeMixin';
-import StudipIcon from '../StudipIcon.vue';
-import StudipAssetImg from '../StudipAssetImg.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
+import StudipAssetImg from '@/vue/components/base/StudipAssetImg.vue';
 import axios from 'axios';
-import StudipTooltipIcon from '../StudipTooltipIcon.vue';
+import StudipTooltipIcon from '@/vue/components/base/StudipTooltipIcon.vue';
 
 export default {
     name: 'StudipTreeNode',
diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue
index 92015a3a6bad93d572ae585c191e2bb571b292e6..86a5eba4892e72fe0136c39eb478197c1ba705e3 100644
--- a/resources/vue/components/tree/StudipTreeTable.vue
+++ b/resources/vue/components/tree/StudipTreeTable.vue
@@ -166,7 +166,7 @@ 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";
+import StudipIcon from "@/vue/components/base/StudipIcon.vue";
 
 export default {
     name: 'StudipTreeTable',
diff --git a/resources/vue/components/tree/StudipTreeTableRows.vue b/resources/vue/components/tree/StudipTreeTableRows.vue
index e13b97d82fe4e2c08baf5ae66310f47d2ae6c9e6..4c5e1a0ba4d853325d94c891656b6f9ae6aa899a 100644
--- a/resources/vue/components/tree/StudipTreeTableRows.vue
+++ b/resources/vue/components/tree/StudipTreeTableRows.vue
@@ -44,7 +44,7 @@
     </tr>
 </template>
 <script>
-import StudipIcon from "../StudipIcon.vue";
+import StudipIcon from "@/vue/components/base/StudipIcon.vue";
 import TreeNodeCourseInfo from "./TreeNodeCourseInfo.vue";
 import {TreeMixin} from "../../mixins/TreeMixin";
 
diff --git a/resources/vue/components/tree/StudipTreeViewWidget.vue b/resources/vue/components/tree/StudipTreeViewWidget.vue
index 30e2d5c30d39dd164ac2e50fcaea2df23737fd04..e1bb6d371f21611c1de417c5fc198acb1279c35a 100644
--- a/resources/vue/components/tree/StudipTreeViewWidget.vue
+++ b/resources/vue/components/tree/StudipTreeViewWidget.vue
@@ -22,7 +22,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     name: 'StudipTreeViewWidget',
diff --git a/resources/vue/components/tree/TreeBreadcrumb.vue b/resources/vue/components/tree/TreeBreadcrumb.vue
index 00486dfd9bc9c4b4861ffdbf457e6f1b5e30f1d2..10da5ec904fac693997d0e6690eaa9bc51050e59 100644
--- a/resources/vue/components/tree/TreeBreadcrumb.vue
+++ b/resources/vue/components/tree/TreeBreadcrumb.vue
@@ -50,7 +50,7 @@
 
 <script>
 import { TreeMixin } from '../../mixins/TreeMixin';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import StudipTreeNode from './StudipTreeNode.vue';
 import axios from 'axios';
 
diff --git a/resources/vue/components/tree/TreeExportWidget.vue b/resources/vue/components/tree/TreeExportWidget.vue
index 61500f53cb88f06e635f9b84991679a506286297..c369395ec8d0a11563e918281d11c6a703425e87 100644
--- a/resources/vue/components/tree/TreeExportWidget.vue
+++ b/resources/vue/components/tree/TreeExportWidget.vue
@@ -11,8 +11,8 @@
 
 <script>
 import axios from 'axios';
-import SidebarWidget from '../SidebarWidget.vue';
-import StudipIcon from '../StudipIcon.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'TreeExportWidget',
diff --git a/resources/vue/components/tree/TreeNodeCoursePath.vue b/resources/vue/components/tree/TreeNodeCoursePath.vue
index 71f69eab0b7d0f9b6a6c454fd7b193cb2b73b4bb..1bf67096e537cfaff284da3f42554bbf1818d50b 100644
--- a/resources/vue/components/tree/TreeNodeCoursePath.vue
+++ b/resources/vue/components/tree/TreeNodeCoursePath.vue
@@ -20,7 +20,7 @@
 </template>
 <script>
 import axios from 'axios';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'TreeNodeCoursePath',
diff --git a/resources/vue/components/tree/TreeSearchResult.vue b/resources/vue/components/tree/TreeSearchResult.vue
index 8ca4794a6b8b16a3d26424f01146f7461f92b970..6efbd758fe1c64081cc5c837885fae1e56a2764e 100644
--- a/resources/vue/components/tree/TreeSearchResult.vue
+++ b/resources/vue/components/tree/TreeSearchResult.vue
@@ -66,10 +66,10 @@
 <script>
 import { TreeMixin } from '../../mixins/TreeMixin';
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import TreeNodeCoursePath from './TreeNodeCoursePath.vue';
 import TreeCourseDetails from './TreeCourseDetails.vue';
-import StudipMessageBox from "../StudipMessageBox.vue";
+import StudipMessageBox from "@/vue/components/base/StudipMessageBox.vue";
 
 export default {
     name: 'TreeSearchResult',
diff --git a/resources/vue/store/StudipStore.js b/resources/vue/store/StudipStore.js
index 7c404a4f18b9037f094b2f2d1b24c9698fccc4cb..cbf88eecad3da2f82921ed29a12c23dde4bc7f2c 100644
--- a/resources/vue/store/StudipStore.js
+++ b/resources/vue/store/StudipStore.js
@@ -1,5 +1,3 @@
-import { eventBus, store } from '../../assets/javascripts/chunks/vue';
-
 const studipStore = {
     namespaced: true,
 
@@ -14,11 +12,11 @@ const studipStore = {
             return state[key];
         },
     },
+    mutations: {
+        switchConsumeMode(state, mode) {
+            state.consumeMode = !!mode;
+        },
+    },
 };
 
-// Make the current state of "focus mode" (fullscreen) available to Vue components.
-eventBus.on('switch-focus-mode', (mode) => {
-    store.state.studip.consumeMode = mode;
-});
-
 export default studipStore;
diff --git a/templates/helpbar/helpbar.php b/templates/helpbar/helpbar.php
index 6f8efbbb1d5a65efc515f65ccaf553e16ddd6766..a98a6da71ece9753d0c03cb17c18b275f0b1d6f0 100644
--- a/templates/helpbar/helpbar.php
+++ b/templates/helpbar/helpbar.php
@@ -49,6 +49,6 @@
 </div>
 <? if (!empty($tour_data['active_tour_id'])) : ?>
     <script>
-        STUDIP.Tour.init('<?=$tour_data['active_tour_id']?>', '<?=$tour_data['active_tour_step_nr']?>')
+       STUDIP.ready(() => STUDIP.Tour.init('<?=$tour_data['active_tour_id']?>', '<?=$tour_data['active_tour_step_nr']?>'));
     </script>
 <? endif ?>
diff --git a/templates/layouts/base.php b/templates/layouts/base.php
index 26f641424fc3552ab94b9eec1d27f5a21f50f116..b65282233d94635c401b421a0c2a4fd5eabe856a 100644
--- a/templates/layouts/base.php
+++ b/templates/layouts/base.php
@@ -33,7 +33,7 @@ $lang_attr = str_replace('_', '-', $_SESSION['_language']);
         <?= htmlReady(PageLayout::getTitle() . ' - ' . Config::get()->UNI_NAME_CLEAN) ?>
     </title>
     <script>
-        CKEDITOR_BASEPATH = "<?= Assets::url('javascripts/ckeditor/') ?>";
+        window.CKEDITOR_BASEPATH = "<?= Assets::url('javascripts/ckeditor/') ?>";
         String.locale = "<?= htmlReady(strtr($_SESSION['_language'], '_', '-')) ?>";
 
         document.querySelector('html').classList.replace('no-js', 'js');
@@ -70,18 +70,45 @@ $lang_attr = str_replace('_', '-', $_SESSION['_language']);
                 'ENABLE_COURSESET_FCFS' => (bool) Config::get()->ENABLE_COURSESET_FCFS
             ]) ?>,
             jsonapi_schemas: <?= json_encode($getJsonApiSchemas()) ?>,
-        }
+        };
+
+        (() => {
+            const handlers = [];
+            let nextId = 1;
+
+            function addHandler(callback, type = false, top = false) {
+                const handler = { type, callback, handlerId: nextId++ };
+                top ? handlers.unshift(handler) : handlers.push(handler);
+                return window.STUDIP;
+            }
+
+            const ready = (callback, top = false) => addHandler(callback, false, top);
+            ready.trigger = function (type, context = document) {
+                console.debug("ready.trigger", type, context);
+                const event = { target: context, eventId: nextId++ };
+
+                handlers
+                    .filter((handler) => !handler.type || handler.type === type)
+                    .forEach((handler) => handler.callback(event));
+
+                $(document).trigger($.Event("studip-ready", event));
+            };
+
+            const domReady = (callback, top = false) => addHandler(callback, "dom", top);
+            const dialogReady = (callback, top = false) => addHandler(callback, "dialog", top);
+
+            Object.assign(window.STUDIP, { ready, domReady, dialogReady });
+        })();
     </script>
 
     <?= PageLayout::getHeadElements() ?>
 
     <script>
-        setTimeout(() => {
-            // This needs to be put in a timeout since otherwise it will not match
+        STUDIP.ready(() => {
             if (STUDIP.Responsive.isResponsive()) {
                 document.querySelector('html').classList.add('responsive-display');
             }
-        }, 0);
+        });
     </script>
 </head>
 
@@ -130,6 +157,12 @@ if (Studip\Debug\DebugBar::isActivated()) {
     echo app()->get(\DebugBar\DebugBar::class)->getJavascriptRenderer()->render();
 }
 ?>
+
+    <script type="module">
+        $(document)
+            .ready(() => STUDIP.Gettext.setLocale().then(() => STUDIP.ready.trigger('dom')))
+            .on('dialog-update', (event, data) => STUDIP.ready.trigger('dialog', data.dialog));
+    </script>
 </body>
 </html>
 <?php NotificationCenter::postNotification('PageDidRender', PageLayout::getBodyElementId());
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ea907859c1b0355fd33ca637d856061ead20f51
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,87 @@
+import vue from '@vitejs/plugin-vue';
+import path from 'path';
+import { defineConfig } from 'vite';
+import { viteStaticCopy } from 'vite-plugin-static-copy';
+// import { federation } from '@module-federation/vite';
+
+const entryFileNames = `assets/javascripts/[name].js`;
+const chunkFileNames = `assets/javascripts/[hash].chunk.js`;
+const assets = [
+    {
+        regex: /\.css$/,
+        output: 'assets/stylesheets/[name][extname]',
+    },
+    {
+        regex: /\.js$/,
+        output: 'assets/javascripts/[name][extname]',
+    },
+];
+
+function assetFileNames(info) {
+    if (info?.name) {
+        const result = assets.find((a) => a.regex.test(info.name));
+        if (result) {
+            return result.output;
+        }
+    }
+
+    return '[name][extname]';
+}
+
+export default defineConfig({
+    build: {
+        manifest: true,
+        rollupOptions: {
+            input: {
+                'studip-bootstrap': './resources/assets/javascripts/entry-bootstrap.js',
+                'studip-legacy-libs': './resources/assets/javascripts/entry-legacy-libs.js',
+                'studip-lib': './resources/assets/javascripts/entry-lib.js',
+                'studip-statusgroups': './resources/assets/javascripts/entry-statusgroups.js',
+                'studip-installer': './resources/assets/javascripts/entry-installer.js',
+                // stylesheets
+                print: './resources/assets/stylesheets/print.scss',
+                accessibility: './resources/assets/stylesheets/highcontrast.scss',
+            },
+            output: {
+                entryFileNames,
+                assetFileNames,
+                chunkFileNames,
+            },
+            // experimentalLogSideEffects: true,
+        },
+        assetsDir: './',
+        copyPublicDir: false,
+        emptyOutDir: false,
+        outDir: './public/',
+        publicDir: path.resolve(__dirname, './public'),
+        sourcemap: true,
+        target: 'es2022',
+    },
+    plugins: [
+        viteStaticCopy({
+            targets: [
+                ...[
+                    './node_modules/jquery/dist/jquery.min.js',
+                    './node_modules/jquery-ui/dist/jquery-ui.min.js',
+                    './node_modules/select2/dist/js/select2.full.min.js',
+                    './node_modules/tablesorter/dist/js/jquery.tablesorter.combined.min.js',
+                    './node_modules/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.js',
+                    './node_modules/jquery.scrollto/jquery.scrollTo.min.js',
+                    './node_modules/jquery.qrcode/jquery.qrcode.min.js',
+                    './node_modules/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js',
+                    './node_modules/lodash/lodash.min.js',
+                ].map((src) => ({ src, dest: 'assets/javascripts/' })),
+                {
+                    src: './node_modules/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.css',
+                    dest: 'assets/stylesheets/',
+                },
+            ],
+        }),
+        vue(),
+    ],
+    resolve: {
+        alias: {
+            '@': path.resolve(__dirname, './resources'),
+        },
+    },
+});