feat: amber wordmark and NavRail active indicator
This commit is contained in:
74
src/components/NavRail.vue
Normal file
74
src/components/NavRail.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useTimerStore } from '../stores/timer'
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
Clock,
|
||||||
|
FolderKanban,
|
||||||
|
List,
|
||||||
|
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: 'Projects', path: '/projects', icon: FolderKanban },
|
||||||
|
{ name: 'Entries', path: '/entries', icon: List },
|
||||||
|
{ name: 'Reports', path: '/reports', icon: BarChart3 },
|
||||||
|
{ name: 'Invoices', path: '/invoices', icon: FileText },
|
||||||
|
{ name: 'Settings', path: '/settings', icon: Settings }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentPath = computed(() => route.path)
|
||||||
|
|
||||||
|
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">
|
||||||
|
<!-- Navigation icons -->
|
||||||
|
<div class="flex-1 flex flex-col items-center pt-2 gap-1">
|
||||||
|
<button
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="item.path"
|
||||||
|
@click="navigate(item.path)"
|
||||||
|
class="relative w-12 h-12 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"
|
||||||
|
>
|
||||||
|
<!-- Active indicator (left border) -->
|
||||||
|
<div
|
||||||
|
v-if="currentPath === item.path"
|
||||||
|
class="absolute left-0 top-2 bottom-2 w-[2px] bg-accent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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 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 running 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>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
119
src/components/TitleBar.vue
Normal file
119
src/components/TitleBar.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useTimerStore } from '../stores/timer'
|
||||||
|
import { useProjectsStore } from '../stores/projects'
|
||||||
|
|
||||||
|
const appWindow = getCurrentWindow()
|
||||||
|
const isMaximized = ref(false)
|
||||||
|
const timerStore = useTimerStore()
|
||||||
|
const projectsStore = useProjectsStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
isMaximized.value = await appWindow.isMaximized()
|
||||||
|
})
|
||||||
|
|
||||||
|
function getProjectName(projectId: number | null): string {
|
||||||
|
if (!projectId) return ''
|
||||||
|
const project = projectsStore.projects.find(p => p.id === projectId)
|
||||||
|
return project?.name || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function minimize() {
|
||||||
|
await appWindow.minimize()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleMaximize() {
|
||||||
|
await appWindow.toggleMaximize()
|
||||||
|
isMaximized.value = await appWindow.isMaximized()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function close() {
|
||||||
|
await appWindow.close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="h-10 flex items-center justify-between px-4 bg-bg-surface border-b border-border-subtle select-none shrink-0"
|
||||||
|
data-tauri-drag-region
|
||||||
|
>
|
||||||
|
<!-- Left: App name -->
|
||||||
|
<div class="flex items-center" data-tauri-drag-region>
|
||||||
|
<span
|
||||||
|
class="text-accent-text text-[0.6875rem] font-medium tracking-[0.1em] uppercase"
|
||||||
|
data-tauri-drag-region
|
||||||
|
>
|
||||||
|
ZeroClock
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center: Running timer status -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 transition-opacity duration-150"
|
||||||
|
:class="timerStore.isRunning ? 'opacity-100' : 'opacity-0 pointer-events-none'"
|
||||||
|
data-tauri-drag-region
|
||||||
|
>
|
||||||
|
<!-- Pulsing green dot -->
|
||||||
|
<div class="w-2 h-2 rounded-full bg-status-running animate-pulse-dot" />
|
||||||
|
|
||||||
|
<!-- Project name -->
|
||||||
|
<span class="text-[0.6875rem] text-text-secondary">
|
||||||
|
{{ getProjectName(timerStore.selectedProjectId) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Elapsed time -->
|
||||||
|
<span class="text-[0.75rem] font-mono text-text-primary tracking-wider">
|
||||||
|
{{ timerStore.formattedTime }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Stop button -->
|
||||||
|
<button
|
||||||
|
@click="timerStore.stop()"
|
||||||
|
class="w-5 h-5 flex items-center justify-center text-text-tertiary hover:text-status-error transition-colors duration-150"
|
||||||
|
title="Stop timer"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3 h-3">
|
||||||
|
<rect x="3" y="3" width="10" height="10" rx="1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Window controls -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button
|
||||||
|
@click="minimize"
|
||||||
|
class="w-10 h-10 flex items-center justify-center text-text-tertiary hover:text-text-secondary transition-colors duration-150"
|
||||||
|
title="Minimize"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" class="w-3.5 h-3.5">
|
||||||
|
<path d="M5 12h14" stroke="currentColor" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="toggleMaximize"
|
||||||
|
class="w-10 h-10 flex items-center justify-center text-text-tertiary hover:text-text-secondary transition-colors duration-150"
|
||||||
|
:title="isMaximized ? 'Restore' : 'Maximize'"
|
||||||
|
>
|
||||||
|
<svg v-if="!isMaximized" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-3 h-3">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="1" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-3 h-3">
|
||||||
|
<rect x="3" y="7" width="14" height="14" rx="1" />
|
||||||
|
<path d="M7 7V5a1 1 0 011-1h12a1 1 0 011 1v12a1 1 0 01-1 1h-2" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close"
|
||||||
|
class="w-10 h-10 flex items-center justify-center text-text-tertiary hover:text-status-error transition-colors duration-150"
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-3.5 h-3.5">
|
||||||
|
<path fill-rule="evenodd" d="M5.47 5.47a.75.75 0 011.06 0L12 10.94l5.47-5.47a.75.75 0 111.06 1.06L13.06 12l5.47 5.47a.75.75 0 11-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 01-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 010-1.06z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user