Phase 3: Graph View, Backlinks UI, Quick Switcher, Dark Mode, Export

Features:
- GraphView.vue: SVG-based force-directed graph visualization for projects
- QuickSwitcher.vue: Cmd+K modal with fuzzy search via /search API
- Dark Mode: Theme toggle in Header, persisted in localStorage, system pref support
- Backlinks UI: Incoming and outgoing links in DocumentView
- Export: Document (markdown/JSON) and Project (ZIP/JSON) export with download
- New composables: useTheme.ts for dark/light/system theme management
- New store methods: fetchBacklinks, fetchOutgoingLinks, search, exportDocument, fetchProjectGraph, exportProject
- TypeScript types for all Phase 3 API responses
This commit is contained in:
Hiro
2026-03-30 23:47:17 +00:00
parent 24925e1acb
commit f758927848
11 changed files with 1563 additions and 10 deletions

View File

@@ -1,13 +1,33 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useTheme } from '@/composables/useTheme'
import QuickSwitcher from '@/components/common/QuickSwitcher.vue'
const router = useRouter()
const authStore = useAuthStore()
const { resolvedTheme, toggleTheme } = useTheme()
const searchQuery = ref('')
const showUserMenu = ref(false)
const showQuickSwitcher = ref(false)
function handleGlobalKeydown(e: KeyboardEvent) {
// Cmd+K or Ctrl+K
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
showQuickSwitcher.value = true
}
}
onMounted(() => {
document.addEventListener('keydown', handleGlobalKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleGlobalKeydown)
})
function handleSearch() {
if (searchQuery.value.trim()) {
@@ -50,6 +70,24 @@ function logout() {
</div>
<div class="header__right">
<button class="header__theme-toggle" @click="toggleTheme" :title="resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'">
<!-- Sun icon for dark mode (click to go light) -->
<svg v-if="resolvedTheme === 'dark'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<!-- Moon icon for light mode (click to go dark) -->
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
<div class="header__user" @click="showUserMenu = !showUserMenu">
<div class="header__avatar">
{{ authStore.user?.username?.charAt(0).toUpperCase() || 'U' }}
@@ -79,6 +117,7 @@ function logout() {
</div>
</div>
</header>
<QuickSwitcher :show="showQuickSwitcher" @close="showQuickSwitcher = false" />
</template>
<style scoped>
@@ -164,6 +203,26 @@ function logout() {
justify-content: flex-end;
}
.header__theme-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: none;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s;
margin-right: 0.5rem;
}
.header__theme-toggle:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.header__user {
position: relative;
display: flex;