111 lines
2.6 KiB
Vue
111 lines
2.6 KiB
Vue
|
|
<template>
|
||
|
|
<NuxtLink
|
||
|
|
:to="to"
|
||
|
|
:class="[
|
||
|
|
'flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group relative',
|
||
|
|
isActive
|
||
|
|
? 'bg-glass-monaco text-monaco-600 font-medium shadow-soft'
|
||
|
|
: 'text-gray-700 hover:bg-glass-monaco-soft hover:text-monaco-600 hover:translate-x-0.5'
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
<!-- Active Indicator -->
|
||
|
|
<div
|
||
|
|
v-if="isActive"
|
||
|
|
class="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-gradient-monaco rounded-r-full"
|
||
|
|
></div>
|
||
|
|
|
||
|
|
<!-- Icon -->
|
||
|
|
<component
|
||
|
|
:is="icon"
|
||
|
|
:class="[
|
||
|
|
'flex-shrink-0 transition-colors',
|
||
|
|
collapsed ? 'w-6 h-6' : 'w-5 h-5',
|
||
|
|
isActive ? 'text-monaco-600' : 'text-gray-500 group-hover:text-monaco-600'
|
||
|
|
]"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<!-- Label and Badge -->
|
||
|
|
<transition name="slide">
|
||
|
|
<div v-if="!collapsed" class="flex-1 flex items-center justify-between">
|
||
|
|
<span class="text-sm font-medium">{{ label }}</span>
|
||
|
|
|
||
|
|
<!-- Badge -->
|
||
|
|
<span
|
||
|
|
v-if="badge"
|
||
|
|
:class="[
|
||
|
|
'px-2 py-0.5 text-xs rounded-full font-medium transition-colors',
|
||
|
|
isActive
|
||
|
|
? 'bg-monaco-600 text-white'
|
||
|
|
: 'bg-glass-monaco-soft text-monaco-700 group-hover:bg-monaco-100'
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
{{ badge }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</transition>
|
||
|
|
|
||
|
|
<!-- Tooltip for collapsed state -->
|
||
|
|
<div
|
||
|
|
v-if="collapsed"
|
||
|
|
class="absolute left-full ml-2 px-3 py-2 bg-gray-900 text-white text-sm rounded-lg
|
||
|
|
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
||
|
|
transition-all duration-200 whitespace-nowrap z-50"
|
||
|
|
>
|
||
|
|
{{ label }}
|
||
|
|
<div class="absolute left-0 top-1/2 -translate-x-1 -translate-y-1/2
|
||
|
|
w-0 h-0 border-4 border-transparent border-r-gray-900"></div>
|
||
|
|
</div>
|
||
|
|
</NuxtLink>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { computed } from 'vue'
|
||
|
|
import { useRoute } from 'vue-router'
|
||
|
|
|
||
|
|
const props = defineProps({
|
||
|
|
to: {
|
||
|
|
type: String,
|
||
|
|
required: true
|
||
|
|
},
|
||
|
|
icon: {
|
||
|
|
type: Object,
|
||
|
|
required: true
|
||
|
|
},
|
||
|
|
label: {
|
||
|
|
type: String,
|
||
|
|
required: true
|
||
|
|
},
|
||
|
|
badge: {
|
||
|
|
type: [String, Number],
|
||
|
|
default: null
|
||
|
|
},
|
||
|
|
collapsed: {
|
||
|
|
type: Boolean,
|
||
|
|
default: false
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
const route = useRoute()
|
||
|
|
|
||
|
|
const isActive = computed(() => {
|
||
|
|
return route.path === props.to || route.path.startsWith(props.to + '/')
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
/* Slide transition for label */
|
||
|
|
.slide-enter-active,
|
||
|
|
.slide-leave-active {
|
||
|
|
transition: all 0.2s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.slide-enter-from {
|
||
|
|
opacity: 0;
|
||
|
|
transform: translateX(-10px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.slide-leave-to {
|
||
|
|
opacity: 0;
|
||
|
|
transform: translateX(-10px);
|
||
|
|
}
|
||
|
|
</style>
|