- Page transitions with slide-up/fade on route changes (App.vue) - NavRail sliding active indicator with spring-like easing - List enter/leave/move animations on Entries, Projects, Clients, Timer - Modal enter/leave transitions with scale+fade on all dialogs - Dropdown transitions with overshoot on all select/picker components - Button feedback (scale on hover/active), card hover lift effects - Timer pulse on start, glow on stop, floating empty state icons - Content fade-in on Dashboard, Reports, Calendar, Timesheet - Tag chip enter/leave animations in AppTagInput - Progress bar smooth width transitions - Implementation plan document
90 lines
3.0 KiB
Vue
90 lines
3.0 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useTimerStore } from '../stores/timer'
|
|
import {
|
|
LayoutDashboard,
|
|
Clock,
|
|
Users,
|
|
FolderKanban,
|
|
List,
|
|
CalendarDays,
|
|
Grid3X3,
|
|
BarChart3,
|
|
FileText,
|
|
Settings
|
|
} from 'lucide-vue-next'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const timerStore = useTimerStore()
|
|
|
|
const navItems = [
|
|
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
|
|
{ name: 'Timer', path: '/timer', icon: Clock },
|
|
{ name: 'Clients', path: '/clients', icon: Users },
|
|
{ name: 'Projects', path: '/projects', icon: FolderKanban },
|
|
{ name: 'Entries', path: '/entries', icon: List },
|
|
{ name: 'Calendar', path: '/calendar', icon: CalendarDays },
|
|
{ name: 'Timesheet', path: '/timesheet', icon: Grid3X3 },
|
|
{ name: 'Invoices', path: '/invoices', icon: FileText },
|
|
{ name: 'Reports', path: '/reports', icon: BarChart3 },
|
|
{ name: 'Settings', path: '/settings', icon: Settings }
|
|
]
|
|
|
|
const currentPath = computed(() => route.path)
|
|
|
|
const activeIndex = computed(() => {
|
|
return navItems.findIndex(item => item.path === currentPath.value)
|
|
})
|
|
|
|
function navigate(path: string) {
|
|
router.push(path)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<nav class="w-12 flex flex-col items-center bg-bg-surface border-r border-border-subtle shrink-0">
|
|
<div class="relative flex-1 flex flex-col items-center pt-2 gap-1">
|
|
<!-- Sliding active indicator -->
|
|
<div
|
|
v-if="activeIndex >= 0"
|
|
class="absolute left-0 w-[2px] bg-accent transition-all duration-300"
|
|
:style="{ top: `${activeIndex * 52 + 8 + 8}px`, height: '36px' }"
|
|
style="transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);"
|
|
/>
|
|
|
|
<button
|
|
v-for="item in navItems"
|
|
:key="item.path"
|
|
@click="navigate(item.path)"
|
|
class="relative w-12 h-[52px] flex items-center justify-center transition-colors duration-150 group"
|
|
:class="currentPath === item.path
|
|
? 'text-text-primary'
|
|
: 'text-text-tertiary hover:text-text-secondary'"
|
|
:title="item.name"
|
|
>
|
|
<component :is="item.icon" class="w-[18px] h-[18px]" :stroke-width="1.5" />
|
|
|
|
<!-- Tooltip -->
|
|
<div class="absolute left-full ml-2 px-2 py-1 bg-bg-elevated border border-border-subtle rounded-lg text-[0.6875rem] text-text-primary whitespace-nowrap opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity duration-150 z-50">
|
|
<div class="absolute -left-1 top-1/2 -translate-y-1/2 w-0 h-0 border-y-4 border-y-transparent border-r-4" style="border-right-color: var(--color-bg-elevated)"></div>
|
|
{{ item.name }}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Timer status indicator (bottom) -->
|
|
<div class="pb-4">
|
|
<div
|
|
v-if="timerStore.isRunning"
|
|
class="w-2 h-2 rounded-full bg-status-running animate-pulse-dot"
|
|
/>
|
|
<div
|
|
v-else-if="timerStore.isPaused"
|
|
class="w-2 h-2 rounded-full bg-status-warning animate-pulse-dot"
|
|
/>
|
|
</div>
|
|
</nav>
|
|
</template>
|