256 lines
8.6 KiB
Vue
256 lines
8.6 KiB
Vue
<template>
|
|
<div class="flex flex-col h-full">
|
|
<!-- Header with breadcrumb -->
|
|
<div class="shrink-0 px-4 py-3 flex items-center justify-between gap-2"
|
|
:style="isDark
|
|
? 'border-bottom: 1px solid rgba(255, 255, 255, 0.08)'
|
|
: 'border-bottom: 1px solid rgba(0, 0, 0, 0.06)'">
|
|
<div class="flex items-center gap-1 min-w-0 flex-1">
|
|
<!-- Breadcrumb: "Projects" root (clickable when deeper) -->
|
|
<button
|
|
v-if="viewState !== 'projects'"
|
|
class="text-xs shrink-0 transition-colors"
|
|
:class="isDark
|
|
? 'text-white/40 hover:text-white/70 hover:underline'
|
|
: 'text-gray-400 hover:text-gray-700 hover:underline'"
|
|
@click="backToProjects"
|
|
>
|
|
Projects
|
|
</button>
|
|
<span v-if="viewState !== 'projects'" class="text-xs shrink-0"
|
|
:class="isDark ? 'text-white/20' : 'text-gray-300'">/</span>
|
|
|
|
<!-- Current location (not clickable) -->
|
|
<span class="text-sm font-semibold truncate"
|
|
:class="isDark ? 'text-white/90' : 'text-gray-900'">
|
|
{{ currentTitle }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 shrink-0">
|
|
<!-- Subtitle info -->
|
|
<p v-if="viewState === 'projects'" class="text-xs"
|
|
:class="isDark ? 'text-white/30' : 'text-gray-400'">
|
|
{{ projectList.length }} repos
|
|
</p>
|
|
<p v-else-if="viewState === 'filetree'" class="text-xs"
|
|
:class="isDark ? 'text-white/30' : 'text-gray-400'">
|
|
{{ activeProject?.language }}
|
|
</p>
|
|
<slot name="header-actions" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project list -->
|
|
<div v-if="viewState === 'projects'" class="flex-1 overflow-y-auto custom-scrollbar p-3">
|
|
<!-- Search + New Project -->
|
|
<div class="mb-3 flex gap-2">
|
|
<input
|
|
v-model="search"
|
|
type="text"
|
|
placeholder="Search projects..."
|
|
class="flex-1 min-w-0 px-3 py-2 rounded-lg text-base bg-transparent outline-none"
|
|
:class="isDark
|
|
? 'text-white/80 placeholder:text-white/20 border border-white/10 focus:border-white/25'
|
|
: 'text-gray-800 placeholder:text-gray-400 border border-black/10 focus:border-black/20'"
|
|
/>
|
|
<button
|
|
class="shrink-0 px-3 py-2 rounded-lg text-xs font-medium transition-colors flex items-center gap-1.5"
|
|
:class="isDark
|
|
? 'bg-accent/20 text-accent hover:bg-accent/30'
|
|
: 'bg-accent/10 text-accent hover:bg-accent/20'"
|
|
@click="showNewProjectDialog"
|
|
>
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
New
|
|
</button>
|
|
</div>
|
|
|
|
<!-- New project inline dialog -->
|
|
<div v-if="isCreatingProject" class="mb-3 p-3 rounded-xl"
|
|
:class="isDark ? 'bg-white/[0.05] border border-white/10' : 'bg-black/[0.03] border border-black/8'">
|
|
<p class="text-xs font-medium mb-2"
|
|
:class="isDark ? 'text-white/70' : 'text-gray-700'">
|
|
New Project
|
|
</p>
|
|
<input
|
|
ref="newProjectInputRef"
|
|
v-model="newProjectName"
|
|
type="text"
|
|
placeholder="Project name..."
|
|
class="w-full px-3 py-2 rounded-lg text-base bg-transparent outline-none mb-2"
|
|
:class="isDark
|
|
? 'text-white/80 placeholder:text-white/20 border border-white/10 focus:border-white/25'
|
|
: 'text-gray-800 placeholder:text-gray-400 border border-black/10 focus:border-black/20'"
|
|
@keydown.enter="confirmCreateProject"
|
|
@keydown.escape="cancelCreateProject"
|
|
/>
|
|
<div class="flex justify-end gap-2">
|
|
<button
|
|
class="text-xs px-2.5 py-1 rounded-lg transition-colors"
|
|
:class="isDark ? 'text-white/40 hover:text-white/70' : 'text-gray-500 hover:text-gray-800'"
|
|
@click="cancelCreateProject"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
class="text-xs px-2.5 py-1 rounded-lg font-medium transition-colors"
|
|
:class="isDark ? 'bg-accent/20 text-accent hover:bg-accent/30' : 'bg-accent/10 text-accent hover:bg-accent/20'"
|
|
:disabled="!newProjectName.trim()"
|
|
@click="confirmCreateProject"
|
|
>
|
|
Create
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project grid -->
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<button
|
|
v-for="project in filteredProjects"
|
|
:key="project.path"
|
|
class="text-left p-3 rounded-xl transition-all duration-150"
|
|
:class="isDark
|
|
? 'bg-white/[0.03] hover:bg-white/[0.07] border border-white/5'
|
|
: 'bg-black/[0.02] hover:bg-black/[0.05] border border-black/5'"
|
|
@click="selectProject(project)"
|
|
>
|
|
<div class="w-8 h-8 rounded-lg flex items-center justify-center mb-2"
|
|
:class="isDark ? 'bg-white/5' : 'bg-black/5'">
|
|
<svg class="w-4 h-4" :class="project.isGit ? 'text-accent' : isDark ? 'text-white/40' : 'text-gray-400'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-xs font-medium truncate"
|
|
:class="isDark ? 'text-white/80' : 'text-gray-800'">
|
|
{{ project.name }}
|
|
</p>
|
|
<p class="text-xs mt-0.5 truncate"
|
|
:class="isDark ? 'text-white/25' : 'text-gray-400'">
|
|
{{ project.language }}
|
|
</p>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File tree -->
|
|
<div v-else-if="viewState === 'filetree'" class="flex-1 overflow-y-auto custom-scrollbar p-2">
|
|
<FileTreeNode
|
|
v-for="entry in fileTree"
|
|
:key="entry.path"
|
|
:entry="entry"
|
|
:active-file="activeFile"
|
|
:depth="0"
|
|
@select="handleFileOpen"
|
|
@toggle-context="handleToggleContext"
|
|
/>
|
|
<div v-if="fileTree.length === 0" class="flex items-center justify-center py-12">
|
|
<p class="text-xs" :class="isDark ? 'text-white/30' : 'text-gray-400'">
|
|
Loading file tree...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, nextTick, onMounted } from 'vue'
|
|
import { useTheme } from '@/composables/useTheme'
|
|
import { useCodeContext, type ProjectInfo } from '@/composables/useCodeContext'
|
|
import FileTreeNode from './FileTreeNode.vue'
|
|
|
|
defineProps<{
|
|
isWideDesktop?: boolean
|
|
isMobile?: boolean
|
|
}>()
|
|
|
|
const { isDark } = useTheme()
|
|
const {
|
|
projectList,
|
|
activeProject,
|
|
fileTree,
|
|
activeFile,
|
|
codeMode,
|
|
selectedFiles,
|
|
selectProject: doSelectProject,
|
|
openFile,
|
|
createProject,
|
|
clearActiveFile,
|
|
toggleFileSelection,
|
|
isFileSelected,
|
|
loadProjects,
|
|
} = useCodeContext()
|
|
|
|
onMounted(() => {
|
|
if (projectList.value.length === 0) loadProjects()
|
|
})
|
|
|
|
const search = ref('')
|
|
const isCreatingProject = ref(false)
|
|
const newProjectName = ref('')
|
|
const newProjectInputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
// View state: projects | filetree (file content always opens in Context panel)
|
|
type CodeViewState = 'projects' | 'filetree'
|
|
const viewState = computed<CodeViewState>(() => {
|
|
if (!activeProject.value) return 'projects'
|
|
return 'filetree'
|
|
})
|
|
|
|
const currentTitle = computed(() => {
|
|
if (viewState.value === 'projects') return 'Projects'
|
|
return activeProject.value?.name ?? 'Projects'
|
|
})
|
|
|
|
const filteredProjects = computed(() => {
|
|
const q = search.value.toLowerCase()
|
|
if (!q) return projectList.value
|
|
return projectList.value.filter(p =>
|
|
p.name.toLowerCase().includes(q) || (p.language ?? '').toLowerCase().includes(q)
|
|
)
|
|
})
|
|
|
|
function selectProject(project: ProjectInfo) {
|
|
doSelectProject(project)
|
|
}
|
|
|
|
function backToProjects() {
|
|
const { activeProject: ap, fileTree: ft } = useCodeContext()
|
|
ap.value = null
|
|
ft.value = []
|
|
clearActiveFile()
|
|
}
|
|
|
|
function handleFileOpen(filePath: string) {
|
|
openFile(filePath)
|
|
}
|
|
|
|
function handleToggleContext(filePath: string) {
|
|
toggleFileSelection(filePath)
|
|
}
|
|
|
|
// New project dialog
|
|
function showNewProjectDialog() {
|
|
isCreatingProject.value = true
|
|
newProjectName.value = ''
|
|
nextTick(() => newProjectInputRef.value?.focus())
|
|
}
|
|
|
|
function cancelCreateProject() {
|
|
isCreatingProject.value = false
|
|
newProjectName.value = ''
|
|
}
|
|
|
|
function confirmCreateProject() {
|
|
const name = newProjectName.value.trim()
|
|
if (!name) return
|
|
createProject(name)
|
|
isCreatingProject.value = false
|
|
newProjectName.value = ''
|
|
}
|
|
</script>
|
|
|