329 lines
7.1 KiB
Vue
329 lines
7.1 KiB
Vue
<template>
|
||
<div
|
||
v-if="fileContent"
|
||
class="content-viewer"
|
||
:class="{
|
||
'dark-mode': preferences.darkMode,
|
||
'line-wrap': preferences.lineWrap
|
||
}"
|
||
>
|
||
<RecycleScroller
|
||
v-if="displayLines.length > 1000"
|
||
ref="virtualScroller"
|
||
class="scroller"
|
||
:items="displayLines"
|
||
:item-size="lineHeight"
|
||
key-field="lineNumber"
|
||
>
|
||
<template #default="{ item }">
|
||
<div
|
||
:data-line="item.lineNumber"
|
||
:class="[
|
||
'line',
|
||
{ selected: selectedLines.has(item.lineNumber) },
|
||
{ 'highlight-match': item.hasMatch },
|
||
{ 'current-result': isCurrentResult(item.lineNumber) }
|
||
]"
|
||
@click="toggleLineSelection(item.lineNumber, $event)"
|
||
>
|
||
<span v-if="preferences.showLineNumbers" class="line-number">
|
||
{{ item.lineNumber }}
|
||
</span>
|
||
<pre
|
||
v-highlight="highlightConfig"
|
||
><code>{{ item.content }}</code></pre>
|
||
</div>
|
||
</template>
|
||
</RecycleScroller>
|
||
|
||
<div v-else class="lines-container">
|
||
<div
|
||
v-for="line in displayLines"
|
||
:key="line.lineNumber"
|
||
:data-line="line.lineNumber"
|
||
:class="[
|
||
'line',
|
||
{ selected: selectedLines.has(line.lineNumber) },
|
||
{ 'highlight-match': line.hasMatch },
|
||
{ 'current-result': isCurrentResult(line.lineNumber) }
|
||
]"
|
||
@click="toggleLineSelection(line.lineNumber, $event)"
|
||
>
|
||
<span v-if="preferences.showLineNumbers" class="line-number">
|
||
{{ line.lineNumber }}
|
||
</span>
|
||
<pre v-highlight="highlightConfig"><code>{{ line.content }}</code></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="fileTooLarge" class="warning-banner">
|
||
⚠️ File exceeds 10,000 lines. Showing first 10,000 lines only. Use search
|
||
or filters to find specific content.
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, toRef, ref } from 'vue';
|
||
import { RecycleScroller } from 'vue-virtual-scroller';
|
||
import { useLineSelection } from '../../composables/useLineSelection.js';
|
||
|
||
const props = defineProps({
|
||
displayLines: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
fileContent: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
selectedFile: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
fileTooLarge: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
preferences: {
|
||
type: Object,
|
||
default: () => ({
|
||
darkMode: false,
|
||
lineWrap: false,
|
||
showLineNumbers: true
|
||
})
|
||
},
|
||
searchResults: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
currentResultIndex: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
highlightConfig: {
|
||
type: Object,
|
||
default: () => ({
|
||
theme: 'github',
|
||
language: 'json'
|
||
})
|
||
},
|
||
lineHeight: {
|
||
type: Number,
|
||
default: 20
|
||
},
|
||
selectionState: {
|
||
type: Object,
|
||
default: null
|
||
}
|
||
});
|
||
|
||
const displayLines = toRef(props, 'displayLines');
|
||
const fileContent = toRef(props, 'fileContent');
|
||
const selectedFile = toRef(props, 'selectedFile');
|
||
|
||
const virtualScroller = ref(null);
|
||
|
||
const internalSelectionState = useLineSelection(
|
||
displayLines,
|
||
fileContent,
|
||
selectedFile
|
||
);
|
||
const activeSelectionState = computed(
|
||
() => props.selectionState || internalSelectionState
|
||
);
|
||
const { selectedLines, toggleLineSelection } = activeSelectionState.value;
|
||
|
||
const isCurrentResult = lineNumber => {
|
||
if (!props.searchResults.length) return false;
|
||
const currentLine = props.searchResults[props.currentResultIndex] + 1;
|
||
return lineNumber === currentLine;
|
||
};
|
||
|
||
defineExpose({
|
||
selectedLines,
|
||
toggleLineSelection,
|
||
virtualScroller
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.content-viewer {
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
max-height: 70vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.content-viewer.dark-mode {
|
||
background: #1e1e1e;
|
||
border-color: #333;
|
||
color: #eee;
|
||
}
|
||
|
||
.content-viewer.line-wrap pre {
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.scroller,
|
||
.lines-container {
|
||
max-height: 65vh;
|
||
overflow-y: auto;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.content-viewer.dark-mode .scroller,
|
||
.content-viewer.dark-mode .lines-container {
|
||
background: #1e1e1e;
|
||
}
|
||
|
||
.line {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 2px 8px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
background: #ffffff !important;
|
||
}
|
||
|
||
.content-viewer.dark-mode .line {
|
||
background: #1e1e1e !important;
|
||
}
|
||
|
||
.line.selected {
|
||
background: rgba(102, 126, 234, 0.15) !important;
|
||
}
|
||
|
||
.content-viewer.dark-mode .line.selected {
|
||
background: rgba(102, 126, 234, 0.25) !important;
|
||
}
|
||
|
||
.line.highlight-match {
|
||
background: rgba(255, 234, 0, 0.25) !important;
|
||
}
|
||
|
||
.content-viewer.dark-mode .line.highlight-match {
|
||
background: rgba(255, 234, 0, 0.15) !important;
|
||
}
|
||
|
||
.line.current-result {
|
||
outline: 2px solid #ff9800;
|
||
background: rgba(255, 193, 7, 0.3) !important;
|
||
}
|
||
|
||
.content-viewer.dark-mode .line.current-result {
|
||
background: rgba(255, 193, 7, 0.2) !important;
|
||
}
|
||
|
||
.line-number {
|
||
width: 50px;
|
||
text-align: right;
|
||
color: #999;
|
||
user-select: none;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.content-viewer.dark-mode .line-number {
|
||
color: #666;
|
||
}
|
||
|
||
/* Force transparent backgrounds on all elements inside lines */
|
||
.line * {
|
||
background: transparent !important;
|
||
}
|
||
|
||
.line pre {
|
||
margin: 0;
|
||
background: transparent !important;
|
||
color: inherit;
|
||
}
|
||
|
||
.line pre code {
|
||
background: transparent !important;
|
||
color: inherit;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
/* Light mode syntax highlighting */
|
||
.content-viewer:not(.dark-mode) .line pre code {
|
||
color: #24292e !important;
|
||
}
|
||
|
||
.content-viewer:not(.dark-mode) :deep(.hljs) {
|
||
background: transparent !important;
|
||
color: #24292e !important;
|
||
}
|
||
|
||
.content-viewer:not(.dark-mode) :deep(.hljs-attr),
|
||
.content-viewer:not(.dark-mode) :deep(.hljs-attribute) {
|
||
color: #6f42c1 !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.content-viewer:not(.dark-mode) :deep(.hljs-string) {
|
||
color: #032f62 !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.content-viewer:not(.dark-mode) :deep(.hljs-number) {
|
||
color: #005a9c !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.content-viewer:not(.dark-mode) :deep(.hljs-literal) {
|
||
color: #d73a49 !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
/* Dark mode syntax highlighting */
|
||
.content-viewer.dark-mode :deep(.hljs) {
|
||
background: transparent !important;
|
||
color: #e1e4e8 !important;
|
||
}
|
||
|
||
.content-viewer.dark-mode :deep(.hljs-attr),
|
||
.content-viewer.dark-mode :deep(.hljs-attribute) {
|
||
color: #79b8ff !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.content-viewer.dark-mode :deep(.hljs-string) {
|
||
color: #85e89d !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.content-viewer.dark-mode :deep(.hljs-number) {
|
||
color: #79b8ff !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.content-viewer.dark-mode :deep(.hljs-literal) {
|
||
color: #f97583 !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.warning-banner {
|
||
background: #fff3cd;
|
||
border-top: 1px solid #ffc107;
|
||
color: #856404;
|
||
padding: 12px;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.content-viewer.dark-mode .warning-banner {
|
||
background: #3d3400;
|
||
color: #ffeb3b;
|
||
border-color: #ffc107;
|
||
}
|
||
|
||
.warning-banner {
|
||
padding: 8px 10px;
|
||
font-size: 13px;
|
||
background: #fff3cd;
|
||
border-top: 1px solid #ffeeba;
|
||
color: #856404;
|
||
}
|
||
</style>
|