Slim sidebar (Home, nyx, Impressum, Datenschutz, Sign-in/User), global AppToolbar for system features, /agents→/nyx rename, agent auto-select, OIDC user name extraction from id_token, theme-consistent content pages, removed DevView and old system panel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
5.1 KiB
Vue
142 lines
5.1 KiB
Vue
<template>
|
|
<aside
|
|
class="app-sidebar"
|
|
:class="{ 'is-collapsed': !isOpen }"
|
|
>
|
|
<!-- Invisible full-screen click target to close -->
|
|
<div class="sidebar-close-target" @click="collapse" />
|
|
|
|
<!-- Top: [chevron] [logo + brand] -->
|
|
<div class="sidebar-header">
|
|
<button class="sidebar-toggle-btn" @click="toggle">
|
|
<ChevronLeftIcon v-if="isOpen" class="sidebar-chevron-anim w-4 h-4" />
|
|
<ChevronRightIcon v-else class="sidebar-chevron-anim w-4 h-4" />
|
|
</button>
|
|
<RouterLink v-if="isOpen" to="/" class="sidebar-brand" title="Home" @click="collapse">
|
|
<img v-if="navLogo" :src="navLogo" class="sidebar-brand-logo" alt="Home" />
|
|
<component v-else :is="THEME_ICONS[theme]" class="sidebar-brand-icon" />
|
|
<span class="sidebar-brand-name">{{ THEME_NAMES[theme] }}</span>
|
|
</RouterLink>
|
|
</div>
|
|
|
|
<!-- Nav -->
|
|
<div class="sidebar-nav">
|
|
<RouterLink to="/" class="sidebar-link" :class="{ active: route.name === 'home' }" @click="collapse">
|
|
<HomeIcon class="w-4 h-4" />
|
|
<span>Home</span>
|
|
</RouterLink>
|
|
<button v-if="isLoggedIn" class="sidebar-link" :class="{ active: route.name === 'nyx' }" @click="goNyx">
|
|
<ChatBubbleLeftRightIcon class="w-4 h-4" />
|
|
<span>nyx</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Spacer -->
|
|
<div class="sidebar-flex-spacer" />
|
|
|
|
<!-- Panel backdrop -->
|
|
<div v-if="userMenuOpen" class="sidebar-panel-backdrop" @click="userMenuOpen = false" />
|
|
|
|
<!-- Footer: legal + user (always visible) -->
|
|
<div class="sidebar-footer">
|
|
<RouterLink to="/impressum" class="sidebar-link" :class="{ active: route.name === 'impressum' }" @click="collapse">
|
|
<DocumentTextIcon class="w-4 h-4" />
|
|
<span>Impressum</span>
|
|
</RouterLink>
|
|
<RouterLink to="/datenschutz" class="sidebar-link" :class="{ active: route.name === 'datenschutz' }" @click="collapse">
|
|
<ShieldCheckIcon class="w-4 h-4" />
|
|
<span>Datenschutz</span>
|
|
</RouterLink>
|
|
<template v-if="isLoggedIn">
|
|
<div class="sidebar-user-wrap" :class="{ open: userMenuOpen }">
|
|
<button
|
|
class="sidebar-link"
|
|
@click="toggleUserMenu"
|
|
:title="isOpen ? '' : currentUser"
|
|
>
|
|
<UserCircleIcon class="w-4 h-4" />
|
|
<span>{{ currentUser || 'Nico' }}</span>
|
|
</button>
|
|
<div v-if="userMenuOpen" class="sidebar-user-menu">
|
|
<div class="sidebar-user-menu-header">{{ currentUser }}</div>
|
|
<button class="sidebar-user-menu-item" @click="handleLogout">Logout</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<RouterLink v-else to="/login" class="sidebar-link" :title="isOpen ? '' : 'Sign in'">
|
|
<ArrowRightEndOnRectangleIcon class="w-4 h-4" />
|
|
<span>Sign in</span>
|
|
</RouterLink>
|
|
</div>
|
|
</aside>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
defineOptions({ name: 'AppSidebar' });
|
|
import { ref, computed } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import {
|
|
ChevronLeftIcon,
|
|
ChevronRightIcon,
|
|
UserCircleIcon,
|
|
ArrowRightEndOnRectangleIcon,
|
|
ChatBubbleLeftRightIcon,
|
|
DocumentTextIcon,
|
|
ShieldCheckIcon,
|
|
HomeIcon,
|
|
} from '@heroicons/vue/20/solid';
|
|
import { THEME_ICONS, THEME_LOGOS, THEME_NAMES, useTheme } from '../composables/useTheme';
|
|
import { auth } from '../store';
|
|
|
|
const emit = defineEmits<{ logout: [] }>();
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const { theme } = useTheme();
|
|
const navLogo = computed(() => THEME_LOGOS[theme.value]);
|
|
|
|
const { isLoggedIn, currentUser } = auth;
|
|
|
|
const isMobile = window.innerWidth <= 480;
|
|
const isLarge = window.innerWidth >= 1024;
|
|
const isOpen = ref(isMobile ? false : isLarge ? true : localStorage.getItem('sidebar_open') !== 'false');
|
|
const userMenuOpen = ref(false);
|
|
|
|
// Auto-collapse when crossing from lg → md
|
|
const lgQuery = window.matchMedia('(min-width: 1024px)');
|
|
lgQuery.addEventListener('change', (e) => {
|
|
if (e.matches && !isOpen.value) {
|
|
isOpen.value = true;
|
|
localStorage.setItem('sidebar_open', 'true');
|
|
} else if (!e.matches && isOpen.value) {
|
|
isOpen.value = false;
|
|
localStorage.setItem('sidebar_open', 'false');
|
|
}
|
|
});
|
|
|
|
function toggle() {
|
|
isOpen.value = !isOpen.value;
|
|
localStorage.setItem('sidebar_open', String(isOpen.value));
|
|
}
|
|
|
|
function collapse() {
|
|
if (window.innerWidth >= 1024) return;
|
|
isOpen.value = false;
|
|
localStorage.setItem('sidebar_open', 'false');
|
|
}
|
|
|
|
function goNyx() {
|
|
collapse();
|
|
router.push({ name: 'nyx' });
|
|
}
|
|
|
|
function toggleUserMenu() {
|
|
userMenuOpen.value = !userMenuOpen.value;
|
|
}
|
|
|
|
function handleLogout() {
|
|
userMenuOpen.value = false;
|
|
emit('logout');
|
|
}
|
|
</script>
|