This repository has been archived on 2026-04-03. You can view files and clone it, but cannot push or open issues or pull requests.
nyx/src/components/AppSidebar.vue
Nico db10ab93fd Production-ready sidebar, toolbar, auth, and routing for loop42.de
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>
2026-04-01 16:14:24 +02:00

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>