fix: inline file editor mode in FilesTab, remove separate TextFileEditor
- Replace separate TextFileEditor modal with inline editor mode in FilesTab - Editor mode replaces browser mode (file list) within the same .files-tab container - Markdown files: view/edit/split modes using existing MarkdownEditor + MarkdownPreview - Plain text files: readonly monospace viewer - Linked .md notes → note editor (via CheckFileAction) - Unlinked .md / text files → inline editor (not FilePreviewModal) - FilePreviewModal only for images, PDF, binary - Back button returns to file browser without losing folder state - Reset editor state on selectedNode change - Remove TextFileEditor.svelte (no longer needed) Co-Authored-By: OWL (Hermes Agent) <hermes@nousresearch.com> )
This commit is contained in:
parent
d7d806530b
commit
383a9546df
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -19,8 +19,8 @@
|
|||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-DLhykySS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CPk9naW6.css">
|
||||
<script type="module" crossorigin src="/assets/main-D23Sgy9Q.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CoirOqGf.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@
|
|||
import FileBreadcrumbs from '../../FileBreadcrumbs.svelte'
|
||||
import FileTreeRow from '../../../FileTreeRow.svelte'
|
||||
import FilePreviewModal from '../../FilePreviewModal.svelte'
|
||||
import TextFileEditor from './TextFileEditor.svelte'
|
||||
import ConfirmModal from '../../ConfirmModal.svelte'
|
||||
import { canPreviewFile, needsBase64Preview, isTextFile, isMarkdownFile } from '../../fileUtils.js'
|
||||
import { t } from '../../i18n'
|
||||
import MarkdownEditor from '../notes/MarkdownEditor.svelte'
|
||||
import MarkdownPreview from '../notes/MarkdownPreview.svelte'
|
||||
|
||||
// ===== Props =====
|
||||
export let selectedNode = null
|
||||
|
|
@ -36,6 +37,8 @@
|
|||
let textEditorLoading = false
|
||||
let textEditorError = ''
|
||||
let textEditorIsMarkdown = false
|
||||
let textEditorMode = 'view' // 'view' | 'edit' | 'split'
|
||||
let textEditorDirty = false
|
||||
let clipboard = { items: [], mode: 'copy' }
|
||||
let selectedIds = []
|
||||
let dragIds = []
|
||||
|
|
@ -64,6 +67,7 @@
|
|||
dragIds = []
|
||||
previewItem = null
|
||||
previewContent = ''
|
||||
closeTextEditor()
|
||||
loadFolder(nodeId)
|
||||
}
|
||||
|
||||
|
|
@ -210,6 +214,14 @@
|
|||
textEditorContent = ''
|
||||
textEditorError = ''
|
||||
textEditorIsMarkdown = false
|
||||
textEditorMode = 'view'
|
||||
textEditorDirty = false
|
||||
}
|
||||
|
||||
function saveTextEditor() {
|
||||
if (!textEditorFile || !textEditorDirty) return
|
||||
wailsCall('SaveFileText', textEditorFile.fileId, textEditorContent)
|
||||
textEditorDirty = false
|
||||
}
|
||||
|
||||
async function openPreviewModal(item) {
|
||||
|
|
@ -639,87 +651,145 @@
|
|||
|
||||
<!-- Files tab template -->
|
||||
<div class="files-tab">
|
||||
<div class="tab-toolbar">
|
||||
<button class="btn btn-primary" on:click={_addFile} disabled={importing}>{t('file.addFile')}</button>
|
||||
<button class="btn" on:click={_addFolder} disabled={importing}>{t('file.addFolder')}</button>
|
||||
<button class="btn" on:click={createFile}>{t('file.newFile')}</button>
|
||||
{#if clipboard.items.length > 0}
|
||||
<button class="btn" on:click={pasteItem}>{t('common.paste')} {clipboard.items.length}</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loadingFiles}
|
||||
<div class="empty-state">
|
||||
<p>{t('common.loading')}</p>
|
||||
{#if textEditorFile}
|
||||
<!-- ===== Editor mode ===== -->
|
||||
<div class="file-editor-mode">
|
||||
<div class="fe-toolbar">
|
||||
<button class="btn btn-sm back-btn" on:click={closeTextEditor}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||
{t('file.backToFiles')}
|
||||
</button>
|
||||
<span class="fe-filename" title={textEditorFile.name}>{textEditorFile.name}</span>
|
||||
{#if textEditorIsMarkdown}
|
||||
<span class="fe-badge">markdown</span>
|
||||
{/if}
|
||||
<div class="fe-spacer"></div>
|
||||
{#if textEditorIsMarkdown}
|
||||
<div class="fe-mode-switcher" role="tablist" aria-label="View mode">
|
||||
<button type="button" class="fe-mode-btn" class:active={textEditorMode === 'view'} on:click={() => textEditorMode = 'view'}>{t('note.mode.preview')}</button>
|
||||
<button type="button" class="fe-mode-btn" class:active={textEditorMode === 'edit'} on:click={() => textEditorMode = 'edit'}>{t('note.mode.edit')}</button>
|
||||
<button type="button" class="fe-mode-btn" class:active={textEditorMode === 'split'} on:click={() => textEditorMode = 'split'}>{t('note.mode.split')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
<button class="btn btn-sm" on:click={() => wailsCall('OpenFile', textEditorFile.fileId)}>{t('file.openExternal')}</button>
|
||||
</div>
|
||||
<div class="fe-body">
|
||||
{#if textEditorLoading}
|
||||
<div class="fe-status"><p>{t('common.loading')}</p></div>
|
||||
{:else if textEditorError}
|
||||
<div class="fe-status fe-error">
|
||||
<p>{textEditorError}</p>
|
||||
<button class="btn btn-sm" on:click={() => wailsCall('OpenFile', textEditorFile.fileId)}>{t('file.openExternal')}</button>
|
||||
</div>
|
||||
{:else if textEditorIsMarkdown && textEditorMode === 'view'}
|
||||
<div class="fe-markdown-preview">
|
||||
<svelte:component this={MarkdownPreview} content={textEditorContent} />
|
||||
</div>
|
||||
{:else if textEditorIsMarkdown && textEditorMode === 'edit'}
|
||||
<svelte:component this={MarkdownEditor} content={textEditorContent} viewMode="edit" on:content-change={(e) => { textEditorContent = e.detail.content; textEditorDirty = true }} on:save={() => saveTextEditor()} />
|
||||
{:else if textEditorIsMarkdown && textEditorMode === 'split'}
|
||||
<div class="fe-split">
|
||||
<div class="fe-split-pane fe-split-editor">
|
||||
<svelte:component this={MarkdownEditor} content={textEditorContent} viewMode="split" on:content-change={(e) => { textEditorContent = e.detail.content; textEditorDirty = true }} on:save={() => saveTextEditor()} />
|
||||
</div>
|
||||
<div class="fe-split-pane fe-split-preview">
|
||||
<svelte:component this={MarkdownPreview} content={textEditorContent} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<pre class="fe-plain-text"><code>{textEditorContent}</code></pre>
|
||||
{/if}
|
||||
</div>
|
||||
{#if textEditorDirty}
|
||||
<div class="fe-footer">
|
||||
<button class="btn btn-primary btn-sm" on:click={saveTextEditor}>{t('common.save')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{#if folderStack.length > 0}
|
||||
<FileBreadcrumbs crumbs={[{ name: t('file.root') }, ...folderStack]} on:navigate={(e) => {
|
||||
const i = e.detail
|
||||
if (i === 0) {
|
||||
folderStack = []
|
||||
currentFolderId = null
|
||||
if (selectedNode) _loadFolder(selectedNode.id)
|
||||
} else {
|
||||
navigateToBreadcrumb(i - 1)
|
||||
}
|
||||
}}/>
|
||||
<button class="btn btn-sm back-btn" on:click={navigateBack}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||
{t('common.backLabel')}
|
||||
</button>
|
||||
{:else}
|
||||
<FileBreadcrumbs crumbs={[{ name: t('file.root') }]}/>
|
||||
{/if}
|
||||
<!-- ===== Browser mode ===== -->
|
||||
<div class="tab-toolbar">
|
||||
<button class="btn btn-primary" on:click={_addFile} disabled={importing}>{t('file.addFile')}</button>
|
||||
<button class="btn" on:click={_addFolder} disabled={importing}>{t('file.addFolder')}</button>
|
||||
<button class="btn" on:click={createFile}>{t('file.newFile')}</button>
|
||||
{#if clipboard.items.length > 0}
|
||||
<button class="btn" on:click={pasteItem}>{t('common.paste')} {clipboard.items.length}</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if fileItems.length === 0}
|
||||
{#if loadingFiles}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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>
|
||||
</div>
|
||||
<p>{folderStack.length > 0 ? t('file.noFiles') : t('file.noFilesCase')}</p>
|
||||
<p class="hint">{t('file.hint')}</p>
|
||||
<div class="empty-actions">
|
||||
<button class="btn btn-primary" on:click={_addFile}>{t('file.addFileSimple')}</button>
|
||||
<button class="btn" on:click={_addFolder}>{t('file.addFolderSimple')}</button>
|
||||
</div>
|
||||
<p>{t('common.loading')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="file-list">
|
||||
{#each fileItems as item (item.id)}
|
||||
<FileTreeRow
|
||||
{item}
|
||||
selected={selectedIds.includes(item.id)}
|
||||
{onDragStart}
|
||||
{onDragOver}
|
||||
{onDrop}
|
||||
on:navigate={(e) => navigateToFolder(e.detail)}
|
||||
on:preview={(e) => _openFile(e.detail)}
|
||||
on:openExternal={(e) => wailsCall('OpenFile', e.detail)}
|
||||
on:showInFolder={(e) => wailsCall('OpenFolder', e.detail)}
|
||||
on:delete={(e) => deleteFile(e.detail)}
|
||||
on:rename={(e) => renameItem(e.detail.id)}
|
||||
on:duplicate={(e) => duplicateItem(e.detail)}
|
||||
on:cut={(e) => cutItem(e.detail)}
|
||||
on:copy={(e) => copyItem(e.detail)}
|
||||
on:selectOne={(e) => selectOne(e.detail)}
|
||||
on:toggleSelect={(e) => toggleSelection(e.detail)}
|
||||
on:rangeSelect={(e) => rangeSelect(e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if folderStack.length > 0}
|
||||
<FileBreadcrumbs crumbs={[{ name: t('file.root') }, ...folderStack]} on:navigate={(e) => {
|
||||
const i = e.detail
|
||||
if (i === 0) {
|
||||
folderStack = []
|
||||
currentFolderId = null
|
||||
if (selectedNode) _loadFolder(selectedNode.id)
|
||||
} else {
|
||||
navigateToBreadcrumb(i - 1)
|
||||
}
|
||||
}}/>
|
||||
<button class="btn btn-sm back-btn" on:click={navigateBack}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||
{t('common.backLabel')}
|
||||
</button>
|
||||
{:else}
|
||||
<FileBreadcrumbs crumbs={[{ name: t('file.root') }]}/>
|
||||
{/if}
|
||||
|
||||
{#if importing && !showImportDialog}
|
||||
<div class="empty-state"><p>{t('file.scanning')}</p></div>
|
||||
{#if fileItems.length === 0}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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>
|
||||
</div>
|
||||
<p>{folderStack.length > 0 ? t('file.noFiles') : t('file.noFilesCase')}</p>
|
||||
<p class="hint">{t('file.hint')}</p>
|
||||
<div class="empty-actions">
|
||||
<button class="btn btn-primary" on:click={_addFile}>{t('file.addFileSimple')}</button>
|
||||
<button class="btn" on:click={_addFolder}>{t('file.addFolderSimple')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="file-list">
|
||||
{#each fileItems as item (item.id)}
|
||||
<FileTreeRow
|
||||
{item}
|
||||
selected={selectedIds.includes(item.id)}
|
||||
{onDragStart}
|
||||
{onDragOver}
|
||||
{onDrop}
|
||||
on:navigate={(e) => navigateToFolder(e.detail)}
|
||||
on:preview={(e) => _openFile(e.detail)}
|
||||
on:openExternal={(e) => wailsCall('OpenFile', e.detail)}
|
||||
on:showInFolder={(e) => wailsCall('OpenFolder', e.detail)}
|
||||
on:delete={(e) => deleteFile(e.detail)}
|
||||
on:rename={(e) => renameItem(e.detail.id)}
|
||||
on:duplicate={(e) => duplicateItem(e.detail)}
|
||||
on:cut={(e) => cutItem(e.detail)}
|
||||
on:copy={(e) => copyItem(e.detail)}
|
||||
on:selectOne={(e) => selectOne(e.detail)}
|
||||
on:toggleSelect={(e) => toggleSelection(e.detail)}
|
||||
on:rangeSelect={(e) => rangeSelect(e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if importing && !showImportDialog}
|
||||
<div class="empty-state"><p>{t('file.scanning')}</p></div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Rename modal (inline in FilesTab) -->
|
||||
<!-- Rename modal -->
|
||||
{#if showRename}
|
||||
<div class="modal-overlay" role="button" tabindex="0" on:click|self={cancelRename} on:keydown={(e) => e.key === 'Escape' && cancelRename()}>
|
||||
<div class="modal">
|
||||
|
|
@ -741,7 +811,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Confirm modal (inline in FilesTab) -->
|
||||
<!-- Confirm modal -->
|
||||
{#if showConfirm}
|
||||
<ConfirmModal
|
||||
title={confirmTitle}
|
||||
|
|
@ -753,7 +823,7 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Import dialog (inline in FilesTab) -->
|
||||
<!-- Import dialog -->
|
||||
{#if showImportDialog && importSummary}
|
||||
<div class="modal-overlay" role="button" tabindex="0" on:click|self={cancelImport} on:keydown={(e) => e.key === 'Escape' && cancelImport()}>
|
||||
<div class="modal">
|
||||
|
|
@ -778,24 +848,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Text file editor (inline in FilesTab) -->
|
||||
{#if textEditorFile}
|
||||
<TextFileEditor
|
||||
file={textEditorFile}
|
||||
content={textEditorContent}
|
||||
isMarkdown={textEditorIsMarkdown}
|
||||
loading={textEditorLoading}
|
||||
error={textEditorError}
|
||||
on:close={closeTextEditor}
|
||||
on:openExternal={(e) => wailsCall('OpenFile', e.detail)}
|
||||
on:save={(e) => {
|
||||
wailsCall('SaveFileText', e.detail.fileId, e.detail.content)
|
||||
closeTextEditor()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- File preview modal (images, PDF, binary) -->
|
||||
<!-- File preview modal (images, PDF, binary only) -->
|
||||
{#if previewItem}
|
||||
<FilePreviewModal
|
||||
item={previewItem}
|
||||
|
|
@ -828,4 +881,155 @@
|
|||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
.btn-danger { color: #ff6b6b; border-color: #4a2222; }
|
||||
.btn-danger:hover { background: #3a2222; }
|
||||
|
||||
/* ===== File editor mode ===== */
|
||||
.file-editor-mode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.fe-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #2a2a3c;
|
||||
background: #16161f;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fe-filename {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #e4e4ef;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.fe-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: #2a2a3c;
|
||||
color: #888;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fe-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fe-mode-switcher {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.fe-mode-btn {
|
||||
padding: 3px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fe-mode-btn:hover {
|
||||
color: #ccc;
|
||||
background: #1e1e30;
|
||||
}
|
||||
|
||||
.fe-mode-btn.active {
|
||||
color: #e4e4ef;
|
||||
background: #22223a;
|
||||
border-color: #333350;
|
||||
}
|
||||
|
||||
.fe-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fe-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 48px 24px;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.fe-error {
|
||||
color: #ff8888;
|
||||
}
|
||||
|
||||
.fe-markdown-preview {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.fe-split {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fe-split-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fe-split-editor {
|
||||
border-right: 1px solid #2a2a3c;
|
||||
}
|
||||
|
||||
.fe-split-preview {
|
||||
background: #11111c;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.fe-plain-text {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
overflow: auto;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #ccc;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: #13131f;
|
||||
}
|
||||
|
||||
.fe-plain-text code {
|
||||
font-family: inherit;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.fe-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #2a2a3c;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,306 +0,0 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import MarkdownEditor from '../notes/MarkdownEditor.svelte'
|
||||
import MarkdownPreview from '../notes/MarkdownPreview.svelte'
|
||||
import { t } from '../../i18n'
|
||||
import { formatFileSize, formatMimeType } from '../../fileUtils.js'
|
||||
|
||||
export let file = null
|
||||
export let content = ''
|
||||
export let isMarkdown = false
|
||||
export let loading = false
|
||||
export let error = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let viewMode = 'edit' // 'edit' | 'preview' | 'split'
|
||||
let dirty = false
|
||||
let activeEditor = undefined
|
||||
|
||||
function handleContentChange(e) {
|
||||
content = e.detail.content
|
||||
dirty = true
|
||||
dispatch('content-change', e.detail)
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (!file) return
|
||||
dispatch('save', { fileId: file.fileId, content })
|
||||
dirty = false
|
||||
}
|
||||
|
||||
function handleOpenExternal() {
|
||||
if (file) dispatch('openExternal', file.fileId)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
dispatch('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="text-editor">
|
||||
<header class="te-header">
|
||||
<div class="te-title-row">
|
||||
<span class="te-name" title={file ? file.name : ''}>{file ? file.name : ''}</span>
|
||||
{#if dirty}<span class="te-dirty" title="Unsaved changes">●</span>{/if}
|
||||
{#if isMarkdown}
|
||||
<span class="te-badge">markdown</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="te-meta">
|
||||
{#if file}
|
||||
<span>{formatFileSize(file.size)}</span>
|
||||
<span class="te-sep">·</span>
|
||||
<span>{formatMimeType(file.mime)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isMarkdown}
|
||||
<div class="te-mode-switcher" role="tablist" aria-label="View mode">
|
||||
<button type="button" class="te-mode-btn" role="tab" class:active={viewMode === 'edit'} on:click={() => viewMode = 'edit'}>
|
||||
{t('note.mode.edit')}
|
||||
</button>
|
||||
<button type="button" class="te-mode-btn" role="tab" class:active={viewMode === 'preview'} on:click={() => viewMode = 'preview'}>
|
||||
{t('note.mode.preview')}
|
||||
</button>
|
||||
<button type="button" class="te-mode-btn" role="tab" class:active={viewMode === 'split'} on:click={() => viewMode = 'split'}>
|
||||
{t('note.mode.split')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="te-body">
|
||||
{#if loading}
|
||||
<div class="te-status"><p>{t('common.loading')}</p></div>
|
||||
{:else if error}
|
||||
<div class="te-status te-error">
|
||||
<p>{error}</p>
|
||||
<button class="btn btn-sm" on:click={handleOpenExternal}>{t('file.openExternal')}</button>
|
||||
</div>
|
||||
{:else if isMarkdown && viewMode === 'edit'}
|
||||
<MarkdownEditor
|
||||
bind:this={activeEditor}
|
||||
{content}
|
||||
viewMode="edit"
|
||||
on:content-change={handleContentChange}
|
||||
on:save={handleSave}
|
||||
/>
|
||||
{:else if isMarkdown && viewMode === 'preview'}
|
||||
<div class="te-preview-pane">
|
||||
<MarkdownPreview {content} />
|
||||
</div>
|
||||
{:else if isMarkdown && viewMode === 'split'}
|
||||
<div class="te-split-pane">
|
||||
<div class="te-split-editor">
|
||||
<MarkdownEditor
|
||||
bind:this={activeEditor}
|
||||
{content}
|
||||
viewMode="split"
|
||||
on:content-change={handleContentChange}
|
||||
on:save={handleSave}
|
||||
/>
|
||||
</div>
|
||||
<div class="te-split-preview">
|
||||
<MarkdownPreview {content} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Plain text viewer (readonly) -->
|
||||
<pre class="te-text-plain"><code>{content}</code></pre>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<footer class="te-footer">
|
||||
<button class="btn btn-sm" on:click={handleOpenExternal}>{t('file.openExternal')}</button>
|
||||
<div class="te-footer-right">
|
||||
{#if isMarkdown}
|
||||
<button class="btn btn-sm btn-primary" on:click={handleSave} disabled={!dirty}>{t('common.save')}</button>
|
||||
{/if}
|
||||
<button class="btn btn-sm" on:click={handleClose}>{t('common.close')}</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.text-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #13131f;
|
||||
}
|
||||
|
||||
.te-header {
|
||||
padding: 10px 16px 8px;
|
||||
border-bottom: 1px solid #2a2a3c;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.te-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.te-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #e4e4ef;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.te-dirty {
|
||||
color: #f59e0b;
|
||||
font-size: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.te-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: #2a2a3c;
|
||||
color: #888;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.te-meta {
|
||||
font-size: 11px;
|
||||
color: #555;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.te-sep {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.te-mode-switcher {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.te-mode-btn {
|
||||
padding: 3px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.te-mode-btn:hover {
|
||||
color: #ccc;
|
||||
background: #1e1e30;
|
||||
}
|
||||
|
||||
.te-mode-btn.active {
|
||||
color: #e4e4ef;
|
||||
background: #22223a;
|
||||
border-color: #333350;
|
||||
}
|
||||
|
||||
.te-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.te-preview-pane {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.te-split-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.te-split-editor {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
border-right: 1px solid #2a2a3c;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.te-split-preview {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
background: #11111c;
|
||||
}
|
||||
|
||||
.te-text-plain {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
overflow: auto;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #ccc;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: #13131f;
|
||||
}
|
||||
|
||||
.te-text-plain code {
|
||||
font-family: inherit;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.te-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 48px 24px;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.te-error {
|
||||
color: #ff8888;
|
||||
}
|
||||
|
||||
.te-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid #2a2a3c;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.te-footer-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.btn:hover { background: #222233; }
|
||||
.btn-primary { background: #6366f1; border-color: #6366f1; color: #fff; }
|
||||
.btn-primary:hover { background: #4f46e5; }
|
||||
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
</style>
|
||||
Loading…
Reference in New Issue