108 lines
2.9 KiB
Vue
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>
|