Initial commit: MarkLite Markdown Reader
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:;">
|
||||
<title>MarkLite</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="../lib/highlight-github.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Toolbar -->
|
||||
<div id="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<button id="btn-open" class="toolbar-btn" title="打开文件 (Ctrl+O)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<span>打开</span>
|
||||
</button>
|
||||
<button id="btn-save" class="toolbar-btn" title="保存文件 (Ctrl+S)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
||||
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
||||
<polyline points="7 3 7 8 15 8"></polyline>
|
||||
</svg>
|
||||
<span>保存</span>
|
||||
</button>
|
||||
<div class="toolbar-divider"></div>
|
||||
<button id="btn-split" class="toolbar-btn active" title="编辑+预览 (Ctrl+1)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="12" y1="3" x2="12" y2="21"></line>
|
||||
</svg>
|
||||
<span>分屏</span>
|
||||
</button>
|
||||
<button id="btn-editor" class="toolbar-btn" title="纯编辑 (Ctrl+2)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||
</svg>
|
||||
<span>编辑</span>
|
||||
</button>
|
||||
<button id="btn-preview" class="toolbar-btn" title="纯预览 (Ctrl+3)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
<span>预览</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div id="main-content">
|
||||
<!-- Editor panel -->
|
||||
<div id="editor-panel">
|
||||
<div id="editor-wrapper">
|
||||
<div id="line-numbers"></div>
|
||||
<textarea id="editor" spellcheck="false" placeholder="在此输入 Markdown 内容,或拖拽 .md 文件到窗口打开..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resizer -->
|
||||
<div id="resizer"></div>
|
||||
|
||||
<!-- Preview panel -->
|
||||
<div id="preview-panel">
|
||||
<div id="preview" class="markdown-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status bar -->
|
||||
<div id="statusbar">
|
||||
<div class="status-left">
|
||||
<span id="status-text">就绪</span>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<span id="status-encoding">UTF-8</span>
|
||||
<span class="status-divider">|</span>
|
||||
<span id="status-lang">Markdown</span>
|
||||
<span class="status-divider">|</span>
|
||||
<span id="status-cursor">行 1, 列 1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drop overlay -->
|
||||
<div id="drop-overlay" class="hidden">
|
||||
<div class="drop-content">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<p>释放文件以打开</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Welcome screen -->
|
||||
<div id="welcome-screen">
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-icon">
|
||||
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="10" y="5" width="80" height="90" rx="8" fill="#f0f6ff" stroke="#1a73e8" stroke-width="2"/>
|
||||
<text x="50" y="45" text-anchor="middle" fill="#1a73e8" font-family="system-ui" font-weight="bold" font-size="32">M↓</text>
|
||||
<text x="50" y="70" text-anchor="middle" fill="#5f6368" font-family="system-ui" font-size="12">MarkLite</text>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>欢迎使用 MarkLite</h1>
|
||||
<p>一款轻量级的 Markdown 阅读器</p>
|
||||
<div class="welcome-actions">
|
||||
<button id="btn-welcome-open" class="welcome-btn primary">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
打开文件
|
||||
</button>
|
||||
<button id="btn-welcome-new" class="welcome-btn secondary">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
新建文件
|
||||
</button>
|
||||
</div>
|
||||
<div class="welcome-tips">
|
||||
<p>💡 提示:可以直接拖拽 .md 文件到窗口打开</p>
|
||||
<p>⌨️ 快捷键:Ctrl+O 打开 | Ctrl+S 保存 | Ctrl+1/2/3 切换视图</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../lib/marked.min.js"></script>
|
||||
<script src="../lib/highlight.min.js"></script>
|
||||
<script src="renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,441 @@
|
||||
// MarkLite Renderer Process
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ===== DOM Elements =====
|
||||
const editor = document.getElementById('editor');
|
||||
const preview = document.getElementById('preview');
|
||||
const lineNumbers = document.getElementById('line-numbers');
|
||||
const welcomeScreen = document.getElementById('welcome-screen');
|
||||
const dropOverlay = document.getElementById('drop-overlay');
|
||||
const statusText = document.getElementById('status-text');
|
||||
const statusCursor = document.getElementById('status-cursor');
|
||||
const resizer = document.getElementById('resizer');
|
||||
const editorPanel = document.getElementById('editor-panel');
|
||||
const previewPanel = document.getElementById('preview-panel');
|
||||
const mainContent = document.getElementById('main-content');
|
||||
|
||||
// Buttons
|
||||
const btnOpen = document.getElementById('btn-open');
|
||||
const btnSave = document.getElementById('btn-save');
|
||||
const btnSplit = document.getElementById('btn-split');
|
||||
const btnEditor = document.getElementById('btn-editor');
|
||||
const btnPreview = document.getElementById('btn-preview');
|
||||
const btnWelcomeOpen = document.getElementById('btn-welcome-open');
|
||||
const btnWelcomeNew = document.getElementById('btn-welcome-new');
|
||||
|
||||
// ===== State =====
|
||||
let currentFilePath = null;
|
||||
let currentContent = '';
|
||||
let viewMode = 'split'; // split, editor, preview
|
||||
let isModified = false;
|
||||
let updateTimer = null;
|
||||
|
||||
// ===== Initialize marked.js =====
|
||||
function initMarked() {
|
||||
if (typeof marked !== 'undefined') {
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
pedantic: false,
|
||||
highlight: function (code, lang) {
|
||||
if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
// Auto detect if no language specified
|
||||
if (typeof hljs !== 'undefined') {
|
||||
try {
|
||||
return hljs.highlightAuto(code).value;
|
||||
} catch (e) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Markdown Rendering =====
|
||||
function renderMarkdown(text) {
|
||||
if (typeof marked === 'undefined') {
|
||||
preview.innerHTML = '<p style="color: red;">错误: Markdown 解析库未加载</p>';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const html = marked.parse(text);
|
||||
preview.innerHTML = html;
|
||||
} catch (e) {
|
||||
preview.innerHTML = '<p style="color: red;">渲染错误: ' + e.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Line Numbers =====
|
||||
function updateLineNumbers() {
|
||||
const lines = editor.value.split('\n');
|
||||
const count = lines.length;
|
||||
let html = '';
|
||||
for (let i = 1; i <= count; i++) {
|
||||
html += '<div class="line-num">' + i + '</div>';
|
||||
}
|
||||
lineNumbers.innerHTML = html;
|
||||
}
|
||||
|
||||
// ===== Cursor Position =====
|
||||
function updateCursorPosition() {
|
||||
const text = editor.value;
|
||||
const pos = editor.selectionStart;
|
||||
const textBeforeCursor = text.substring(0, pos);
|
||||
const lines = textBeforeCursor.split('\n');
|
||||
const line = lines.length;
|
||||
const col = lines[lines.length - 1].length + 1;
|
||||
statusCursor.textContent = `行 ${line}, 列 ${col}`;
|
||||
}
|
||||
|
||||
// ===== Sync Scroll =====
|
||||
function syncScroll() {
|
||||
if (viewMode !== 'split') return;
|
||||
const editorEl = editor;
|
||||
const previewEl = previewPanel;
|
||||
const scrollPercent = editorEl.scrollTop / (editorEl.scrollHeight - editorEl.clientHeight);
|
||||
previewEl.scrollTop = scrollPercent * (previewEl.scrollHeight - previewEl.clientHeight);
|
||||
}
|
||||
|
||||
// ===== Update Preview =====
|
||||
function scheduleUpdate() {
|
||||
if (updateTimer) clearTimeout(updateTimer);
|
||||
updateTimer = setTimeout(() => {
|
||||
const text = editor.value;
|
||||
renderMarkdown(text);
|
||||
updateLineNumbers();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
// ===== File Operations =====
|
||||
function setModified(modified) {
|
||||
isModified = modified;
|
||||
const title = currentFilePath
|
||||
? `MarkLite - ${getFileName(currentFilePath)}${modified ? ' *' : ''}`
|
||||
: `MarkLite - 未命名${modified ? ' *' : ''}`;
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
function getFileName(filePath) {
|
||||
return filePath.split(/[/\\]/).pop();
|
||||
}
|
||||
|
||||
function setEditorContent(text, filePath) {
|
||||
editor.value = text;
|
||||
currentContent = text;
|
||||
currentFilePath = filePath || null;
|
||||
isModified = false;
|
||||
updateLineNumbers();
|
||||
renderMarkdown(text);
|
||||
updateCursorPosition();
|
||||
|
||||
if (filePath) {
|
||||
document.title = `MarkLite - ${getFileName(filePath)}`;
|
||||
statusText.textContent = getFileName(filePath);
|
||||
} else {
|
||||
document.title = 'MarkLite - 未命名';
|
||||
statusText.textContent = '未命名';
|
||||
}
|
||||
|
||||
// Hide welcome screen
|
||||
welcomeScreen.classList.add('hidden');
|
||||
}
|
||||
|
||||
async function handleOpenFile() {
|
||||
if (typeof window.electronAPI !== 'undefined') {
|
||||
const result = await window.electronAPI.openFile();
|
||||
if (result) {
|
||||
setEditorContent(result.content, result.filePath);
|
||||
}
|
||||
} else {
|
||||
// Fallback: use file input
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.md,.markdown,.txt';
|
||||
input.onchange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
setEditorContent(ev.target.result, file.name);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (typeof window.electronAPI !== 'undefined') {
|
||||
const result = await window.electronAPI.saveFile({
|
||||
filePath: currentFilePath,
|
||||
content: editor.value
|
||||
});
|
||||
if (result.success) {
|
||||
currentFilePath = result.filePath;
|
||||
currentContent = editor.value;
|
||||
setModified(false);
|
||||
statusText.textContent = '已保存';
|
||||
setTimeout(() => {
|
||||
statusText.textContent = getFileName(currentFilePath) || '就绪';
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
// Fallback: download file
|
||||
const blob = new Blob([editor.value], { type: 'text/markdown' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = (currentFilePath || 'untitled.md');
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
setModified(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveAs() {
|
||||
if (typeof window.electronAPI !== 'undefined') {
|
||||
const result = await window.electronAPI.saveFileAs({
|
||||
content: editor.value
|
||||
});
|
||||
if (result.success) {
|
||||
currentFilePath = result.filePath;
|
||||
currentContent = editor.value;
|
||||
setModified(false);
|
||||
statusText.textContent = '已保存';
|
||||
setTimeout(() => {
|
||||
statusText.textContent = getFileName(currentFilePath) || '就绪';
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
handleSave();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== View Mode =====
|
||||
function setViewMode(mode) {
|
||||
viewMode = mode;
|
||||
const app = document.getElementById('app');
|
||||
|
||||
// Remove all mode classes
|
||||
app.classList.remove('mode-editor', 'mode-preview');
|
||||
|
||||
// Update button states
|
||||
btnSplit.classList.remove('active');
|
||||
btnEditor.classList.remove('active');
|
||||
btnPreview.classList.remove('active');
|
||||
|
||||
switch (mode) {
|
||||
case 'split':
|
||||
btnSplit.classList.add('active');
|
||||
break;
|
||||
case 'editor':
|
||||
btnEditor.classList.add('active');
|
||||
app.classList.add('mode-editor');
|
||||
break;
|
||||
case 'preview':
|
||||
btnPreview.classList.add('active');
|
||||
app.classList.add('mode-preview');
|
||||
// Update preview when switching to preview mode
|
||||
renderMarkdown(editor.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Resizer =====
|
||||
let isResizing = false;
|
||||
|
||||
function initResizer() {
|
||||
resizer.addEventListener('mousedown', (e) => {
|
||||
isResizing = true;
|
||||
resizer.classList.add('active');
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isResizing) return;
|
||||
const containerRect = mainContent.getBoundingClientRect();
|
||||
const percentage = ((e.clientX - containerRect.left) / containerRect.width) * 100;
|
||||
const clamped = Math.max(20, Math.min(80, percentage));
|
||||
editorPanel.style.flex = `0 0 ${clamped}%`;
|
||||
previewPanel.style.flex = `0 0 ${100 - clamped}%`;
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isResizing) {
|
||||
isResizing = false;
|
||||
resizer.classList.remove('active');
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Drag & Drop =====
|
||||
function initDragDrop() {
|
||||
let dragCounter = 0;
|
||||
|
||||
document.addEventListener('dragenter', (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter++;
|
||||
dropOverlay.classList.remove('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter--;
|
||||
if (dragCounter === 0) {
|
||||
dropOverlay.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter = 0;
|
||||
dropOverlay.classList.add('hidden');
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
setEditorContent(ev.target.result, file.name);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Keyboard Shortcuts =====
|
||||
function initKeyboard() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ctrl+O: Open
|
||||
if (e.ctrlKey && e.key === 'o') {
|
||||
e.preventDefault();
|
||||
handleOpenFile();
|
||||
}
|
||||
// Ctrl+S: Save
|
||||
if (e.ctrlKey && e.key === 's' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
// Ctrl+Shift+S: Save As
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
|
||||
e.preventDefault();
|
||||
handleSaveAs();
|
||||
}
|
||||
// Ctrl+1: Split view
|
||||
if (e.ctrlKey && e.key === '1') {
|
||||
e.preventDefault();
|
||||
setViewMode('split');
|
||||
}
|
||||
// Ctrl+2: Editor only
|
||||
if (e.ctrlKey && e.key === '2') {
|
||||
e.preventDefault();
|
||||
setViewMode('editor');
|
||||
}
|
||||
// Ctrl+3: Preview only
|
||||
if (e.ctrlKey && e.key === '3') {
|
||||
e.preventDefault();
|
||||
setViewMode('preview');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Tab key support in editor =====
|
||||
function initEditorTab() {
|
||||
editor.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = editor.selectionStart;
|
||||
const end = editor.selectionEnd;
|
||||
const value = editor.value;
|
||||
editor.value = value.substring(0, start) + ' ' + value.substring(end);
|
||||
editor.selectionStart = editor.selectionEnd = start + 4;
|
||||
scheduleUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Event Listeners =====
|
||||
function initEventListeners() {
|
||||
// Editor input
|
||||
editor.addEventListener('input', () => {
|
||||
setModified(true);
|
||||
scheduleUpdate();
|
||||
});
|
||||
|
||||
// Editor scroll sync
|
||||
editor.addEventListener('scroll', syncScroll);
|
||||
|
||||
// Editor cursor position
|
||||
editor.addEventListener('click', updateCursorPosition);
|
||||
editor.addEventListener('keyup', updateCursorPosition);
|
||||
editor.addEventListener('select', updateCursorPosition);
|
||||
|
||||
// Toolbar buttons
|
||||
btnOpen.addEventListener('click', handleOpenFile);
|
||||
btnSave.addEventListener('click', handleSave);
|
||||
btnSplit.addEventListener('click', () => setViewMode('split'));
|
||||
btnEditor.addEventListener('click', () => setViewMode('editor'));
|
||||
btnPreview.addEventListener('click', () => setViewMode('preview'));
|
||||
|
||||
// Welcome buttons
|
||||
btnWelcomeOpen.addEventListener('click', handleOpenFile);
|
||||
btnWelcomeNew.addEventListener('click', () => {
|
||||
setEditorContent('', null);
|
||||
});
|
||||
|
||||
// Electron IPC events
|
||||
if (typeof window.electronAPI !== 'undefined') {
|
||||
window.electronAPI.onFileOpened((data) => {
|
||||
setEditorContent(data.content, data.filePath);
|
||||
});
|
||||
|
||||
window.electronAPI.onMenuSave(() => {
|
||||
handleSave();
|
||||
});
|
||||
|
||||
window.electronAPI.onMenuSaveAs(() => {
|
||||
handleSaveAs();
|
||||
});
|
||||
|
||||
window.electronAPI.onViewModeChange((mode) => {
|
||||
setViewMode(mode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Initialize =====
|
||||
function init() {
|
||||
initMarked();
|
||||
initResizer();
|
||||
initDragDrop();
|
||||
initKeyboard();
|
||||
initEditorTab();
|
||||
initEventListeners();
|
||||
updateLineNumbers();
|
||||
setViewMode('split');
|
||||
}
|
||||
|
||||
// Run when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,548 @@
|
||||
/* ===== Reset & Base ===== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #1a73e8;
|
||||
--primary-light: #e8f0fe;
|
||||
--primary-dark: #1557b0;
|
||||
--bg: #ffffff;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--bg-tertiary: #f1f3f4;
|
||||
--text: #333333;
|
||||
--text-secondary: #5f6368;
|
||||
--text-tertiary: #9aa0a6;
|
||||
--border: #e1e4e8;
|
||||
--border-light: #f0f0f0;
|
||||
--code-bg: #f6f8fa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
--radius: 6px;
|
||||
--toolbar-height: 44px;
|
||||
--statusbar-height: 28px;
|
||||
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Toolbar ===== */
|
||||
#toolbar {
|
||||
height: var(--toolbar-height);
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
font-family: var(--font-ui);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toolbar-btn.active {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.toolbar-btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--border);
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
/* ===== Main Content ===== */
|
||||
#main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ===== Editor Panel ===== */
|
||||
#editor-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
#editor-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#line-numbers {
|
||||
width: 50px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-light);
|
||||
padding: 12px 0;
|
||||
text-align: right;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-tertiary);
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#line-numbers .line-num {
|
||||
padding-right: 12px;
|
||||
height: 20.8px;
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
tab-size: 4;
|
||||
overflow-y: auto;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
#editor::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== Resizer ===== */
|
||||
#resizer {
|
||||
width: 4px;
|
||||
background: var(--border);
|
||||
cursor: col-resize;
|
||||
transition: background 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#resizer:hover,
|
||||
#resizer.active {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
/* ===== Preview Panel ===== */
|
||||
#preview-panel {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
#preview {
|
||||
padding: 24px 32px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ===== Markdown Body Styles ===== */
|
||||
.markdown-body {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
color: var(--text);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 2em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 1.5em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.markdown-body h3 { font-size: 1.25em; }
|
||||
.markdown-body h4 { font-size: 1em; }
|
||||
.markdown-body h5 { font-size: 0.875em; }
|
||||
.markdown-body h6 { font-size: 0.85em; color: var(--text-secondary); }
|
||||
|
||||
.markdown-body p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-body strong { font-weight: 600; }
|
||||
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
height: 2px;
|
||||
background: var(--border);
|
||||
border: none;
|
||||
margin: 24px 0;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
margin: 0 0 16px 0;
|
||||
padding: 4px 16px;
|
||||
border-left: 4px solid var(--primary);
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 0 var(--radius) var(--radius) 0;
|
||||
}
|
||||
|
||||
.markdown-body blockquote p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.markdown-body li + li {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
background: var(--code-bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: #e83e8c;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: var(--code-bg);
|
||||
border-radius: var(--radius);
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.markdown-body table th,
|
||||
.markdown-body table td {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-body table th {
|
||||
font-weight: 600;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.markdown-body table tr:nth-child(even) {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.markdown-body input[type="checkbox"] {
|
||||
margin-right: 6px;
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
/* ===== Status Bar ===== */
|
||||
#statusbar {
|
||||
height: var(--statusbar-height);
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-left,
|
||||
.status-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-divider {
|
||||
color: var(--border);
|
||||
}
|
||||
|
||||
/* ===== Drop Overlay ===== */
|
||||
#drop-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(26, 115, 232, 0.1);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
border: 3px dashed var(--primary);
|
||||
margin: 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
#drop-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.drop-content {
|
||||
text-align: center;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.drop-content svg {
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.drop-content p {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== Welcome Screen ===== */
|
||||
#welcome-screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#welcome-screen.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.welcome-content h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.welcome-content > p {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.welcome-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-family: var(--font-ui);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.welcome-btn.primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.welcome-btn.primary:hover {
|
||||
background: var(--primary-dark);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.welcome-btn.secondary {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.welcome-btn.secondary:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.welcome-tips {
|
||||
text-align: left;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px 20px;
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.welcome-tips p {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.welcome-tips p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ===== View Modes ===== */
|
||||
#app.mode-editor #preview-panel,
|
||||
#app.mode-editor #resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#app.mode-preview #editor-panel,
|
||||
#app.mode-preview #resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ===== Scrollbar ===== */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== Animations ===== */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
Reference in New Issue
Block a user