Files
archy/aiui/packages/app/src/components/content/FileTreeNode.vue
T

133 lines
5.0 KiB
Vue
Raw Normal View History

2026-08-12 10:55:49 +00:00
<template>
<div class="group/node">
<div
class="w-full flex items-center gap-1.5 py-1 px-2 rounded-lg text-xs transition-colors cursor-pointer"
:class="[
isActive
? isDark ? 'bg-white/10 text-white/90' : 'bg-black/8 text-gray-900'
: isDark ? 'text-white/60 hover:bg-white/[0.04] hover:text-white/80' : 'text-gray-600 hover:bg-black/[0.03] hover:text-gray-800',
]"
:style="{ paddingLeft: `${depth * 12 + 8}px` }"
@click="handleClick"
>
<!-- Expand/collapse for directories -->
<svg
v-if="entry.isDirectory"
class="w-3 h-3 shrink-0 transition-transform duration-150"
:class="expanded ? 'rotate-90' : ''"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<!-- File/folder icon -->
<svg class="w-3.5 h-3.5 shrink-0"
:class="entry.isDirectory
? 'text-accent/70'
: isDark ? 'text-white/30' : 'text-gray-400'"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-if="entry.isDirectory" 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" />
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span class="truncate flex-1">{{ entry.name }}</span>
<!-- Context selector checkbox (files: far right, visible on hover or when selected) -->
<button
v-if="!entry.isDirectory"
class="shrink-0 w-4 h-4 rounded-full border flex items-center justify-center transition-all ml-auto"
:class="[
isSelected
? 'bg-accent border-accent text-white'
: isDark
? 'border-white/20 opacity-0 group-hover/node:opacity-100 hover:border-white/40'
: 'border-black/15 opacity-0 group-hover/node:opacity-100 hover:border-black/30',
]"
aria-label="Toggle file for chat context"
@click.stop="handleToggleContext"
>
<svg v-if="isSelected" class="w-2.5 h-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg>
</button>
<!-- Context selector for directories (top-right, visible on hover or when selected) -->
<button
v-if="entry.isDirectory"
class="shrink-0 w-4 h-4 rounded-full border flex items-center justify-center transition-all ml-auto"
:class="[
isDirSelected
? 'bg-accent border-accent text-white'
: isDark
? 'border-white/20 opacity-0 group-hover/node:opacity-100 hover:border-white/40'
: 'border-black/15 opacity-0 group-hover/node:opacity-100 hover:border-black/30',
]"
aria-label="Add folder to chat context"
@click.stop="handleToggleDirContext"
>
<svg v-if="isDirSelected" class="w-2.5 h-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg>
</button>
</div>
<!-- Children (when expanded) -->
<div v-if="entry.isDirectory && expanded && entry.children">
<FileTreeNode
v-for="child in entry.children"
:key="child.path"
:entry="child"
:active-file="activeFile"
:depth="depth + 1"
@select="$emit('select', $event)"
@toggle-context="$emit('toggle-context', $event)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useTheme } from '@/composables/useTheme'
import { useCodeContext, type FileEntry } from '@/composables/useCodeContext'
const props = defineProps<{
entry: FileEntry
activeFile: string | null
depth: number
}>()
const { isFileSelected } = useCodeContext()
const emit = defineEmits<{
select: [path: string]
'toggle-context': [path: string]
}>()
const { isDark } = useTheme()
const expanded = ref(props.depth < 1) // Auto-expand first level
const isActive = computed(() => !props.entry.isDirectory && props.activeFile === props.entry.path)
const isSelected = computed(() => !props.entry.isDirectory && isFileSelected(props.entry.path))
const isDirSelected = computed(() => props.entry.isDirectory && isFileSelected(props.entry.path))
function handleClick() {
if (props.entry.isDirectory) {
expanded.value = !expanded.value
} else {
// Click opens file in code viewer
emit('select', props.entry.path)
}
}
function handleToggleContext() {
emit('toggle-context', props.entry.path)
}
function handleToggleDirContext() {
emit('toggle-context', props.entry.path)
}
</script>