feat: TextFileEditor for text/markdown files, fix menu positioning
- Create TextFileEditor.svelte: full editor/viewer for text files - Markdown: edit/preview/split modes with MarkdownEditor + MarkdownPreview - Plain text: readonly monospace viewer - Footer: external open button + save (markdown) + close - Rewrite _openFile in FilesTab: text/markdown → TextFileEditor, images/PDF → FilePreviewModal - Linked .md notes → note editor (via CheckFileAction) - Unlinked .md / text files → TextFileEditor (not FilePreviewModal) - Fix toggleMenu in FileTreeRow: position using getBoundingClientRect - Expand codeNames/textMimes for all text file types - Add debug logs for file open flow and menu positioning Co-Authored-By: OWL (Hermes Agent) <hermes@nousresearch.com>
This commit is contained in:
parent
82f59ab8da
commit
d7d806530b
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;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-WcQPBFFT.js"></script>
|
<script type="module" crossorigin src="/assets/main-DLhykySS.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-DKrNFx34.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-CPk9naW6.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
import FileBreadcrumbs from '../../FileBreadcrumbs.svelte'
|
import FileBreadcrumbs from '../../FileBreadcrumbs.svelte'
|
||||||
import FileTreeRow from '../../../FileTreeRow.svelte'
|
import FileTreeRow from '../../../FileTreeRow.svelte'
|
||||||
import FilePreviewModal from '../../FilePreviewModal.svelte'
|
import FilePreviewModal from '../../FilePreviewModal.svelte'
|
||||||
|
import TextFileEditor from './TextFileEditor.svelte'
|
||||||
import ConfirmModal from '../../ConfirmModal.svelte'
|
import ConfirmModal from '../../ConfirmModal.svelte'
|
||||||
import { canPreviewFile, needsBase64Preview, needsTextPreview, isMarkdownFile } from '../../fileUtils.js'
|
import { canPreviewFile, needsBase64Preview, isTextFile, isMarkdownFile } from '../../fileUtils.js'
|
||||||
import { t } from '../../i18n'
|
import { t } from '../../i18n'
|
||||||
|
|
||||||
// ===== Props =====
|
// ===== Props =====
|
||||||
|
|
@ -24,10 +25,17 @@
|
||||||
let currentFolderId = null
|
let currentFolderId = null
|
||||||
let folderStack = []
|
let folderStack = []
|
||||||
let fileItems = []
|
let fileItems = []
|
||||||
|
// Preview modal state (images, PDF, binary)
|
||||||
let previewItem = null
|
let previewItem = null
|
||||||
let previewContent = ''
|
let previewContent = ''
|
||||||
let previewLoading = false
|
let previewLoading = false
|
||||||
let previewError = ''
|
let previewError = ''
|
||||||
|
// Text file editor state (text, markdown, code)
|
||||||
|
let textEditorFile = null
|
||||||
|
let textEditorContent = ''
|
||||||
|
let textEditorLoading = false
|
||||||
|
let textEditorError = ''
|
||||||
|
let textEditorIsMarkdown = false
|
||||||
let clipboard = { items: [], mode: 'copy' }
|
let clipboard = { items: [], mode: 'copy' }
|
||||||
let selectedIds = []
|
let selectedIds = []
|
||||||
let dragIds = []
|
let dragIds = []
|
||||||
|
|
@ -79,7 +87,7 @@
|
||||||
// Find the file in the current fileItems and open preview
|
// Find the file in the current fileItems and open preview
|
||||||
const fileItem = fileItems.find(f => f.id === fileNodeId)
|
const fileItem = fileItems.find(f => f.id === fileNodeId)
|
||||||
if (fileItem) {
|
if (fileItem) {
|
||||||
_openPreview(fileItem)
|
_openFile(fileItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,32 +147,72 @@
|
||||||
_loadFolder(target.id)
|
_loadFolder(target.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== File preview =====
|
// ===== Open file: text editor or preview modal =====
|
||||||
async function _openPreview(item) {
|
async function _openFile(item) {
|
||||||
_fdl('openPreview item=' + JSON.stringify({id: item.id, name: item.name, type: item.type, fileId: item.fileId}))
|
_fdl('openFile item=' + JSON.stringify({id: item.id, name: item.name, type: item.type, fileId: item.fileId}))
|
||||||
// For .md files: check if linked to a note, open note editor instead
|
|
||||||
|
// 1. Markdown: check if linked to a note
|
||||||
if (item && item.fileId && isMarkdownFile(item)) {
|
if (item && item.fileId && isMarkdownFile(item)) {
|
||||||
_fdl('isMarkdown=true fileId=' + item.fileId)
|
|
||||||
try {
|
try {
|
||||||
const action = await wailsCall('CheckFileAction', item.fileId)
|
const action = await wailsCall('CheckFileAction', item.fileId)
|
||||||
_fdl('CheckFileAction result=' + JSON.stringify(action))
|
_fdl('CheckFileAction result=' + JSON.stringify(action))
|
||||||
if (action.action === 'note') {
|
if (action && action.action === 'note') {
|
||||||
|
// Linked note → open note editor
|
||||||
dispatch('openNote', { id: action.noteId, title: action.noteTitle })
|
dispatch('openNote', { id: action.noteId, title: action.noteTitle })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (action.action === 'external') {
|
// action === 'preview' → fall through to text editor below
|
||||||
await wailsCall('OpenFile', item.fileId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// action === 'preview' → fall through to built-in preview below
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_fdl('CheckFileAction ERROR: ' + String(e))
|
_fdl('CheckFileAction ERROR: ' + String(e))
|
||||||
console.warn('CheckFileAction failed, falling back to preview:', e)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_fdl('isMarkdown=false or no fileId, type=' + item.type)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Text/markdown files → TextFileEditor
|
||||||
|
if (item && item.fileId && (isTextFile(item) || isMarkdownFile(item))) {
|
||||||
|
_fdl('opening text editor for ' + item.name)
|
||||||
|
openTextEditor(item, isMarkdownFile(item))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Images / PDF / binary preview → FilePreviewModal
|
||||||
|
if (item && item.fileId && canPreviewFile(item)) {
|
||||||
|
_fdl('opening preview modal for ' + item.name)
|
||||||
|
openPreviewModal(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback: unknown type, try as text
|
||||||
|
if (item && item.fileId && item.type !== 'folder') {
|
||||||
|
_fdl('fallback: trying text editor for unknown type ' + item.name)
|
||||||
|
openTextEditor(item, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_fdl('no suitable opener for ' + item.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openTextEditor(item, isMarkdown) {
|
||||||
|
textEditorFile = item
|
||||||
|
textEditorContent = ''
|
||||||
|
textEditorError = ''
|
||||||
|
textEditorLoading = true
|
||||||
|
textEditorIsMarkdown = isMarkdown
|
||||||
|
try {
|
||||||
|
textEditorContent = await wailsCall('ReadFileText', item.fileId) || ''
|
||||||
|
} catch (e) {
|
||||||
|
textEditorError = String(e)
|
||||||
|
}
|
||||||
|
textEditorLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTextEditor() {
|
||||||
|
textEditorFile = null
|
||||||
|
textEditorContent = ''
|
||||||
|
textEditorError = ''
|
||||||
|
textEditorIsMarkdown = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPreviewModal(item) {
|
||||||
previewItem = item
|
previewItem = item
|
||||||
previewContent = ''
|
previewContent = ''
|
||||||
previewError = ''
|
previewError = ''
|
||||||
|
|
@ -172,12 +220,6 @@
|
||||||
try {
|
try {
|
||||||
if (needsBase64Preview(item)) {
|
if (needsBase64Preview(item)) {
|
||||||
previewContent = await wailsCall('GetFileBase64', item.fileId) || ''
|
previewContent = await wailsCall('GetFileBase64', item.fileId) || ''
|
||||||
} else if (needsTextPreview(item)) {
|
|
||||||
previewContent = await wailsCall('ReadFileText', item.fileId) || ''
|
|
||||||
} else if (item.fileId && item.type !== 'folder') {
|
|
||||||
// Fallback: try to read as text for unknown types
|
|
||||||
_fdl('fallback text read for item=' + item.name + ' mime=' + item.mime)
|
|
||||||
previewContent = await wailsCall('ReadFileText', item.fileId) || ''
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
previewError = String(e)
|
previewError = String(e)
|
||||||
|
|
@ -387,7 +429,7 @@
|
||||||
if (item.type === 'folder') {
|
if (item.type === 'folder') {
|
||||||
navigateToFolder(item.id)
|
navigateToFolder(item.id)
|
||||||
} else {
|
} else {
|
||||||
_openPreview(item)
|
_openFile(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -563,9 +605,11 @@
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
openSelected()
|
openSelected()
|
||||||
} else if (e.key === 'Delete') {
|
} else if (e.key === 'Delete') {
|
||||||
|
if (textEditorFile) { e.preventDefault(); closeTextEditor(); return }
|
||||||
if (previewItem) { e.preventDefault(); closePreview(); return }
|
if (previewItem) { e.preventDefault(); closePreview(); return }
|
||||||
if (selectedIds.length > 0) { e.preventDefault(); deleteSelected(); return }
|
if (selectedIds.length > 0) { e.preventDefault(); deleteSelected(); return }
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
|
if (textEditorFile) { closeTextEditor(); return }
|
||||||
if (previewItem) { closePreview(); return }
|
if (previewItem) { closePreview(); return }
|
||||||
if (selectedIds.length > 0) { clearSelection(); return }
|
if (selectedIds.length > 0) { clearSelection(); return }
|
||||||
} else if (e.key === 'F2') {
|
} else if (e.key === 'F2') {
|
||||||
|
|
@ -653,7 +697,7 @@
|
||||||
{onDragOver}
|
{onDragOver}
|
||||||
{onDrop}
|
{onDrop}
|
||||||
on:navigate={(e) => navigateToFolder(e.detail)}
|
on:navigate={(e) => navigateToFolder(e.detail)}
|
||||||
on:preview={(e) => _openPreview(e.detail)}
|
on:preview={(e) => _openFile(e.detail)}
|
||||||
on:openExternal={(e) => wailsCall('OpenFile', e.detail)}
|
on:openExternal={(e) => wailsCall('OpenFile', e.detail)}
|
||||||
on:showInFolder={(e) => wailsCall('OpenFolder', e.detail)}
|
on:showInFolder={(e) => wailsCall('OpenFolder', e.detail)}
|
||||||
on:delete={(e) => deleteFile(e.detail)}
|
on:delete={(e) => deleteFile(e.detail)}
|
||||||
|
|
@ -734,7 +778,24 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- File preview modal (inline in FilesTab) -->
|
<!-- 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) -->
|
||||||
{#if previewItem}
|
{#if previewItem}
|
||||||
<FilePreviewModal
|
<FilePreviewModal
|
||||||
item={previewItem}
|
item={previewItem}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,306 @@
|
||||||
|
<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