Files
agent-ui/frontend/src/pages/GitPage.vue
josedario87 6167dfa440 feat: Add project file tree viewer to Git page
Add Files tab with browsable project structure and file content viewer.
New components: ProjectTree for navigation, FileViewer for content display.
Backend endpoints: /api/git/tree and /api/git/file.
2026-02-14 10:51:17 -06:00

1241 lines
28 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useGitApi } from '@/composables/git'
import { DiffViewer, FileTree, CommitList, BranchSelector, ProjectTree, FileViewer } from '@/components/git'
type TabName = 'status' | 'history' | 'compare' | 'files'
const {
status,
commits,
branches,
currentBranch,
diff,
compareResult,
selectedCommit,
fileTree,
fileContent,
loading,
error,
fetchStatus,
fetchDiff,
fetchLog,
fetchCommit,
fetchBranches,
compare,
clearDiff,
clearCompare,
fetchFileTree,
fetchFileContent,
clearFileContent
} = useGitApi()
const activeTab = ref<TabName>('status')
const selectedFile = ref<string | null>(null)
const selectedStaged = ref<boolean>(false)
const expandedFiles = ref<Set<string>>(new Set())
// Compare tab state
const compareBase = ref('')
const compareHead = ref('')
// Files tab state
const selectedFilePath = ref<string | null>(null)
// Load initial data
onMounted(async () => {
await Promise.all([
fetchStatus(),
fetchLog(30),
fetchBranches()
])
// Load initial diff
await fetchDiff()
})
// Refresh when tab changes
watch(activeTab, async (tab) => {
if (tab === 'status') {
await fetchStatus()
await fetchDiff()
} else if (tab === 'history') {
await fetchLog(30)
} else if (tab === 'files') {
await fetchFileTree()
}
})
// Handle file selection in status tab
async function handleFileSelect(path: string, staged: boolean) {
selectedFile.value = path
selectedStaged.value = staged
await fetchDiff({ file: path, staged })
}
// Select file from list
async function selectFile(path: string, staged: boolean) {
selectedFile.value = path
selectedStaged.value = staged
await fetchDiff({ file: path, staged })
}
// Clear selection
function clearSelection() {
selectedFile.value = null
clearDiff()
}
// Handle commit selection in history tab
async function handleCommitSelect(sha: string) {
await fetchCommit(sha)
}
// Load more commits
async function handleLoadMore() {
await fetchLog(30, commits.value.length, true)
}
// Toggle file expansion in diff viewer
function toggleFile(path: string) {
if (expandedFiles.value.has(path)) {
expandedFiles.value.delete(path)
} else {
expandedFiles.value.add(path)
}
expandedFiles.value = new Set(expandedFiles.value)
}
// Run comparison
async function runCompare() {
if (compareBase.value && compareHead.value) {
await compare(compareBase.value, compareHead.value)
}
}
// Refresh status
async function refresh() {
await fetchStatus()
await fetchDiff()
}
// Handle file selection in files tab
async function handleFileTreeSelect(path: string) {
selectedFilePath.value = path
await fetchFileContent(path)
}
// Close file viewer
function closeFileViewer() {
selectedFilePath.value = null
clearFileContent()
}
// Total changes count
const totalChanges = computed(() => {
if (!status.value) return 0
return status.value.staged.length + status.value.unstaged.length + status.value.untracked.length
})
</script>
<template>
<div class="git-page">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<h2>Git</h2>
<button class="refresh-btn" @click="refresh" :disabled="loading" title="Refresh">
<svg :class="{ spinning: loading }" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
</button>
</div>
<div class="branch-info">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="6" y1="3" x2="6" y2="15" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
<span class="branch-name">{{ currentBranch || 'No branch' }}</span>
<span v-if="status?.ahead" class="ahead-behind">
{{ status.ahead }}
</span>
<span v-if="status?.behind" class="ahead-behind">
{{ status.behind }}
</span>
</div>
<div v-if="activeTab === 'status' && status" class="file-tree-container">
<FileTree
:staged="status.staged"
:unstaged="status.unstaged"
:untracked="status.untracked"
:selected-file="selectedFile"
@select="handleFileSelect"
/>
</div>
</aside>
<!-- Main content -->
<main class="main-content">
<!-- Tab bar -->
<div class="tab-bar">
<button
:class="['tab', { active: activeTab === 'status' }]"
@click="activeTab = 'status'"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
Status
<span v-if="totalChanges > 0" class="tab-badge">{{ totalChanges }}</span>
</button>
<button
:class="['tab', { active: activeTab === 'history' }]"
@click="activeTab = 'history'"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
History
</button>
<button
:class="['tab', { active: activeTab === 'compare' }]"
@click="activeTab = 'compare'"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10" />
<line x1="12" y1="20" x2="12" y2="4" />
<line x1="6" y1="20" x2="6" y2="14" />
</svg>
Compare
</button>
<button
:class="['tab', { active: activeTab === 'files' }]"
@click="activeTab = 'files'"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
Files
</button>
</div>
<!-- Tab content -->
<div class="tab-content">
<!-- Status Tab -->
<div v-if="activeTab === 'status'" class="status-tab">
<div v-if="loading && !diff" class="loading-state">
<div class="spinner"></div>
Cargando...
</div>
<template v-else-if="status">
<!-- Staged files -->
<div v-if="status.staged.length > 0" class="file-section">
<div class="section-header staged">
<span class="section-dot"></span>
<span class="section-label">Staged</span>
<span class="section-count">{{ status.staged.length }}</span>
</div>
<div class="file-list">
<div
v-for="file in status.staged"
:key="'staged-' + file.path"
:class="['file-row', { selected: selectedFile === file.path && selectedStaged }]"
@click="selectFile(file.path, true)"
>
<span :class="['file-badge', file.status]">{{ file.status[0].toUpperCase() }}</span>
<span class="file-name">{{ file.path }}</span>
</div>
</div>
</div>
<!-- Unstaged files -->
<div v-if="status.unstaged.length > 0" class="file-section">
<div class="section-header unstaged">
<span class="section-dot"></span>
<span class="section-label">Unstaged</span>
<span class="section-count">{{ status.unstaged.length }}</span>
</div>
<div class="file-list">
<div
v-for="file in status.unstaged"
:key="'unstaged-' + file.path"
:class="['file-row', { selected: selectedFile === file.path && !selectedStaged }]"
@click="selectFile(file.path, false)"
>
<span :class="['file-badge', file.status]">{{ file.status[0].toUpperCase() }}</span>
<span class="file-name">{{ file.path }}</span>
</div>
</div>
</div>
<!-- Untracked files -->
<div v-if="status.untracked.length > 0" class="file-section">
<div class="section-header untracked">
<span class="section-dot"></span>
<span class="section-label">Untracked</span>
<span class="section-count">{{ status.untracked.length }}</span>
</div>
<div class="file-list">
<div
v-for="file in status.untracked"
:key="'untracked-' + file"
class="file-row untracked-file"
>
<span class="file-badge untracked">?</span>
<span class="file-name">{{ file }}</span>
</div>
</div>
</div>
<!-- Diff viewer for selected file -->
<div v-if="selectedFile" class="diff-section">
<div class="diff-header">
<span class="diff-file-name">{{ selectedFile }}</span>
<span class="diff-type">{{ selectedStaged ? '(staged)' : '(unstaged)' }}</span>
<button class="close-diff" @click="clearSelection"></button>
</div>
<div v-if="loading" class="diff-loading">
<div class="spinner"></div>
Cargando diff...
</div>
<div v-else-if="diff?.files?.length">
<DiffViewer
:files="diff.files"
:expanded-files="expandedFiles"
@toggle-file="toggleFile"
/>
</div>
<div v-else class="diff-empty">
No hay cambios para mostrar
</div>
</div>
<!-- Empty state when no changes -->
<div v-if="!status.staged.length && !status.unstaged.length && !status.untracked.length" class="empty-state">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
<p>Working directory clean</p>
<span>No hay cambios pendientes</span>
</div>
</template>
<div v-else class="empty-state">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
<p>Working directory clean</p>
<span>No hay cambios pendientes</span>
</div>
</div>
<!-- History Tab -->
<div v-if="activeTab === 'history'" class="history-tab">
<div class="history-layout">
<div class="commit-list-container">
<CommitList
:commits="commits"
:selected-sha="selectedCommit?.sha"
:loading="loading"
@select="handleCommitSelect"
@load-more="handleLoadMore"
/>
</div>
<div v-if="selectedCommit" class="commit-detail">
<div class="commit-header">
<h3>{{ selectedCommit.message }}</h3>
<div class="commit-meta">
<span class="sha">{{ selectedCommit.shortSha }}</span>
<span>{{ selectedCommit.author }}</span>
<span>{{ new Date(selectedCommit.timestamp * 1000).toLocaleString() }}</span>
</div>
<p v-if="selectedCommit.body" class="commit-body">{{ selectedCommit.body }}</p>
</div>
<DiffViewer
v-if="selectedCommit.files?.length"
:files="selectedCommit.files"
:expanded-files="expandedFiles"
@toggle-file="toggleFile"
/>
</div>
<div v-else class="select-commit-hint">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<p>Selecciona un commit para ver los detalles</p>
</div>
</div>
</div>
<!-- Compare Tab -->
<div v-if="activeTab === 'compare'" class="compare-tab">
<div class="compare-header">
<BranchSelector
:branches="branches"
:selected="compareBase"
label="Base"
@select="compareBase = $event"
/>
<span class="compare-arrow"></span>
<BranchSelector
:branches="branches"
:selected="compareHead"
label="Head"
@select="compareHead = $event"
/>
<button
class="compare-btn"
:disabled="!compareBase || !compareHead || loading"
@click="runCompare"
>
Comparar
</button>
</div>
<div v-if="compareResult" class="compare-result">
<div class="compare-stats">
<span class="stat">
<strong>{{ compareResult.stats.filesChanged }}</strong> archivos
</span>
<span class="stat additions">
+{{ compareResult.stats.additions }}
</span>
<span class="stat deletions">
-{{ compareResult.stats.deletions }}
</span>
</div>
<DiffViewer
:files="compareResult.files"
:expanded-files="expandedFiles"
@toggle-file="toggleFile"
/>
</div>
<div v-else-if="!compareBase || !compareHead" class="compare-hint">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<line x1="18" y1="20" x2="18" y2="10" />
<line x1="12" y1="20" x2="12" y2="4" />
<line x1="6" y1="20" x2="6" y2="14" />
</svg>
<p>Selecciona dos ramas o commits para comparar</p>
</div>
</div>
<!-- Files Tab -->
<div v-if="activeTab === 'files'" class="files-tab">
<div class="files-layout">
<div class="file-tree-panel">
<div class="panel-header">
<span>Project Files</span>
<button class="refresh-tree-btn" @click="fetchFileTree" :disabled="loading" title="Refresh">
<svg :class="{ spinning: loading }" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
</button>
</div>
<div class="tree-container">
<ProjectTree
:nodes="fileTree"
:selected-path="selectedFilePath"
:loading="loading && !fileTree.length"
@select="handleFileTreeSelect"
/>
</div>
</div>
<div class="file-content-panel">
<FileViewer
:file="fileContent"
:loading="loading && selectedFilePath !== null"
@close="closeFileViewer"
/>
</div>
</div>
</div>
</div>
<!-- Error message -->
<div v-if="error" class="error-message">
{{ error }}
</div>
</main>
</div>
</template>
<style scoped>
.git-page {
display: flex;
flex: 1;
overflow: hidden;
background: var(--bg-primary);
color: var(--text-primary);
}
/* Sidebar */
.sidebar {
width: 280px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.sidebar-header h2 {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.refresh-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s;
}
.refresh-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.refresh-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.refresh-btn svg.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.branch-info {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: var(--bg-hover);
font-size: 13px;
}
.branch-name {
font-weight: 500;
}
.ahead-behind {
font-size: 11px;
color: var(--text-muted);
}
.file-tree-container {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
/* Main content */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
/* Tab bar */
.tab-bar {
display: flex;
gap: 0.25rem;
padding: 0.5rem 1rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.tab {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.tab:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tab.active {
background: var(--accent);
color: white;
}
.tab-badge {
background: rgba(255, 255, 255, 0.2);
padding: 0.1rem 0.4rem;
border-radius: 10px;
font-size: 11px;
}
.tab.active .tab-badge {
background: rgba(255, 255, 255, 0.3);
}
/* Tab content */
.tab-content {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
/* File sections */
.file-section {
margin-bottom: 1rem;
}
.section-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
margin-bottom: 0.25rem;
}
.section-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.section-header.staged .section-dot {
background: #22c55e;
}
.section-header.unstaged .section-dot {
background: #eab308;
}
.section-header.untracked .section-dot {
background: #6b7280;
}
.section-count {
background: var(--bg-hover);
padding: 0.1rem 0.4rem;
border-radius: 10px;
font-size: 11px;
}
.file-list {
display: flex;
flex-direction: column;
}
.file-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
cursor: pointer;
border-radius: 4px;
transition: background 0.15s;
}
.file-row:hover {
background: var(--bg-hover);
}
.file-row.selected {
background: rgba(99, 102, 241, 0.15);
}
.file-row.untracked-file {
cursor: default;
opacity: 0.7;
}
.file-badge {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
flex-shrink: 0;
}
.file-badge.added {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.file-badge.modified {
background: rgba(234, 179, 8, 0.2);
color: #eab308;
}
.file-badge.deleted {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.file-badge.renamed {
background: rgba(168, 85, 247, 0.2);
color: #a855f7;
}
.file-badge.untracked {
background: rgba(107, 114, 128, 0.2);
color: #6b7280;
}
.file-name {
font-size: 13px;
font-family: 'SF Mono', 'Fira Code', monospace;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Diff section */
.diff-section {
margin-top: 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.diff-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.75rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.diff-file-name {
font-size: 13px;
font-family: 'SF Mono', 'Fira Code', monospace;
color: var(--text-primary);
}
.close-diff {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 0.25rem;
font-size: 14px;
}
.close-diff:hover {
color: var(--text-primary);
}
.diff-type {
font-size: 11px;
color: var(--text-muted);
flex: 1;
}
.diff-loading,
.diff-empty {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 2rem;
color: var(--text-muted);
font-size: 13px;
}
/* Loading and empty states */
.loading-state,
.empty-state,
.select-commit-hint,
.compare-hint {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 3rem;
color: var(--text-muted);
text-align: center;
}
.empty-state svg,
.select-commit-hint svg,
.compare-hint svg {
opacity: 0.5;
}
.empty-state p,
.select-commit-hint p,
.compare-hint p {
font-size: 15px;
color: var(--text-secondary);
margin: 0;
}
.empty-state span {
font-size: 13px;
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-color);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* History tab */
.history-layout {
display: grid;
grid-template-columns: 350px 1fr;
gap: 1rem;
height: 100%;
}
.commit-list-container {
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
background: var(--bg-secondary);
}
.commit-detail {
overflow-y: auto;
}
.commit-header {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
margin-bottom: 1rem;
}
.commit-header h3 {
margin: 0 0 0.5rem 0;
font-size: 16px;
}
.commit-meta {
display: flex;
gap: 1rem;
font-size: 12px;
color: var(--text-muted);
}
.commit-meta .sha {
font-family: 'SF Mono', 'Fira Code', monospace;
color: var(--accent);
}
.commit-body {
margin-top: 0.75rem;
color: var(--text-secondary);
font-size: 13px;
white-space: pre-wrap;
}
/* Compare tab */
.compare-header {
display: flex;
align-items: flex-end;
gap: 1rem;
margin-bottom: 1.5rem;
}
.compare-arrow {
padding-bottom: 0.5rem;
color: var(--text-muted);
font-size: 18px;
}
.compare-btn {
padding: 0.5rem 1.5rem;
background: var(--accent);
border: none;
border-radius: 6px;
color: white;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.compare-btn:hover:not(:disabled) {
filter: brightness(1.1);
}
.compare-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.compare-stats {
display: flex;
gap: 1.5rem;
padding: 0.75rem 1rem;
background: var(--bg-secondary);
border-radius: 8px;
margin-bottom: 1rem;
font-size: 13px;
}
.stat {
color: var(--text-secondary);
}
.stat.additions {
color: #22c55e;
}
.stat.deletions {
color: #ef4444;
}
/* Files tab */
.files-tab {
height: 100%;
}
.files-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 1rem;
height: 100%;
}
.file-tree-panel {
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-secondary);
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
}
.refresh-tree-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: transparent;
border: none;
border-radius: 4px;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s;
}
.refresh-tree-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.refresh-tree-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.refresh-tree-btn svg.spinning {
animation: spin 1s linear infinite;
}
.tree-container {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.file-content-panel {
display: flex;
flex-direction: column;
min-height: 0;
}
/* Error message */
.error-message {
padding: 0.75rem 1rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 6px;
color: #ef4444;
font-size: 13px;
margin: 1rem;
}
/* Responsive */
@media (max-width: 1024px) {
.sidebar {
width: 240px;
}
.history-layout {
grid-template-columns: 280px 1fr;
}
.files-layout {
grid-template-columns: 240px 1fr;
}
}
@media (max-width: 768px) {
.git-page {
flex-direction: column;
}
.sidebar {
width: 100%;
flex-shrink: 0;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
.sidebar-header {
padding: 0.5rem 0.75rem;
}
.sidebar-header h2 {
font-size: 14px;
}
.branch-info {
padding: 0.4rem 0.75rem;
font-size: 12px;
}
.file-tree-container {
display: none;
}
.main-content {
flex: 1;
min-height: 0;
overflow: hidden;
}
.tab-bar {
padding: 0.4rem 0.5rem;
gap: 0.25rem;
flex-shrink: 0;
}
.tab {
padding: 0.4rem 0.6rem;
font-size: 11px;
gap: 0.35rem;
}
.tab svg {
width: 12px;
height: 12px;
}
.tab-badge {
padding: 0 0.3rem;
font-size: 10px;
}
.file-section {
margin-bottom: 0.75rem;
}
.section-header {
padding: 0.4rem 0;
font-size: 11px;
}
.file-row {
padding: 0.35rem 0.4rem;
}
.file-badge {
width: 16px;
height: 16px;
font-size: 10px;
}
.file-name {
font-size: 12px;
}
.diff-header {
padding: 0.4rem 0.5rem;
}
.diff-file-name {
font-size: 11px;
}
.tab-content {
flex: 1;
padding: 0.5rem;
overflow-y: auto;
min-height: 0;
}
.history-layout {
display: flex;
flex-direction: column;
gap: 0.5rem;
height: 100%;
}
.commit-list-container {
max-height: 200px;
flex-shrink: 0;
}
.commit-detail {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.commit-header {
padding: 0.5rem;
}
.commit-header h3 {
font-size: 13px;
margin-bottom: 0.25rem;
}
.commit-meta {
gap: 0.5rem;
font-size: 11px;
}
.compare-header {
flex-direction: column;
gap: 0.5rem;
align-items: stretch;
}
.compare-arrow {
display: none;
}
.compare-btn {
width: 100%;
padding: 0.5rem;
}
.compare-stats {
padding: 0.5rem;
gap: 0.5rem;
font-size: 12px;
}
.empty-state,
.select-commit-hint,
.compare-hint {
padding: 1.5rem 0.75rem;
}
.empty-state svg,
.select-commit-hint svg,
.compare-hint svg {
width: 32px;
height: 32px;
}
.empty-state p,
.select-commit-hint p,
.compare-hint p {
font-size: 13px;
}
.empty-state span {
font-size: 12px;
}
.error-message {
margin: 0.5rem;
padding: 0.5rem;
font-size: 12px;
}
.files-layout {
display: flex;
flex-direction: column;
gap: 0.5rem;
height: 100%;
}
.file-tree-panel {
max-height: 250px;
flex-shrink: 0;
}
.panel-header {
padding: 0.5rem 0.75rem;
font-size: 12px;
}
.tree-container {
padding: 0.25rem;
}
.file-content-panel {
flex: 1;
min-height: 0;
}
}
</style>