wraith/frontend/components/sftp/FileEditor.vue
Vantz Stockwell c8868258d5 feat: Phase 2 — SSH terminal + SFTP sidebar in browser
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 17:21:11 -04:00

108 lines
2.9 KiB
Vue

<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
const props = defineProps<{
filePath: string
content: string
}>()
const emit = defineEmits<{
(e: 'save', path: string, content: string): void
(e: 'close'): void
}>()
const editorContainer = ref<HTMLElement | null>(null)
const currentContent = ref(props.content)
const isDirty = ref(false)
let editor: any = null
watch(() => props.content, (val) => {
currentContent.value = val
if (editor) {
editor.setValue(val)
isDirty.value = false
}
})
onMounted(async () => {
if (!editorContainer.value) return
// Monaco is browser-only and heavy — dynamic import
const monaco = await import('monaco-editor')
// Detect language from file extension
const ext = props.filePath.split('.').pop() || ''
const langMap: Record<string, string> = {
ts: 'typescript', js: 'javascript', json: 'json', py: 'python',
sh: 'shell', bash: 'shell', yml: 'yaml', yaml: 'yaml',
md: 'markdown', html: 'html', css: 'css', xml: 'xml',
go: 'go', rs: 'rust', rb: 'ruby', php: 'php',
}
editor = monaco.editor.create(editorContainer.value, {
value: props.content,
language: langMap[ext] || 'plaintext',
theme: 'vs-dark',
fontSize: 13,
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
})
editor.onDidChangeModelContent(() => {
isDirty.value = editor.getValue() !== props.content
})
// Ctrl+S to save
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
handleSave()
})
})
onBeforeUnmount(() => {
editor?.dispose()
})
function handleSave() {
if (editor) {
emit('save', props.filePath, editor.getValue())
isDirty.value = false
}
}
function handleClose() {
if (isDirty.value) {
if (!confirm('You have unsaved changes. Close anyway?')) return
}
emit('close')
}
</script>
<template>
<div class="flex flex-col h-full bg-gray-900">
<!-- Editor toolbar -->
<div class="flex items-center justify-between px-3 py-1.5 bg-gray-800 border-b border-gray-700 shrink-0">
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500 font-mono truncate max-w-xs">{{ filePath }}</span>
<span v-if="isDirty" class="text-xs text-amber-400">● unsaved</span>
</div>
<div class="flex items-center gap-2">
<button
@click="handleSave"
class="text-xs px-2 py-1 bg-wraith-600 hover:bg-wraith-700 text-white rounded"
:disabled="!isDirty"
:class="!isDirty ? 'opacity-40 cursor-default' : ''"
>Save</button>
<button
@click="handleClose"
class="text-xs px-2 py-1 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded"
>Close</button>
</div>
</div>
<!-- Monaco editor mount point -->
<div ref="editorContainer" class="flex-1" />
</div>
</template>