Includes FileTree, CommitList, BranchSelector and DiffViewer components, Git API routes, and mobile keyboard visibility handling for FAB buttons
311 lines
6.2 KiB
Vue
311 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import type { FileDiff, DiffHunk } from '@/types/git'
|
|
|
|
const props = defineProps<{
|
|
files: FileDiff[]
|
|
expandedFiles?: Set<string>
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
toggleFile: [path: string]
|
|
}>()
|
|
|
|
function isExpanded(path: string): boolean {
|
|
return props.expandedFiles?.has(path) ?? true
|
|
}
|
|
|
|
function getStatusBadge(status: string) {
|
|
const badges: Record<string, { label: string; class: string }> = {
|
|
added: { label: 'A', class: 'badge-added' },
|
|
modified: { label: 'M', class: 'badge-modified' },
|
|
deleted: { label: 'D', class: 'badge-deleted' },
|
|
renamed: { label: 'R', class: 'badge-renamed' }
|
|
}
|
|
return badges[status] || { label: '?', class: '' }
|
|
}
|
|
|
|
function getLineNumbers(hunk: DiffHunk) {
|
|
let oldLine = hunk.oldStart
|
|
let newLine = hunk.newStart
|
|
|
|
return hunk.lines.map(line => {
|
|
const result = {
|
|
old: line.type === 'add' ? '' : oldLine,
|
|
new: line.type === 'delete' ? '' : newLine
|
|
}
|
|
if (line.type !== 'add') oldLine++
|
|
if (line.type !== 'delete') newLine++
|
|
return result
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="diff-viewer">
|
|
<div v-if="files.length === 0" class="empty-state">
|
|
No hay cambios para mostrar
|
|
</div>
|
|
|
|
<div v-for="file in files" :key="file.path" class="file-diff">
|
|
<div class="file-header" @click="emit('toggleFile', file.path)">
|
|
<span :class="['status-badge', getStatusBadge(file.status).class]">
|
|
{{ getStatusBadge(file.status).label }}
|
|
</span>
|
|
<span class="file-path">{{ file.path }}</span>
|
|
<span v-if="file.oldPath" class="old-path">
|
|
(from {{ file.oldPath }})
|
|
</span>
|
|
<svg v-if="file.isBinary" class="binary-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="3" width="20" height="18" rx="2" />
|
|
<path d="M8 7v10M12 7v10M16 7v10" />
|
|
</svg>
|
|
<span class="toggle-icon">
|
|
{{ isExpanded(file.path) ? '▼' : '▶' }}
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="isExpanded(file.path) && !file.isBinary" class="file-content">
|
|
<div v-for="(hunk, hunkIdx) in file.hunks" :key="hunkIdx" class="hunk">
|
|
<div class="hunk-header">
|
|
@@ -{{ hunk.oldStart }},{{ hunk.oldLines }} +{{ hunk.newStart }},{{ hunk.newLines }} @@
|
|
<span v-if="hunk.header" class="hunk-context">{{ hunk.header }}</span>
|
|
</div>
|
|
<div class="hunk-lines">
|
|
<div
|
|
v-for="(line, lineIdx) in hunk.lines"
|
|
:key="lineIdx"
|
|
:class="['diff-line', `line-${line.type}`]"
|
|
>
|
|
<span class="line-number old">{{ getLineNumbers(hunk)[lineIdx].old }}</span>
|
|
<span class="line-number new">{{ getLineNumbers(hunk)[lineIdx].new }}</span>
|
|
<span class="line-prefix">{{ line.type === 'add' ? '+' : line.type === 'delete' ? '-' : ' ' }}</span>
|
|
<span class="line-content">{{ line.content }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="file.isBinary" class="binary-notice">
|
|
Archivo binario - no se puede mostrar diff
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.diff-viewer {
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.empty-state {
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.file-diff {
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
margin-bottom: 1rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.file-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--bg-secondary);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.file-header:hover {
|
|
background: var(--bg-hover);
|
|
}
|
|
|
|
.status-badge {
|
|
width: 20px;
|
|
height: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.badge-added {
|
|
background: rgba(34, 197, 94, 0.2);
|
|
color: #22c55e;
|
|
}
|
|
|
|
.badge-modified {
|
|
background: rgba(234, 179, 8, 0.2);
|
|
color: #eab308;
|
|
}
|
|
|
|
.badge-deleted {
|
|
background: rgba(239, 68, 68, 0.2);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.badge-renamed {
|
|
background: rgba(168, 85, 247, 0.2);
|
|
color: #a855f7;
|
|
}
|
|
|
|
.file-path {
|
|
color: var(--text-primary);
|
|
flex: 1;
|
|
}
|
|
|
|
.old-path {
|
|
color: var(--text-muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.binary-icon {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.toggle-icon {
|
|
color: var(--text-muted);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.file-content {
|
|
background: var(--bg-primary);
|
|
}
|
|
|
|
.hunk {
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
.hunk-header {
|
|
padding: 0.5rem 1rem;
|
|
background: var(--bg-hover);
|
|
color: var(--accent);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.hunk-context {
|
|
color: var(--text-muted);
|
|
margin-left: 0.5rem;
|
|
}
|
|
|
|
.hunk-lines {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.diff-line {
|
|
display: flex;
|
|
white-space: pre;
|
|
}
|
|
|
|
.line-number {
|
|
min-width: 45px;
|
|
padding: 0 0.5rem;
|
|
text-align: right;
|
|
color: var(--text-muted);
|
|
background: var(--bg-secondary);
|
|
user-select: none;
|
|
border-right: 1px solid var(--border-color);
|
|
}
|
|
|
|
.line-number.old {
|
|
border-right: none;
|
|
}
|
|
|
|
.line-prefix {
|
|
width: 20px;
|
|
text-align: center;
|
|
user-select: none;
|
|
}
|
|
|
|
.line-content {
|
|
flex: 1;
|
|
padding-right: 1rem;
|
|
}
|
|
|
|
.line-context {
|
|
background: transparent;
|
|
}
|
|
|
|
.line-add {
|
|
background: rgba(34, 197, 94, 0.1);
|
|
}
|
|
|
|
.line-add .line-prefix,
|
|
.line-add .line-content {
|
|
color: #22c55e;
|
|
}
|
|
|
|
.line-add .line-number.new {
|
|
background: rgba(34, 197, 94, 0.15);
|
|
}
|
|
|
|
.line-delete {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
}
|
|
|
|
.line-delete .line-prefix,
|
|
.line-delete .line-content {
|
|
color: #ef4444;
|
|
}
|
|
|
|
.line-delete .line-number.old {
|
|
background: rgba(239, 68, 68, 0.15);
|
|
}
|
|
|
|
.binary-notice {
|
|
padding: 1rem;
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
font-style: italic;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.diff-viewer {
|
|
font-size: 11px;
|
|
}
|
|
|
|
.file-header {
|
|
padding: 0.5rem 0.75rem;
|
|
}
|
|
|
|
.file-path {
|
|
font-size: 12px;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.status-badge {
|
|
width: 18px;
|
|
height: 18px;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.hunk-header {
|
|
padding: 0.4rem 0.75rem;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.line-number {
|
|
min-width: 32px;
|
|
padding: 0 0.25rem;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.line-prefix {
|
|
width: 16px;
|
|
}
|
|
|
|
.line-content {
|
|
padding-right: 0.5rem;
|
|
}
|
|
}
|
|
</style>
|