create webapp stencil with side bar, routes and fake login
This commit is contained in:
193
cmd/cli/templates/vue/SideNavBar.vue.tmpl
Normal file
193
cmd/cli/templates/vue/SideNavBar.vue.tmpl
Normal file
@ -0,0 +1,193 @@
|
||||
<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>
|
Reference in New Issue
Block a user