Files
peach/webapp/src/components/SideNavBar.vue
2025-04-06 10:25:26 -06:00

194 lines
6.8 KiB
Vue

<template>
<!-- Mobile Toggle Button when menu is closed: shows a hamburger button at top-left -->
<button
v-if="!mobileOpen"
@click="toggleMobile"
:class="[theme.hamburgerButtonBg, 'sm:hidden fixed top-4 left-4 z-40 p-2 rounded focus:outline-none']">
<!-- Hamburger Icon -->
<span class="block w-6 h-0.5 bg-current mb-1"></span>
<span class="block w-6 h-0.5 bg-current mb-1"></span>
<span class="block w-6 h-0.5 bg-current"></span>
</button>
<!-- Sidebar container -->
<!-- In mobile view, the sidebar overlays content; in desktop (sm and up), it's static -->
<nav :class="sidebarClasses">
<!-- Desktop Header: Branding (optional) and Minimize Toggle -->
<div class="hidden sm:flex justify-between items-center p-2">
<!-- Optional Branding Slot: only shown when sidebar is expanded -->
<div v-if="!minimized && $slots.brand" class="flex-shrink-0">
<!-- Wrap branding in a container that controls size -->
<div class="w-32 h-10 overflow-hidden">
<slot name="brand"></slot>
</div>
</div>
<!-- Minimize Toggle Button -->
<button
@click="toggleMinimized"
:class="[theme.toggleButtonBg, 'p-2 rounded hover:opacity-80 focus:outline-none']">
<!-- Toggle icon: changes based on minimized state -->
<svg v-if="minimized" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 19l-7-7 7-7" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
<!-- Mobile Close Button (X icon) displayed when menu is open -->
<button
v-if="mobileOpen"
@click="toggleMobile"
:class="[theme.toggleButtonBg, 'sm:hidden absolute top-4 right-4 z-50 p-2 rounded focus:outline-none']">
<!-- X Icon -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Navigation Links -->
<ul>
<li v-for="(link, i) in links" :key="i" class="my-1">
<RouterLink
:to="link.route || '#'"
class="flex items-center p-2 transition-colors"
:class="[isActive(link) ? theme.sidebarActive : theme.sidebarHover, theme.sidebarLink]">
<span class="flex-shrink-0" :class="minimized ? 'mx-auto' : 'mr-3'">
<!-- Render icon provided in the link -->
<component :is="link.icon" class="w-6 h-6" />
</span>
<span v-if="!minimized" class="flex-1">
{{ link.title }}
</span>
</RouterLink>
<!-- Sub-links; visible only when the sidebar is not minimized -->
<ul v-if="link.subLinks && link.subLinks.length" v-show="!minimized" class="ml-8">
<li v-for="(sub, j) in link.subLinks" :key="j" class="my-1">
<RouterLink
:to="sub.route || '#'"
class="flex items-center p-2 transition-colors"
:class="[isActive(sub) ? theme.sidebarActive : theme.sidebarHover, theme.sidebarLink]">
<span v-if="sub.icon" class="flex-shrink-0 mr-3">
<component :is="sub.icon" class="w-5 h-5" />
</span>
<span>{{ sub.title }}</span>
</RouterLink>
</li>
</ul>
</li>
</ul>
</nav>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { RouterLink, useRoute } from 'vue-router';
// Define the interface for navigation links.
export interface NavLink {
title: string;
icon?: any; // Vue component, SVG, or any renderable element.
route?: string;
subLinks?: Omit<NavLink, 'subLinks'>[];
}
// Define the interface for theming the component.
export interface Theme {
// Sidebar container background and text colors.
sidebarBg?: string; // e.g. "bg-gray-800"
sidebarText?: string; // e.g. "text-white"
// Classes for links (default state)
sidebarLink?: string; // e.g. "rounded"
// Classes on hover state
sidebarHover?: string; // e.g. "hover:bg-gray-700"
// Classes for the active link
sidebarActive?: string; // e.g. "bg-gray-900"
// Toggle button (used for mobile hamburger & desktop minimize toggle)
toggleButtonBg?: string; // e.g. "bg-gray-700"
hamburgerButtonBg?: string;
}
// Accept props: links are required; theme is optional.
const props = defineProps<{
links: NavLink[];
theme?: Partial<Theme>;
}>();
// Provide default theme values.
const defaultTheme: Theme = {
sidebarBg: "bg-gray-800",
sidebarText: "text-white",
sidebarLink: "",
sidebarHover: "hover:bg-gray-700",
sidebarActive: "bg-gray-900",
toggleButtonBg: "bg-gray-700",
hamburgerButtonBg: "bg-white"
};
// Merge provided theme with defaults.
const theme = {
...defaultTheme,
...props.theme
};
// Get current route to highlight active links.
const route = useRoute();
// Local state: mobile sidebar open/closed and desktop minimized state.
const mobileOpen = ref(false);
const minimized = ref(false);
// Toggle mobile sidebar visibility.
const toggleMobile = () => {
mobileOpen.value = !mobileOpen.value;
};
// Toggle desktop minimized state.
const toggleMinimized = () => {
minimized.value = !minimized.value;
};
// Check if a given link (or any of its sub-links) is active based on the current route.
const isActive = (link: NavLink) => {
if (link.route && route.path === link.route) return true;
if (link.subLinks) {
return link.subLinks.some(sub => sub.route === route.path);
}
return false;
};
// Compute sidebar classes with a 200ms transition for both mobile sliding and desktop width changes.
const sidebarClasses = computed(() => {
let classes = `${theme.sidebarBg} ${theme.sidebarText} transition-all duration-200 overflow-y-auto shadow-lg z-30`;
// On mobile: fixed positioning to overlay content.
classes += " fixed sm:static top-0 h-screen";
// Mobile: animate slide in/out.
classes += mobileOpen.value ? " transform translate-x-0" : " transform -translate-x-full";
// On desktop, ensure sidebar is visible.
classes += " sm:translate-x-0";
// Animate width change on desktop for minimized/expanded states.
classes += minimized.value ? " w-20" : " w-64";
return classes;
});
</script>
<style scoped>
/* Styling for internal scrolling within the sidebar */
nav::-webkit-scrollbar {
width: 6px;
}
nav::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
</style>