4f5ea8c442
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
167 lines
5.6 KiB
Vue
167 lines
5.6 KiB
Vue
<script setup lang="ts">
|
|
import { computed, watch } from 'vue';
|
|
import { useLayoutStore, type MenuMode } from '@KTXC/stores/layoutStore';
|
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
|
import type { IntegrationPointType } from '@KTXC/types/integrationTypes';
|
|
import { useL10n } from '@KTXC/composables/useL10n';
|
|
import Logo from '@KTXC/layouts/logo/LogoDark.vue';
|
|
import SystemMenuGroupStatic from './LayoutSystemMenuGroupStatic.vue';
|
|
import SystemMenuGroupDynamic from './LayoutSystemMenuGroupDynamic.vue';
|
|
import SystemMenuItem from './LayoutSystemMenuItem.vue';
|
|
|
|
const layoutStore = useLayoutStore();
|
|
const integrationStore = useIntegrationStore();
|
|
const { t } = useL10n('core');
|
|
|
|
const menuPointByMode: Record<MenuMode, IntegrationPointType> = {
|
|
apps: 'app_menu',
|
|
'user-settings': 'user_settings_menu',
|
|
'admin-settings': 'admin_settings_menu',
|
|
};
|
|
|
|
// Get all entries based on current menu mode
|
|
const menuEntries = computed(() => integrationStore.getPoint(menuPointByMode[layoutStore.menuMode]));
|
|
|
|
// Only show menu modes that currently have visible items.
|
|
const menuModes = computed((): Array<{ value: MenuMode; icon: string; label: string }> => {
|
|
const modes: Array<{ value: MenuMode; icon: string; label: string }> = [
|
|
{ value: 'apps', icon: 'mdi-view-dashboard', label: t('systemMenu.apps', 'Applications') },
|
|
{ value: 'user-settings', icon: 'mdi-account-cog', label: t('systemMenu.personalSettings', 'Settings') },
|
|
{ value: 'admin-settings', icon: 'mdi-shield-crown', label: t('systemMenu.adminSettings', 'System') },
|
|
];
|
|
|
|
return modes.filter(mode => integrationStore.getPoint(menuPointByMode[mode.value]).length > 0);
|
|
});
|
|
|
|
// If the active menu becomes empty (for example, after a module is disabled),
|
|
// move to the first menu that still has content.
|
|
watch(menuModes, (availableModes) => {
|
|
if (availableModes.length > 0 && !availableModes.some(mode => mode.value === layoutStore.menuMode)) {
|
|
layoutStore.setMenuMode(availableModes[0].value);
|
|
}
|
|
}, { immediate: true });
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
export default {
|
|
methods: {
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<v-navigation-drawer
|
|
left
|
|
v-model="layoutStore.sidebarDrawer"
|
|
elevation="0"
|
|
rail-width="60"
|
|
mobile-breakpoint="lg"
|
|
app
|
|
class="leftSidebar"
|
|
:rail="layoutStore.miniSidebar"
|
|
expand-on-hover
|
|
>
|
|
<div class="pa-5">
|
|
<Logo />
|
|
</div>
|
|
<!-- ---------------------------------------------- -->
|
|
<!---Navigation -->
|
|
<!-- ---------------------------------------------- -->
|
|
<perfect-scrollbar class="scrollnavbar">
|
|
<v-list aria-busy="true" aria-label="menu list">
|
|
<!---Menu Loop -->
|
|
<template v-for="entry in menuEntries" :key="entry.id">
|
|
<!-- Group with dynamic style (collapsible) -->
|
|
<SystemMenuGroupDynamic
|
|
v-if="'items' in entry && entry.style === 'dynamic'"
|
|
class="leftPadding"
|
|
:group="entry"
|
|
/>
|
|
<!-- Group with static style (subheader) -->
|
|
<SystemMenuGroupStatic
|
|
v-else-if="'items' in entry"
|
|
:group="entry"
|
|
/>
|
|
<!-- Single item (no group) -->
|
|
<SystemMenuItem
|
|
v-else
|
|
:item="entry"
|
|
/>
|
|
</template>
|
|
</v-list>
|
|
</perfect-scrollbar>
|
|
|
|
<!-- Menu Mode Switcher -->
|
|
<template v-slot:append>
|
|
<div v-if="menuModes.length">
|
|
<v-divider />
|
|
<div class="menu-mode-switcher d-flex justify-space-around align-center py-2">
|
|
<v-tooltip
|
|
v-for="mode in menuModes"
|
|
:key="mode.value"
|
|
location="right"
|
|
>
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn
|
|
v-bind="props"
|
|
icon
|
|
variant="text"
|
|
density="comfortable"
|
|
:color="layoutStore.menuMode === mode.value ? 'primary' : undefined"
|
|
:class="['menu-mode-btn', { 'menu-mode-btn--active': layoutStore.menuMode === mode.value }]"
|
|
@click="layoutStore.setMenuMode(mode.value)"
|
|
>
|
|
<v-icon>{{ mode.icon }}</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<span>{{ mode.label }}</span>
|
|
</v-tooltip>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</v-navigation-drawer>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.menu-mode-btn {
|
|
opacity: 0.6;
|
|
transition: opacity 0.2s ease, background-color 0.2s ease;
|
|
}
|
|
|
|
.menu-mode-btn:hover {
|
|
opacity: 1;
|
|
background-color: rgba(var(--v-theme-on-surface), 0.06);
|
|
}
|
|
|
|
.menu-mode-btn--active {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Collapsed rail: only the active mode icon fits without crowding */
|
|
.leftSidebar.v-navigation-drawer--rail:not(.v-navigation-drawer--is-hovering) .menu-mode-switcher {
|
|
justify-content: center;
|
|
}
|
|
|
|
.leftSidebar.v-navigation-drawer--rail:not(.v-navigation-drawer--is-hovering) .menu-mode-btn:not(.menu-mode-btn--active) {
|
|
display: none;
|
|
}
|
|
|
|
/* Sub-menu items: Vuetify's default group indent (prepend width + indent
|
|
size) stacks up to ~72px; bring it back down closer to top-level items */
|
|
.scrollnavbar :deep(.v-list-group__items .v-list-item) {
|
|
padding-inline-start: 40px;
|
|
}
|
|
|
|
/* Collapsed rail (not hovering): sub-items have no room to show icon or
|
|
text, so they render as an empty highlighted bar when active. Hide the
|
|
expanded group entirely and highlight the parent icon instead. */
|
|
.leftSidebar.v-navigation-drawer--rail:not(.v-navigation-drawer--is-hovering) :deep(.v-list-group__items) {
|
|
display: none;
|
|
}
|
|
|
|
.leftSidebar.v-navigation-drawer--rail:not(.v-navigation-drawer--is-hovering)
|
|
:deep(.v-list-group:has(.v-list-item--active) > .v-list-item.v-list-group__header) {
|
|
color: rgb(var(--v-theme-primary));
|
|
}
|
|
</style>
|