From 9854f6ec9bfb51c79562789b253a2b7370ef67d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E5=BF=97=E5=86=9B?= <851516902@qq.com> Date: Mon, 6 Jul 2026 20:30:40 +0800 Subject: [PATCH] =?UTF-8?q?=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96,=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E9=A2=9C=E5=80=BC=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main.ts | 5 + electron/preload.ts | 2 + src/renderer/src/App.tsx | 350 +++++++----- src/renderer/src/ContentArea.tsx | 12 +- src/renderer/src/PreviewArticle.tsx | 50 +- src/renderer/src/StatsPanel.tsx | 2 +- src/renderer/src/appearance-styles.css | 562 ++++++-------------- src/renderer/src/appearance.ts | 46 +- src/renderer/src/index.css | 707 ++++++++++++++++++------- src/renderer/src/markdown.ts | 31 +- 10 files changed, 1012 insertions(+), 755 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 58e8233..7d5a888 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -360,6 +360,11 @@ ipcMain.on('myreader:allow-close', (event) => { win.close() }) +ipcMain.handle('myreader:set-menu-visible', (_evt, visible: boolean): void => { + const menu = visible ? buildMenu() : null + Menu.setApplicationMenu(menu) +}) + let pendingOpenPaths: string[] = [] const gotTheLock = app.requestSingleInstanceLock() diff --git a/electron/preload.ts b/electron/preload.ts index cbdc425..f653b78 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -33,6 +33,7 @@ export interface MyReaderApi { deleteFile: (filePath: string) => Promise renamePath: (oldPath: string, newName: string) => Promise<{ newPath: string }> allowClose: () => void + setMenuVisible: (visible: boolean) => Promise onRequestClose: (handler: () => void) => () => void onMenuAction: (handler: (action: string) => void) => () => void onOpenFiles: (handler: (paths: string[]) => void) => () => void @@ -59,6 +60,7 @@ const api: MyReaderApi = { allowClose: () => { ipcRenderer.send('myreader:allow-close') }, + setMenuVisible: (visible: boolean) => ipcRenderer.invoke('myreader:set-menu-visible', visible), onRequestClose: (handler: () => void) => { const listener = (): void => { handler() diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index fd2d387..b866a75 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -69,7 +69,7 @@ export default function App(): React.ReactElement { return Number.isFinite(n) ? clamp(n, 160, 400) : 220 }) const [statsHidden, setStatsHidden] = React.useState( - () => localStorage.getItem(STATS_HIDDEN_KEY) === '1' + () => localStorage.getItem(STATS_HIDDEN_KEY) !== '0' ) const [splitPct, setSplitPct] = React.useState(() => { const n = Number(localStorage.getItem(SPLIT_KEY) ?? '48') @@ -129,11 +129,13 @@ export default function App(): React.ReactElement { const [dragOver, setDragOver] = React.useState(false) const [imagePreviewData, setImagePreviewData] = React.useState(null) const [imagePreviewFileName, setImagePreviewFileName] = React.useState('') - const [imageScale, setImageScale] = React.useState(1) + const [imageScale, setImageScale] = React.useState(5) const [imagePosition, setImagePosition] = React.useState({ x: 0, y: 0 }) const [isDragging, setIsDragging] = React.useState(false) const [dragStart, setDragStart] = React.useState({ x: 0, y: 0 }) const [isExportingImage, setIsExportingImage] = React.useState(false) + const [isFullscreen, setIsFullscreen] = React.useState(false) + const [toolbarCollapsed, setToolbarCollapsed] = React.useState(false) const activeTab = tabs.find((t) => t.id === activeId) ?? null const activeDoc = activeTab?.content ?? '' @@ -147,6 +149,7 @@ export default function App(): React.ReactElement { const openingPathsRef = React.useRef>(new Set()) const startupFilesHandledRef = React.useRef(false) const scrollPositionsRef = React.useRef>(new Map()) + const previewScrollPositionsRef = React.useRef>(new Map()) const prevActiveIdRef = React.useRef(null) const refreshDir = React.useCallback(async (): Promise => { @@ -327,6 +330,13 @@ export default function App(): React.ReactElement { localStorage.setItem(AUTO_SAVE_KEY, autoSave ? '1' : '0') }, [autoSave]) + React.useEffect(() => { + api.setMenuVisible(!isFullscreen) + if (isFullscreen) { + setPreviewMaxWidth(window.innerWidth) + } + }, [isFullscreen]) + React.useEffect(() => { if (!activeDoc) { setStatusText('') @@ -559,6 +569,14 @@ export default function App(): React.ReactElement { if (editor) { scrollPositionsRef.current.set(prevId, editor.getScrollTop()) } + const readPreviewOuter = readPreviewOuterRef.current + if (readPreviewOuter) { + previewScrollPositionsRef.current.set(prevId, readPreviewOuter.scrollTop) + } + const livePreviewOuter = livePreviewOuterRef.current + if (livePreviewOuter) { + previewScrollPositionsRef.current.set(prevId, livePreviewOuter.scrollTop) + } } prevActiveIdRef.current = activeId }, [activeId]) @@ -572,6 +590,15 @@ export default function App(): React.ReactElement { if (editor) editor.setScrollTop(saved) }) } + const previewSaved = previewScrollPositionsRef.current.get(activeId) + if (previewSaved !== undefined && previewSaved > 0) { + requestAnimationFrame(() => { + const readPreviewOuter = readPreviewOuterRef.current + if (readPreviewOuter) readPreviewOuter.scrollTop = previewSaved + const livePreviewOuter = livePreviewOuterRef.current + if (livePreviewOuter) livePreviewOuter.scrollTop = previewSaved + }) + } }, [editorMountKey]) const scrollPreviewToAnchorId = React.useCallback( @@ -893,7 +920,7 @@ export default function App(): React.ReactElement { const startX = e.clientX const startW = previewMaxWidth const onMove = (ev: MouseEvent): void => { - setPreviewMaxWidth(clamp(startW + (ev.clientX - startX), 420, 1400)) + setPreviewMaxWidth(clamp(startW + (ev.clientX - startX), 420, 3000)) } const onUp = (): void => { document.removeEventListener('mousemove', onMove) @@ -1282,10 +1309,13 @@ ${previewHtml} setSearchVisible(false) setSearchQuery('') } + if (e.key === 'Escape' && isFullscreen) { + setIsFullscreen(false) + } } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) - }, [searchVisible, saveActive, newTabAction, activeId, requestCloseTab, cycleViewMode, openFileDialog, saveActiveAs, exportHtml, openFolderDialog, refreshDir, viewMode]) + }, [searchVisible, saveActive, newTabAction, activeId, requestCloseTab, cycleViewMode, openFileDialog, saveActiveAs, exportHtml, openFolderDialog, refreshDir, viewMode, isFullscreen]) if (!api) { return ( @@ -1365,114 +1395,146 @@ ${previewHtml} onCtxDelete={(path) => void deleteFileAt(path)} /> -
- -
+ {!isFullscreen && ( +
+ +
+ )} -
- - - - - - - - - - - -
- - - + {!isFullscreen && ( +
+ {toolbarCollapsed ? ( + + ) : ( + <> + + + + + + + + + + + + +
+ + + + +
+ + )}
-
+ )} -
- {!sidebarHidden ? ( +
+ {!isFullscreen && !sidebarHidden ? ( <>
{dragOver ? (
@@ -1592,19 +1665,54 @@ ${previewHtml}
e.stopPropagation()}>

📷 图片预览

- +
+ + {Math.round(imageScale * 100)}% + + + +
-
+
{ + e.preventDefault() + const delta = e.deltaY > 0 ? -0.25 : 0.25 + setImageScale((s) => Math.min(20, Math.max(0.25, s + delta))) + }} + > 预览
diff --git a/src/renderer/src/ContentArea.tsx b/src/renderer/src/ContentArea.tsx index f2c3d5c..38e5aee 100644 --- a/src/renderer/src/ContentArea.tsx +++ b/src/renderer/src/ContentArea.tsx @@ -112,9 +112,9 @@ export function ContentArea(props: ContentAreaProps): React.ReactElement { } return ( - <> +
{searchVisible ? ( -
+
) : null} - -
{(viewMode === 'hybrid' || viewMode === 'live') && activeTab ? ( ) : null} @@ -158,11 +156,6 @@ export function ContentArea(props: ContentAreaProps): React.ReactElement {
{renderPreviewArticle(readPreviewArticleRef)} -
) : ( @@ -264,6 +257,5 @@ export function ContentArea(props: ContentAreaProps): React.ReactElement { ) ) : null}
- ) } diff --git a/src/renderer/src/PreviewArticle.tsx b/src/renderer/src/PreviewArticle.tsx index e49f074..080b892 100644 --- a/src/renderer/src/PreviewArticle.tsx +++ b/src/renderer/src/PreviewArticle.tsx @@ -45,7 +45,7 @@ function mermaidSourceFromWrap(wrap: HTMLElement): string { return pre?.textContent ?? '' } -async function renderMermaidIn(root: HTMLElement, isStale: () => boolean): Promise { +async function renderMermaidIn(root: HTMLElement, isStale: () => boolean, onZoom?: (source: string, svgHtml: string) => void): Promise { const wraps = root.querySelectorAll('.md-diagram-wrap') if (wraps.length === 0) return @@ -88,6 +88,19 @@ async function renderMermaidIn(root: HTMLElement, isStale: () => boolean): Promi }) }) wrap.dataset.mermaidRendered = '1' + + if (onZoom) { + const zoomBtn = document.createElement('button') + zoomBtn.type = 'button' + zoomBtn.className = 'diagram-zoom-btn' + zoomBtn.innerHTML = '🔍' + zoomBtn.title = '放大查看' + zoomBtn.onclick = (e) => { + e.stopPropagation() + onZoom(source, svg) + } + wrap.appendChild(zoomBtn) + } } catch (err) { if (isStale()) return const msg = err instanceof Error ? err.message : String(err) @@ -255,6 +268,11 @@ type Props = { onOpenExternal?: (url: string) => void | Promise } +type DiagramZoomState = { + source: string + svgHtml: string +} | null + export function PreviewArticle({ html, articleRef, @@ -274,6 +292,16 @@ export function PreviewArticle({ const openExtRef = React.useRef(onOpenExternal) openExtRef.current = onOpenExternal + const [diagramZoom, setDiagramZoom] = React.useState(null) + + const handleDiagramZoom = React.useCallback((source: string, svgHtml: string) => { + setDiagramZoom({ source, svgHtml }) + }, []) + + const handleDiagramCloseZoom = React.useCallback(() => { + setDiagramZoom(null) + }, []) + const setRef = React.useCallback( (el: HTMLElement | null) => { innerRef.current = el @@ -366,7 +394,7 @@ export function PreviewArticle({ const gen = ++renderGenRef.current root.innerHTML = html - void renderMermaidIn(root, () => gen !== renderGenRef.current).catch((err) => { + void renderMermaidIn(root, () => gen !== renderGenRef.current, handleDiagramZoom).catch((err) => { console.warn("[Huangzhijun's Reader] mermaid batch failed", err) }) @@ -374,9 +402,23 @@ export function PreviewArticle({ renderGenRef.current += 1 disconnectMermaidObservers(root) } - }, [html]) + }, [html, handleDiagramZoom]) return ( -
+ <> +
+ {diagramZoom ? ( +
+
e.stopPropagation()}> + +
+
+
+
+
+ ) : null} + ) } diff --git a/src/renderer/src/StatsPanel.tsx b/src/renderer/src/StatsPanel.tsx index e42ea0c..20141fb 100644 --- a/src/renderer/src/StatsPanel.tsx +++ b/src/renderer/src/StatsPanel.tsx @@ -176,7 +176,7 @@ export function StatsPanel({ doc, width, onResizeStart }: Props): React.ReactEle
-
made by huangzhijun 2026
+
diff --git a/src/renderer/src/appearance-styles.css b/src/renderer/src/appearance-styles.css index cae824e..98f36f3 100644 --- a/src/renderer/src/appearance-styles.css +++ b/src/renderer/src/appearance-styles.css @@ -1,330 +1,201 @@ -/* 背景(12):颜色变量,挂到 document.documentElement data-bg */ -:root[data-bg='0'] { - --app-bg: #f3f4f6; - --panel-bg: #e5e7eb; - --text: #111827; - --muted: #6b7280; - --border: #d1d5db; - --accent: #2563eb; - --btn-bg: #ffffff; - --hover: rgba(0, 0, 0, 0.06); - --preview-bg: #ffffff; - --preview-text: #111827; - --preview-shadow: 0 8px 30px rgba(15, 23, 42, 0.08); - --code-bg: #f9fafb; +:root { + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.06); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 16px 40px rgba(0, 0, 0, 0.12); + + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 24px; + --radius-full: 9999px; + + --glass-border: rgba(255, 255, 255, 0.12); + --glass-bg: rgba(255, 255, 255, 0.06); } + +:root[data-bg='0'] { + --app-bg: #fafafa; + --bg-start: #fafafa; + --bg-mid: #f5f5f5; + --bg-end: #f0f0f0; + --panel-bg: #fafafa; + --text: #1c1917; + --muted: #78716c; + --border: #e7e5e4; + --accent: #6366f1; + --btn-bg: #f5f5f5; + --hover: rgba(99, 102, 241, 0.08); + --preview-bg: #fafafa; + --preview-text: #1c1917; + --preview-shadow: none; + --code-bg: #f5f5f4; + --glass-border: rgba(255, 255, 255, 0.6); + --glass-bg: rgba(255, 255, 255, 0.8); +} + :root[data-bg='1'] { --app-bg: #f4ecd8; - --panel-bg: #e8dfc8; + --bg-start: #f4ecd8; + --bg-mid: #f0e6c8; + --bg-end: #ebe0c8; + --panel-bg: #f2e8d0; --text: #3e3428; --muted: #7a6a58; --border: #d2c7b2; --accent: #8b4513; - --btn-bg: #fffaf0; + --btn-bg: #f0e6d6; --hover: rgba(62, 52, 40, 0.08); - --preview-bg: #fffaf0; + --preview-bg: #f4ecd8; --preview-text: #3e3428; - --preview-shadow: 0 8px 24px rgba(62, 52, 40, 0.12); + --preview-shadow: none; --code-bg: #f0e6d6; } + :root[data-bg='2'] { - --app-bg: #0f172a; - --panel-bg: #111c33; - --text: #e5e7eb; - --muted: #94a3b8; - --border: #1e293b; - --accent: #38bdf8; - --btn-bg: #1e293b; - --hover: rgba(148, 163, 184, 0.12); - --preview-bg: #111827; - --preview-text: #e5e7eb; - --preview-shadow: 0 12px 40px rgba(0, 0, 0, 0.45); - --code-bg: #0b1224; + --app-bg: #1a1a1a; + --bg-start: #1a1a1a; + --bg-mid: #202020; + --bg-end: #1e1e1e; + --panel-bg: #1e1e1e; + --text: #fafafa; + --muted: #a3a3a3; + --border: #3f3f3f; + --accent: #60a5fa; + --btn-bg: #202020; + --hover: rgba(96, 165, 250, 0.12); + --preview-bg: #1a1a1a; + --preview-text: #fafafa; + --preview-shadow: none; + --code-bg: #141414; + --glass-border: rgba(255, 255, 255, 0.1); + --glass-bg: rgba(255, 255, 255, 0.06); } + :root[data-bg='3'] { - --app-bg: #102216; - --panel-bg: #143220; - --text: #e7f7ec; - --muted: #8fb39a; - --border: #1f3b2a; - --accent: #4ade80; - --btn-bg: #163527; - --hover: rgba(74, 222, 128, 0.12); - --preview-bg: #132a1f; - --preview-text: #e7f7ec; - --preview-shadow: 0 10px 36px rgba(0, 0, 0, 0.45); - --code-bg: #0d1f16; + --app-bg: #f0fdf4; + --bg-start: #f0fdf4; + --bg-mid: #dcfce7; + --bg-end: #dcfce7; + --panel-bg: #e8f5ec; + --text: #166534; + --muted: #22c55e; + --border: #bbf7d0; + --accent: #15803d; + --btn-bg: #dcfce7; + --hover: rgba(21, 128, 61, 0.08); + --preview-bg: #f0fdf4; + --preview-text: #166534; + --preview-shadow: none; + --code-bg: #dcfce7; } + :root[data-bg='4'] { --app-bg: linear-gradient(160deg, #e0f2fe 0%, #eef2ff 45%, #fdf4ff 100%); - --panel-bg: rgba(255, 255, 255, 0.72); + --panel-bg: rgba(255, 255, 255, 0.88); --text: #0f172a; --muted: #475569; --border: #cbd5f5; --accent: #4f46e5; - --btn-bg: rgba(255, 255, 255, 0.9); + --btn-bg: rgba(255, 255, 255, 0.95); --hover: rgba(79, 70, 229, 0.1); - --preview-bg: rgba(255, 255, 255, 0.92); + --preview-bg: rgba(255, 255, 255, 0.98); --preview-text: #0f172a; - --preview-shadow: 0 12px 40px rgba(15, 23, 42, 0.12); + --preview-shadow: none; --code-bg: #f8fafc; } -:root[data-bg='5'] { - --app-bg: #fafafa; - --panel-bg: #ffffff; - --text: #111111; - --muted: #737373; - --border: #e5e5e5; - --accent: #db2777; - --btn-bg: #ffffff; - --hover: rgba(219, 39, 119, 0.08); - --preview-bg: #ffffff; - --preview-text: #111111; - --preview-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); - --code-bg: #f5f5f5; -} -:root[data-bg='6'] { - --app-bg: #121212; - --panel-bg: #1a1a1a; - --text: #fafafa; - --muted: #a3a3a3; - --border: #262626; - --accent: #f97316; - --btn-bg: #171717; - --hover: rgba(249, 115, 22, 0.15); - --preview-bg: #0a0a0a; - --preview-text: #fafafa; - --preview-shadow: 0 0 0 1px #262626; - --code-bg: #000000; -} -:root[data-bg='7'] { - --app-bg: #f5f3ff; - --panel-bg: #ede9fe; - --text: #312e81; - --muted: #6d28d9; - --border: #ddd6fe; - --accent: #7c3aed; - --btn-bg: #ffffff; - --hover: rgba(124, 58, 237, 0.1); - --preview-bg: #ffffff; - --preview-text: #312e81; - --preview-shadow: 0 10px 32px rgba(49, 46, 129, 0.12); - --code-bg: #f5f3ff; -} -:root[data-bg='8'] { - --app-bg: #1c1917; - --panel-bg: #292524; - --text: #fafaf9; - --muted: #a8a29e; - --border: #44403c; - --accent: #fbbf24; - --btn-bg: #292524; - --hover: rgba(251, 191, 36, 0.12); - --preview-bg: #1c1917; - --preview-text: #fafaf9; - --preview-shadow: 0 12px 40px rgba(0, 0, 0, 0.55); - --code-bg: #0c0a09; -} -:root[data-bg='9'] { - --app-bg: #1a1033; - --panel-bg: #221547; - --text: #f3e8ff; - --muted: #c4b5fd; - --border: #3b2a6b; - --accent: #a78bfa; - --btn-bg: #2a185f; - --hover: rgba(167, 139, 250, 0.15); - --preview-bg: #120c24; - --preview-text: #f3e8ff; - --preview-shadow: 0 14px 48px rgba(0, 0, 0, 0.55); - --code-bg: #0f0820; -} -:root[data-bg='10'] { - --app-bg: #e8f4fc; - --panel-bg: #d7eaf8; - --text: #0c4a6e; - --muted: #0369a1; - --border: #bae6fd; - --accent: #0284c7; - --btn-bg: #f0f9ff; - --hover: rgba(2, 132, 199, 0.12); - --preview-bg: #ffffff; - --preview-text: #0c4a6e; - --preview-shadow: 0 10px 28px rgba(12, 74, 110, 0.1); - --code-bg: #e0f2fe; -} -:root[data-bg='11'] { - --app-bg: #fdf2f4; - --panel-bg: #fce7eb; - --text: #881337; - --muted: #9f1239; - --border: #fbcfe8; - --accent: #db2777; - --btn-bg: #fff1f2; - --hover: rgba(219, 39, 119, 0.1); - --preview-bg: #ffffff; - --preview-text: #881337; - --preview-shadow: 0 10px 28px rgba(136, 19, 55, 0.08); - --code-bg: #ffe4e6; -} -:root[data-bg='12'] { - --app-bg: #18181b; - --panel-bg: #27272a; - --text: #fafafa; - --muted: #a1a1aa; - --border: #3f3f46; - --accent: #22d3ee; - --btn-bg: #27272a; - --hover: rgba(34, 211, 238, 0.12); - --preview-bg: #18181b; - --preview-text: #fafafa; - --preview-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); - --code-bg: #09090b; -} -:root[data-bg='13'] { - --app-bg: #ecfeff; - --panel-bg: #cffafe; - --text: #0e7490; - --muted: #0891b2; - --border: #a5f3fc; - --accent: #0d9488; - --btn-bg: #f0fdfa; - --hover: rgba(13, 148, 136, 0.12); - --preview-bg: #ffffff; - --preview-text: #134e4a; - --preview-shadow: 0 10px 28px rgba(8, 145, 178, 0.1); - --code-bg: #ccfbf1; -} -:root[data-bg='14'] { - --app-bg: #faf5f0; - --panel-bg: #ede4d8; - --text: #422006; - --muted: #92400e; - --border: #d6c4b0; - --accent: #b45309; - --btn-bg: #fffbeb; - --hover: rgba(180, 83, 9, 0.1); - --preview-bg: #fffbeb; - --preview-text: #422006; - --preview-shadow: 0 10px 28px rgba(66, 32, 6, 0.08); - --code-bg: #fef3c7; -} -:root[data-bg='15'] { - --app-bg: #f0fdfa; - --panel-bg: #ccfbf1; - --text: #115e59; - --muted: #0f766e; - --border: #99f6e4; - --accent: #0d9488; - --btn-bg: #ecfdf5; - --hover: rgba(13, 148, 136, 0.12); - --preview-bg: #ffffff; - --preview-text: #134e4a; - --preview-shadow: 0 10px 28px rgba(17, 94, 89, 0.08); - --code-bg: #d1fae5; -} -:root[data-bg='16'] { - --app-bg: #f5f3ff; - --panel-bg: #ede9fe; - --text: #4c1d95; - --muted: #6d28d9; - --border: #ddd6fe; - --accent: #7c3aed; - --btn-bg: #faf5ff; - --hover: rgba(124, 58, 237, 0.1); - --preview-bg: #ffffff; - --preview-text: #4c1d95; - --preview-shadow: 0 10px 28px rgba(76, 29, 149, 0.08); - --code-bg: #ede9fe; -} -:root[data-bg='17'] { - --app-bg: #faf8f5; - --panel-bg: #f3efe8; - --text: #44403c; - --muted: #78716c; - --border: #e7e5e4; - --accent: #d97706; - --btn-bg: #ffffff; - --hover: rgba(217, 119, 6, 0.1); - --preview-bg: #ffffff; - --preview-text: #44403c; - --preview-shadow: 0 10px 28px rgba(68, 64, 60, 0.08); - --code-bg: #f5f5f4; -} -:root[data-bg='18'] { - --app-bg: #1c1917; - --panel-bg: #292524; - --text: #fafaf9; - --muted: #a8a29e; - --border: #44403c; - --accent: #eab308; - --btn-bg: #292524; - --hover: rgba(234, 179, 8, 0.12); - --preview-bg: #1c1917; - --preview-text: #fafaf9; - --preview-shadow: 0 12px 40px rgba(0, 0, 0, 0.45); - --code-bg: #0c0a09; -} -:root[data-bg='19'] { - --app-bg: #fdf4f8; - --panel-bg: #fce7f3; - --text: #831843; - --muted: #be185d; - --border: #fbcfe8; - --accent: #db2777; - --btn-bg: #fff1f2; - --hover: rgba(219, 39, 119, 0.1); - --preview-bg: #ffffff; - --preview-text: #831843; - --preview-shadow: 0 10px 28px rgba(131, 24, 67, 0.08); - --code-bg: #ffe4e6; -} -:root[data-bg='20'] { - --app-bg: #f1f5f9; - --panel-bg: #e2e8f0; - --text: #1e293b; - --muted: #64748b; - --border: #cbd5e1; - --accent: #475569; - --btn-bg: #ffffff; - --hover: rgba(71, 85, 105, 0.12); - --preview-bg: #ffffff; - --preview-text: #1e293b; - --preview-shadow: 0 10px 28px rgba(30, 41, 59, 0.1); - --code-bg: #f8fafc; -} -:root[data-bg='21'] { - --app-bg: #ecfdf5; - --panel-bg: #d1fae5; - --text: #14532d; - --muted: #166534; - --border: #bbf7d0; - --accent: #15803d; - --btn-bg: #f0fdf4; - --hover: rgba(21, 128, 61, 0.12); - --preview-bg: #ffffff; - --preview-text: #14532d; - --preview-shadow: 0 10px 28px rgba(20, 83, 45, 0.08); - --code-bg: #dcfce7; -} -/* Kindle 风格:暖灰纸面 + 轻微颗粒感(纯 CSS) */ -:root[data-bg='22'] { +:root[data-bg='5'] { --app-bg: #e8e2d8; - --panel-bg: rgba(245, 241, 234, 0.94); + --bg-start: #e8e2d8; + --bg-mid: #e0d8cc; + --bg-end: #dcccb8; + --panel-bg: #e6ddd2; --text: #2d2a26; --muted: #6b6560; --border: #c9c2b8; --accent: #c2410c; - --btn-bg: #faf6ef; + --btn-bg: #e4dcc8; --hover: rgba(194, 65, 12, 0.08); - --preview-bg: #f7f3eb; + --preview-bg: #e8e2d8; --preview-text: #1f1c18; - --preview-shadow: 0 10px 28px rgba(45, 42, 38, 0.1); + --preview-shadow: none; --code-bg: #ede8df; } -/* 字体:仅字体栈 — 20 种效果差异明显的字体 */ +:root[data-bg='6'] { + --app-bg: #f5efe6; + --bg-start: #f5efe6; + --bg-mid: #efe6d8; + --bg-end: #e8dfd0; + --panel-bg: #f1e9dd; + --text: #4a3f35; + --muted: #7a6b5e; + --border: #d9cfc0; + --accent: #a16207; + --btn-bg: #efe7db; + --hover: rgba(161, 98, 7, 0.08); + --preview-bg: #f5efe6; + --preview-text: #4a3f35; + --preview-shadow: none; + --code-bg: #efe7db; +} + +:root[data-bg='7'] { + --app-bg: #0a0a0f; + --bg-start: #0a0a0f; + --bg-mid: #10101a; + --bg-end: #0f0f1a; + --panel-bg: #0f0f1a; + --text: #e2e8f0; + --muted: #718096; + --border: #2d2d44; + --accent: #6366f1; + --btn-bg: #12121e; + --hover: rgba(99, 102, 241, 0.15); + --preview-bg: #0a0a0f; + --preview-text: #e2e8f0; + --preview-shadow: none; + --code-bg: #070710; +} + +:root[data-bg='8'] { + --app-bg: #e8e6e1; + --bg-start: #e8e6e1; + --bg-mid: #e0ddd5; + --bg-end: #d9d5cd; + --panel-bg: #e4e1d9; + --text: #404040; + --muted: #707070; + --border: #c5c1b8; + --accent: #6b7280; + --btn-bg: #e0ddd5; + --hover: rgba(107, 114, 128, 0.08); + --preview-bg: #e8e6e1; + --preview-text: #404040; + --preview-shadow: none; + --code-bg: #dfddd8; +} + +:root[data-bg='9'] { + --app-bg: #f8f5f0; + --bg-start: #f8f5f0; + --bg-mid: #f0ebe1; + --bg-end: #e8dfd0; + --panel-bg: #f2ede3; + --text: #4a3e30; + --muted: #8a7d6e; + --border: #d8cec0; + --accent: #b45309; + --btn-bg: #f0ebe1; + --hover: rgba(180, 83, 9, 0.08); + --preview-bg: #f8f5f0; + --preview-text: #4a3e30; + --preview-shadow: none; + --code-bg: #f0ebe1; +} + :root[data-font='0'] { --font-ui: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; --font-editor: ui-monospace, Menlo, Consolas, monospace; @@ -426,115 +297,6 @@ --font-preview: 'Space Mono', 'Courier New', monospace; } -/* 新增 7 种背景(23-29),凑满 30 种 */ -:root[data-bg='23'] { - --app-bg: #fffdf5; - --panel-bg: #fef9e7; - --text: #5c4033; - --muted: #8b7355; - --border: #f0e6d3; - --accent: #d4a373; - --btn-bg: #ffffff; - --hover: rgba(92, 64, 51, 0.06); - --preview-bg: #ffffff; - --preview-text: #5c4033; - --preview-shadow: 0 10px 28px rgba(92, 64, 51, 0.08); - --code-bg: #faf5eb; -} -[data-bg='23'] ::selection, -[data-bg='24'] ::selection, -[data-bg='25'] ::selection, -[data-bg='26'] ::selection, -[data-bg='27'] ::selection, -[data-bg='28'] ::selection, -[data-bg='29'] ::selection { - background: var(--border); -} -:root[data-bg='24'] { - --app-bg: #fef4e8; - --panel-bg: #fce8d0; - --text: #7b341e; - --muted: #c05621; - --border: #fbd38d; - --accent: #dd6b20; - --btn-bg: #fffaf0; - --hover: rgba(221, 107, 32, 0.1); - --preview-bg: #fff7ed; - --preview-text: #7b341e; - --preview-shadow: 0 10px 28px rgba(123, 52, 30, 0.1); - --code-bg: #feebc8; -} -:root[data-bg='25'] { - --app-bg: #eef6ff; - --panel-bg: #dbeafe; - --text: #1e3a5f; - --muted: #3b82f6; - --border: #bfdbfe; - --accent: #2563eb; - --btn-bg: #f8faff; - --hover: rgba(37, 99, 235, 0.1); - --preview-bg: #ffffff; - --preview-text: #1e3a5f; - --preview-shadow: 0 10px 28px rgba(30, 58, 95, 0.08); - --code-bg: #e0edff; -} -:root[data-bg='26'] { - --app-bg: #0a0a0f; - --panel-bg: #12121e; - --text: #e2e8f0; - --muted: #718096; - --border: #2d2d44; - --accent: #6366f1; - --btn-bg: #1a1a2e; - --hover: rgba(99, 102, 241, 0.15); - --preview-bg: #0f0f1a; - --preview-text: #e2e8f0; - --preview-shadow: 0 12px 40px rgba(0, 0, 0, 0.55); - --code-bg: #070710; -} -:root[data-bg='27'] { - --app-bg: #fdf2e9; - --panel-bg: #fae2d0; - --text: #7a2e1a; - --muted: #c05621; - --border: #f6c8a8; - --accent: #c2410c; - --btn-bg: #fff8f3; - --hover: rgba(194, 65, 12, 0.1); - --preview-bg: #fff5ed; - --preview-text: #7a2e1a; - --preview-shadow: 0 10px 28px rgba(122, 46, 26, 0.08); - --code-bg: #fde8d0; -} -:root[data-bg='28'] { - --app-bg: #eef2f7; - --panel-bg: #e0e6ef; - --text: #2d3748; - --muted: #718096; - --border: #d1d9e6; - --accent: #4a5568; - --btn-bg: #f8faff; - --hover: rgba(74, 85, 104, 0.1); - --preview-bg: #ffffff; - --preview-text: #2d3748; - --preview-shadow: 0 10px 28px rgba(45, 55, 72, 0.08); - --code-bg: #e8edf5; -} -:root[data-bg='29'] { - --app-bg: #1a1423; - --panel-bg: #2a1f3d; - --text: #e8e0f0; - --muted: #9b8ab0; - --border: #3d2f57; - --accent: #b39ddb; - --btn-bg: #2a1f3d; - --hover: rgba(179, 157, 219, 0.15); - --preview-bg: #151020; - --preview-text: #e8e0f0; - --preview-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); - --code-bg: #100c1a; -} - :root[data-bg='4'] .toolbar, :root[data-bg='4'] .tab-bar, :root[data-bg='4'] .sidebar { diff --git a/src/renderer/src/appearance.ts b/src/renderer/src/appearance.ts index 6b381d2..6b0d2c6 100644 --- a/src/renderer/src/appearance.ts +++ b/src/renderer/src/appearance.ts @@ -1,38 +1,16 @@ -/** 背景 id 0–29,对应 `appearance-styles.css` 中 `:root[data-bg]`。 */ -export const BACKGROUNDS: { id: string; name: string }[] = [ - { id: '0', name: '暖雾灰' }, - { id: '1', name: '羊皮纸' }, - { id: '2', name: '深海蓝' }, - { id: '3', name: '森绿' }, +export const BACKGROUNDS: { id: string; name: string; eyeCare?: boolean }[] = [ + { id: '0', name: '极简白' }, + { id: '1', name: '羊皮纸', eyeCare: true }, + { id: '2', name: '深空灰' }, + { id: '3', name: '护眼绿', eyeCare: true }, { id: '4', name: '极光渐变' }, - { id: '5', name: '极简粉' }, - { id: '6', name: '暗夜橙' }, - { id: '7', name: '薰衣草' }, - { id: '8', name: '暖石褐' }, - { id: '9', name: '星夜紫' }, - { id: '10', name: '霜青' }, - { id: '11', name: '玫瑰灰' }, - { id: '12', name: '石墨黑' }, - { id: '13', name: '薄荷冰' }, - { id: '14', name: '焦糖拿铁' }, - { id: '15', name: '青瓷' }, - { id: '16', name: '暮光紫灰' }, - { id: '17', name: '沙岸米' }, - { id: '18', name: '松烟墨' }, - { id: '19', name: '樱粉雾' }, - { id: '20', name: '钢蓝灰' }, - { id: '21', name: '苔痕绿' }, - { id: '22', name: 'Kindle 纸感' }, - { id: '23', name: '暖雾白' }, - { id: '24', name: '落日橘' }, - { id: '25', name: '冰川蓝' }, - { id: '26', name: '暗夜墨' }, - { id: '27', name: '陶土红' }, - { id: '28', name: '灰蓝雾' }, - { id: '29', name: '夜雾紫' } + { id: '5', name: 'Kindle 纸感', eyeCare: true }, + { id: '6', name: '宣纸黄', eyeCare: true }, + { id: '7', name: '暗夜紫' }, + { id: '8', name: '磨砂灰', eyeCare: true }, + { id: '9', name: '茶纸棕', eyeCare: true } ] -/** 字体 id 0–19,对应 `:root[data-font]`。 */ export const FONTS: { id: string; name: string }[] = [ { id: '0', name: '系统默认' }, { id: '1', name: '经典衬线' }, @@ -60,10 +38,10 @@ export type ViewMode = 'read' | 'hybrid' | 'live' export function validBackgroundId(s: string | null): string | null { if (s === null) return null - return /^([0-9]|[12][0-9])$/.test(s) ? s : null + return /^[0-9]$/.test(s) ? s : null } export function validFontId(s: string | null): string | null { if (s === null) return null return /^([0-9]|1[0-9])$/.test(s) ? s : null -} \ No newline at end of file +} diff --git a/src/renderer/src/index.css b/src/renderer/src/index.css index 1c17f99..32ab72b 100644 --- a/src/renderer/src/index.css +++ b/src/renderer/src/index.css @@ -90,13 +90,23 @@ html[data-bg='22'] .app-root { flex-wrap: nowrap; align-items: stretch; gap: 2px; - padding: 10px 12px 0; + padding: 3px 8px 0; border-bottom: 1px solid var(--border); background: var(--panel-bg); position: relative; z-index: 30; overflow: hidden; - box-shadow: 0 1px 4px color-mix(in srgb, #000 3%, transparent); + font-family: var(--font-preview, var(--font-ui, system-ui, sans-serif)); +} + +.tab-bar::before { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(to right, transparent, var(--border), transparent); } .tab { @@ -106,34 +116,35 @@ html[data-bg='22'] .app-root { min-width: 40px; max-width: 220px; border: none; - border-radius: 8px 8px 0 0; + border-radius: 10px 10px 0 0; background: transparent; color: var(--muted); overflow: hidden; - transition: background 0.18s, color 0.18s; + transition: background 0.2s ease, color 0.2s ease, transform 0.15s ease; position: relative; } .tab:not(.active):hover { - background: var(--hover); + background: color-mix(in srgb, var(--text) 4%, transparent); color: var(--text); + transform: translateY(-1px); } .tab.active { background: var(--app-bg); color: var(--text); - box-shadow: 0 -1.5px 0 var(--accent); + box-shadow: 0 -2px 0 var(--accent), 0 -1px 6px color-mix(in srgb, var(--accent) 10%, transparent); } .tab.active::after { content: ''; position: absolute; bottom: 0; - left: 8px; - right: 8px; - height: 2.5px; + left: 12px; + right: 12px; + height: 3px; background: var(--accent); - border-radius: 2px 2px 0 0; + border-radius: 3px 3px 0 0; } .tab.drag-over { @@ -151,8 +162,8 @@ html[data-bg='22'] .app-root { min-width: 0; display: inline-flex; align-items: center; - gap: 8px; - padding: 8px 6px 10px 14px; + gap: 6px; + padding: 4px 4px 6px 10px; border: none; background: transparent; color: inherit; @@ -162,32 +173,33 @@ html[data-bg='22'] .app-root { } .tab-close { - flex: 0 0 26px; + flex: 0 0 22px; border: none; background: transparent; color: var(--muted); cursor: pointer; - font-size: 14px; + font-size: 11px; line-height: 1; border-radius: 6px; padding: 0; display: flex; align-items: center; justify-content: center; - margin: 6px 6px 6px 0; - transition: color 0.15s, background 0.15s, transform 0.15s; + margin: 4px 6px 4px 0; + transition: color 0.18s ease, background 0.18s ease, transform 0.15s ease, opacity 0.18s ease; opacity: 0; } .tab.active .tab-close, .tab:hover .tab-close { - opacity: 1; + opacity: 0.7; } .tab-close:hover { color: #fff; - background: #e53e3e; - transform: scale(1.1); + background: #ef4444; + transform: scale(1.15); + opacity: 1; } .tab-title { @@ -272,7 +284,47 @@ html[data-bg='22'] .app-root { position: relative; z-index: 120; isolation: isolate; - box-shadow: 0 1px 4px color-mix(in srgb, #000 3%, transparent); + box-shadow: 0 4px 12px color-mix(in srgb, #000 4%, transparent); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; +} + +.toolbar-collapsed { + padding: 6px 14px; + height: 36px; +} + +.toolbar-collapse-btn { + width: 28px; + height: 28px; + padding: 0; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--btn-bg); + color: var(--muted); + font-size: 14px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.2s, background 0.2s, border-color 0.2s, transform 0.2s; +} + +.toolbar-collapse-btn:hover { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 8%, var(--btn-bg)); + border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); +} + +.toolbar-collapse-btn:active { + transform: scale(0.95); +} + +.toolbar-collapsed .toolbar-collapse-btn { + border-color: var(--accent); + color: var(--accent); + background: color-mix(in srgb, var(--accent) 10%, transparent); } .toolbar button, @@ -286,24 +338,27 @@ html[data-bg='22'] .app-root { } .toolbar button { - padding: 7px 14px; - border-radius: 7px; + padding: 8px 16px; + border-radius: 9px; border: 1px solid var(--border); background: var(--btn-bg); color: var(--text); font-size: 13px; font-weight: 500; - transition: border-color 0.15s, background 0.15s, box-shadow 0.15s, transform 0.1s; + transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; + position: relative; } .toolbar button:hover { border-color: var(--accent); - background: color-mix(in srgb, var(--accent) 8%, var(--btn-bg)); - box-shadow: 0 1px 4px color-mix(in srgb, var(--accent) 15%, transparent); + background: color-mix(in srgb, var(--accent) 6%, var(--btn-bg)); + box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 12%, transparent); + transform: translateY(-1px); } .toolbar button:active { - transform: scale(0.97); + transform: translateY(0) scale(0.98); + box-shadow: 0 2px 6px color-mix(in srgb, var(--accent) 8%, transparent); } .toolbar-tail { @@ -369,6 +424,56 @@ html[data-bg='22'] .app-root { background: color-mix(in srgb, var(--bg) 98%, var(--accent)); } +.main.fullscreen-mode { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + background: var(--bg); + flex-direction: column; +} + +.main.fullscreen-mode .content { + flex: 1; + min-height: 0; + padding: 0; + margin: 0; +} + +.main.fullscreen-mode .read-preview-outer, +.main.fullscreen-mode .live-preview-outer, +.main.fullscreen-mode .hybrid-preview { + height: 100%; +} + + + +.fullscreen-exit-btn { + position: fixed; + top: 16px; + right: 16px; + width: 40px; + height: 40px; + border: none; + border-radius: 50%; + background: rgba(0, 0, 0, 0.5); + color: #ffffff; + font-size: 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + z-index: 101; +} + +.fullscreen-exit-btn:hover { + background: rgba(0, 0, 0, 0.7); + transform: scale(1.1); +} + .sidebar { flex: 0 0 auto; flex-shrink: 0; @@ -435,21 +540,40 @@ html[data-bg='22'] .app-root { font-size: 13px; font-weight: 500; cursor: pointer; - transition: color 0.15s, border-color 0.15s, background 0.15s; + transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, transform 0.15s ease; + position: relative; + overflow: hidden; +} + +.sidebar-tabs button::before { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%) scaleX(0); + width: 60%; + height: 2px; + background: var(--accent); + border-radius: 1px; + transition: transform 0.25s ease; } .sidebar-tabs button:hover { color: var(--text); - background: var(--hover); + background: color-mix(in srgb, var(--text) 3%, transparent); + transform: translateY(-1px); } .sidebar-tabs button.on { color: var(--accent); - border-bottom-color: var(--accent); font-weight: 600; background: color-mix(in srgb, var(--accent) 4%, transparent); } +.sidebar-tabs button.on::before { + transform: translateX(-50%) scaleX(1); +} + .sidebar-tabs button.on:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); } @@ -461,20 +585,21 @@ html[data-bg='22'] .app-root { .file-filter-input { width: 100%; - padding: 7px 10px; + padding: 8px 12px; border: 1px solid var(--border); - border-radius: 7px; + border-radius: 9px; background: var(--btn-bg); color: var(--text); font-size: 13px; font-family: inherit; outline: none; - transition: border-color 0.2s, box-shadow 0.2s; + transition: border-color 0.25s ease, box-shadow 0.25s ease, transform 0.15s ease; } .file-filter-input:focus { border-color: var(--accent); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent), 0 2px 8px color-mix(in srgb, var(--accent) 10%, transparent); + transform: translateY(-0.5px); } .file-filter-input::placeholder { @@ -494,9 +619,10 @@ html[data-bg='22'] .app-root { flex-direction: row; align-items: center; gap: 8px; - padding: 10px 12px; + padding: 8px 10px; border-bottom: 1px solid var(--border); flex-wrap: wrap; + background: color-mix(in srgb, var(--panel-bg) 95%, var(--border)); } .path-row-actions { @@ -513,26 +639,40 @@ html[data-bg='22'] .app-root { } .path-row button { - padding: 6px 12px; - border-radius: 6px; - border: 1px solid var(--border); - background: var(--btn-bg); - color: var(--text); - font-size: 12px; + padding: 4px 8px; + border-radius: 5px; + border: none; + background: transparent; + color: var(--muted); + font-size: 14px; cursor: pointer; - transition: border-color 0.15s, background 0.15s; + transition: color 0.15s, background 0.15s; + line-height: 1; } .path-row button:hover:not(:disabled) { - border-color: var(--accent); - background: color-mix(in srgb, var(--accent) 6%, var(--btn-bg)); + color: var(--text); + background: var(--hover); } .path-row button:disabled { - opacity: 0.45; + opacity: 0.35; cursor: not-allowed; } +.path-label { + flex: 1; + min-width: 0; + font-size: 12px; + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 4px 6px; + border-radius: 4px; + background: var(--hover); +} + .path { flex: 1; font-size: 12px; @@ -604,20 +744,40 @@ html[data-bg='22'] .app-root { .dir-item { width: 100%; text-align: left; - padding: 8px 14px; + padding: 9px 14px; border: none; background: transparent; color: inherit; display: flex; align-items: center; - gap: 8px; + gap: 10px; font-size: 13px; - border-radius: 6px; - transition: background 0.12s; + border-radius: 8px; + transition: background 0.2s ease, color 0.2s ease, transform 0.15s ease; + position: relative; + overflow: hidden; } .dir-item:hover { - background: var(--hover); + background: color-mix(in srgb, var(--text) 4%, transparent); +} + +.dir-item.dir-item--active { + background: color-mix(in srgb, var(--accent) 15%, transparent); + color: var(--accent); + font-weight: 500; +} + +.dir-item.dir-item--active::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 60%; + background: var(--accent); + border-radius: 0 3px 3px 0; } .outline-title, @@ -857,15 +1017,15 @@ html[data-bg='22'] .app-root { overflow: auto; flex: 1; min-height: 0; - padding: 6px 8px; + padding: 8px 6px; } .outline-tree-row { display: flex; align-items: center; - gap: 6px; + gap: 8px; min-width: 0; - padding: 4px 10px 4px 8px; + padding: 5px 12px 5px 8px; border-radius: 6px; transition: background 0.12s ease; } @@ -874,39 +1034,49 @@ html[data-bg='22'] .app-root { background: var(--hover); } -.outline-fold-btn { - flex: 0 0 24px; - width: 24px; - height: 24px; - padding: 0; - border: 1px solid var(--border); - border-radius: 5px; - background: var(--btn-bg); +.outline-tree-row.is-active { + background: color-mix(in srgb, var(--accent) 12%, transparent); +} + +.outline-tree-row.is-active .outline-tree-title { color: var(--accent); - font-size: 11px; + font-weight: 600; +} + +.outline-fold-btn { + flex: 0 0 20px; + width: 20px; + height: 20px; + padding: 0; + border: none; + border-radius: 4px; + background: transparent; + color: var(--muted); + font-size: 10px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; - transition: background 0.15s, border-color 0.15s, transform 0.15s; + transition: background 0.15s, color 0.15s, transform 0.15s; } .outline-fold-btn:hover { - border-color: var(--accent); - background: color-mix(in srgb, var(--accent) 12%, var(--btn-bg)); - transform: scale(1.08); + color: var(--accent); + background: color-mix(in srgb, var(--accent) 10%, transparent); } .outline-fold-btn.is-collapsed { - color: var(--muted); - border-color: var(--border); - background: transparent; + transform: rotate(-90deg); +} + +.outline-fold-btn:not(.is-collapsed) { + color: var(--accent); } .outline-fold-spacer { - flex: 0 0 24px; - width: 24px; + flex: 0 0 20px; + width: 20px; } .outline-tree-title { @@ -917,10 +1087,10 @@ html[data-bg='22'] .app-root { background: transparent; color: var(--text); font: inherit; - padding: 5px 8px; + padding: 4px 8px; border-radius: 5px; cursor: pointer; - line-height: 1.4; + line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -929,66 +1099,64 @@ html[data-bg='22'] .app-root { .outline-tree-title:hover { color: var(--accent); - background: color-mix(in srgb, var(--accent) 6%, transparent); } .outline-tree-title--h1 { font-weight: 700; font-size: 14px; - letter-spacing: 0.01em; + letter-spacing: 0.02em; } .outline-tree-title--h2 { - font-weight: 650; - font-size: 13.5px; + font-weight: 600; + font-size: 13px; } .outline-tree-title--h3 { - font-weight: 600; - font-size: 13px; + font-weight: 550; + font-size: 12.5px; } .outline-tree-title--h4, .outline-tree-title--h5, .outline-tree-title--h6 { font-weight: 500; - font-size: 12.5px; + font-size: 12px; color: var(--muted); } .outline-tree { list-style: none; margin: 0; - padding: 4px 0 8px; + padding: 4px 0; } .outline-tree .outline-tree { - margin: 2px 0 4px 8px; - padding: 2px 0 2px 14px; - border-left: 2px solid color-mix(in srgb, var(--accent) 18%, var(--border)); + margin: 2px 0; + padding: 2px 0 2px 12px; + border-left: 1px solid color-mix(in srgb, var(--accent) 20%, var(--border)); position: relative; } .outline-tree .outline-tree::before { content: ''; position: absolute; - left: -2px; - top: 0; + left: -1px; + top: 12px; bottom: 0; - width: 2px; + width: 1px; border-radius: 1px; - background: linear-gradient( - to bottom, - color-mix(in srgb, var(--accent) 30%, transparent), - color-mix(in srgb, var(--accent) 8%, transparent) 60%, - transparent - ); + background: var(--border); } .outline-tree-node { margin: 0; } +.outline-tree-node + .outline-tree-node { + margin-top: 2px; +} + .outline-empty { margin: 16px 12px; font-size: 13px; @@ -1093,8 +1261,9 @@ html[data-bg='22'] .app-root { .history-toolbar { display: flex; align-items: center; + justify-content: space-between; gap: 8px; - padding: 10px 14px 8px; + padding: 6px 10px; border-bottom: 1px solid var(--border); flex-shrink: 0; } @@ -1113,27 +1282,26 @@ html[data-bg='22'] .app-root { } .history-clear-btn { - padding: 4px 10px; + padding: 3px 8px; font-size: 11px; - border-radius: 5px; - border: 1px solid var(--border); - background: var(--btn-bg); + border-radius: 4px; + border: none; + background: transparent; color: var(--muted); cursor: pointer; font: inherit; - transition: border-color 0.15s, color 0.15s, background 0.15s; + transition: color 0.15s, background 0.15s; } .history-clear-btn:hover { - border-color: #e53e3e; color: #e53e3e; - background: color-mix(in srgb, #e53e3e 6%, var(--btn-bg)); + background: color-mix(in srgb, #e53e3e 8%, transparent); } .history-list { list-style: none; margin: 0; - padding: 6px 6px 8px; + padding: 4px; flex: 1; overflow: auto; } @@ -1142,15 +1310,15 @@ html[data-bg='22'] .app-root { width: 100%; display: flex; flex-direction: row; - align-items: flex-start; + align-items: center; gap: 8px; text-align: left; border: none; background: transparent; color: inherit; font: inherit; - padding: 10px 12px; - border-radius: 8px; + padding: 7px 10px; + border-radius: 6px; cursor: pointer; transition: background 0.12s; } @@ -1165,33 +1333,42 @@ html[data-bg='22'] .app-root { display: inline-flex; align-items: center; justify-content: center; - width: 20px; - height: 20px; + width: 22px; + height: 22px; margin-top: 1px; color: var(--muted); + transition: color 0.2s ease; } .dir-item-icon svg, .history-item-icon svg { - width: 16px; - height: 16px; + width: 17px; + height: 17px; } .dir-item[data-kind='dir'] .dir-item-icon { color: var(--accent); } +.dir-item[data-kind='file'] .dir-item-icon { + color: color-mix(in srgb, var(--muted) 70%, var(--text)); +} + +.dir-item:hover .dir-item-icon { + color: var(--accent); +} + .history-item-body { flex: 1; min-width: 0; display: flex; flex-direction: column; - gap: 2px; + gap: 1px; } .history-item-name { - font-weight: 600; - font-size: 13px; + font-weight: 500; + font-size: 12px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; @@ -1199,7 +1376,7 @@ html[data-bg='22'] .app-root { } .history-item-path { - font-size: 11px; + font-size: 10px; color: var(--muted); max-width: 100%; overflow: hidden; @@ -1315,36 +1492,48 @@ html[data-bg='22'] .app-root { .search-bar { display: flex; align-items: center; - gap: 8px; - padding: 10px 14px; + gap: 10px; + padding: 12px 16px; border-bottom: 1px solid var(--border); background: var(--panel-bg); z-index: 26; + position: relative; +} + +.search-bar::after { + content: ''; + position: absolute; + bottom: 0; + left: 16px; + right: 16px; + height: 1px; + background: linear-gradient(to right, transparent, var(--border), transparent); } .search-input { flex: 1; - padding: 8px 14px; - border-radius: 8px; + padding: 9px 16px; + border-radius: 10px; border: 1px solid var(--border); background: var(--btn-bg); color: var(--text); font: inherit; font-size: 13px; - transition: border-color 0.15s, box-shadow 0.15s; + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; } .search-input:focus { outline: none; border-color: var(--accent); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent), 0 2px 8px color-mix(in srgb, var(--accent) 10%, transparent); + transform: translateY(-0.5px); } .search-close-btn { flex: 0 0 32px; height: 32px; border: 1px solid var(--border); - border-radius: 6px; + border-radius: 8px; background: var(--btn-bg); color: var(--muted); cursor: pointer; @@ -1352,20 +1541,21 @@ html[data-bg='22'] .app-root { display: flex; align-items: center; justify-content: center; - transition: border-color 0.15s, color 0.15s, background 0.15s; + transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, transform 0.15s ease; } .search-close-btn:hover { color: var(--text); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 6%, var(--btn-bg)); + transform: translateY(-1px); } .search-next-btn { flex: 0 0 32px; height: 32px; border: 1px solid var(--border); - border-radius: 6px; + border-radius: 8px; background: var(--btn-bg); color: var(--accent); cursor: pointer; @@ -1374,12 +1564,13 @@ html[data-bg='22'] .app-root { display: flex; align-items: center; justify-content: center; - transition: border-color 0.15s, background 0.15s; + transition: border-color 0.2s ease, background 0.2s ease, transform 0.15s ease; } .search-next-btn:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--btn-bg)); + transform: translateY(-1px); } .status-bar { @@ -1509,10 +1700,10 @@ html[data-bg='22'] .app-root { .stats-panel-body { flex: 1; overflow: auto; - padding: 10px 12px 14px; + padding: 12px 14px 16px; display: flex; flex-direction: column; - gap: 14px; + gap: 16px; } .stats-panel-body::-webkit-scrollbar { @@ -1529,45 +1720,52 @@ html[data-bg='22'] .app-root { } .stats-panel-section-title { - font-size: 10px; + font-size: 11px; font-weight: 600; color: var(--muted); - margin-bottom: 6px; - padding-left: 2px; + margin-bottom: 8px; + padding-left: 4px; + letter-spacing: 0.04em; + text-transform: uppercase; } .stats-panel-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 4px; + gap: 8px; } .stats-panel-item { display: flex; flex-direction: column; align-items: center; - padding: 8px 6px; - border-radius: 6px; + padding: 10px 8px; + border-radius: 10px; background: var(--btn-bg); border: 1px solid var(--border); - transition: border-color 0.15s; + transition: border-color 0.2s ease, background 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease; } .stats-panel-item:hover { - border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 5%, var(--btn-bg)); + transform: translateY(-1px); + box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 10%, transparent); } .stats-panel-value { - font-size: 15px; + font-size: 16px; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; } .stats-panel-label { - font-size: 10px; + font-size: 11px; color: var(--muted); - margin-top: 2px; + margin-top: 3px; + font-weight: 500; } .stats-panel-sign { @@ -1682,41 +1880,21 @@ html[data-bg='22'] .app-root { padding: 0 8px 8px; } -.preview-resizer { - position: absolute; - top: 0; - right: -4px; - width: 6px; - height: 100%; - cursor: ew-resize; - z-index: 5; - border-radius: 3px; - background: var(--accent); - opacity: 0; - transition: opacity 0.18s ease; -} -.preview-shell:hover .preview-resizer, -.preview-resizer:active { - opacity: 0.6; -} - -.preview-resizer:hover { - opacity: 1; -} .preview { flex: 0 0 auto; overflow: visible; - padding: 28px 36px 52px; + padding: 32px 40px 56px; width: 100%; font-size: 16px; - line-height: var(--preview-line-height, 1.7); - background: var(--preview-bg); + line-height: var(--preview-line-height, 1.75); + background: transparent; color: var(--preview-text); - border-radius: 10px; - box-shadow: var(--preview-shadow); + border-radius: 0; + box-shadow: none; font-family: var(--font-preview, var(--font-ui, Georgia, serif)); + position: relative; } .preview code { @@ -1729,11 +1907,12 @@ html[data-bg='22'] .app-root { .preview .md-code-wrap { position: relative; - margin: 1.2em 0; - border: 1px solid color-mix(in srgb, var(--border) 90%, #000); - border-radius: 8px; + margin: 1.4em 0; + border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); + border-radius: 10px; background: var(--code-bg); overflow: hidden; + box-shadow: 0 2px 8px color-mix(in srgb, #000 4%, transparent); } .preview .md-code-wrap .md-code-lang { @@ -1836,6 +2015,122 @@ html[data-bg='22'] .app-root { overflow: visible; } +.preview .md-diagram-wrap .diagram-zoom-btn { + position: absolute; + top: 8px; + right: 8px; + width: 32px; + height: 32px; + border: none; + border-radius: 8px; + background: color-mix(in srgb, var(--accent) 10%, var(--preview-bg)); + color: var(--accent); + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + opacity: 0; + visibility: hidden; +} + +.preview .md-diagram-wrap:hover .diagram-zoom-btn { + opacity: 1; + visibility: visible; +} + +.preview .md-diagram-wrap .diagram-zoom-btn:hover { + background: var(--accent); + color: white; + transform: scale(1.1); + box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 25%, transparent); +} + +.preview .md-diagram-wrap { + position: relative; +} + +.diagram-zoom-overlay { + position: fixed; + inset: 0; + background: color-mix(in srgb, black 75%, transparent); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.diagram-zoom-modal { + position: relative; + background: var(--preview-bg); + border-radius: 12px; + box-shadow: 0 20px 60px color-mix(in srgb, black 40%, transparent); + max-width: 90vw; + max-height: 90vh; + overflow: hidden; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: scale(0.9) translateY(20px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.diagram-zoom-close { + position: absolute; + top: 12px; + right: 12px; + width: 36px; + height: 36px; + border: none; + border-radius: 50%; + background: color-mix(in srgb, black 20%, transparent); + color: white; + font-size: 18px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + z-index: 10; +} + +.diagram-zoom-close:hover { + background: color-mix(in srgb, black 40%, transparent); + transform: scale(1.1); +} + +.diagram-zoom-content { + padding: 24px; + max-height: 85vh; + overflow: auto; +} + +.diagram-zoom-svg { + display: flex; + justify-content: center; + align-items: flex-start; +} + +.diagram-zoom-svg svg { + max-width: 100%; + height: auto; + display: block; +} + .preview .md-diagram-wrap .mermaid-render-host svg g[data-et='note'] text, .preview .md-diagram-wrap .mermaid-render-host svg g[data-et='note'] .noteText { overflow: visible; @@ -1875,46 +2170,96 @@ html[data-bg='22'] .app-root { .preview h5, .preview h6 { font-family: inherit; - color: inherit; - margin-top: 1.3em; - margin-bottom: 0.6em; - line-height: 1.3; + margin-top: 2em; + margin-bottom: 0.8em; + line-height: 1.2; + position: relative; + scroll-margin-top: 60px; +} + +.preview p { + margin-top: 1em; + margin-bottom: 1em; + line-height: var(--preview-line-height, 1.75); } .preview h1 { - font-size: 2rem; - font-weight: 700; - margin-top: 0.4em; - padding-bottom: 0.25em; - border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent); + font-size: 2.1rem; + font-weight: 800; + margin-top: 0.3em; + margin-bottom: 1em; + padding-bottom: 0.35em; + border-bottom: 2px solid color-mix(in srgb, var(--accent) 35%, var(--border)); + color: var(--text); + letter-spacing: -0.02em; +} + +.preview h1::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 80px; + height: 2px; + background: var(--accent); + border-radius: 1px; } .preview h2 { - font-size: 1.6rem; - font-weight: 650; + font-size: 1.65rem; + font-weight: 700; + margin-top: 1.8em; + padding-left: 12px; + border-left: 4px solid var(--accent); + color: var(--text); } .preview h3 { - font-size: 1.3rem; - font-weight: 600; + font-size: 1.35rem; + font-weight: 650; + margin-top: 1.6em; + padding-left: 10px; + border-left: 3px solid color-mix(in srgb, var(--accent) 70%, var(--border)); + color: color-mix(in srgb, var(--text) 95%, var(--muted)); } .preview h4 { - font-size: 1.15rem; - font-weight: 600; + font-size: 1.2rem; + font-weight: 650; + margin-top: 1.4em; + padding-left: 8px; + border-left: 3px solid color-mix(in srgb, var(--accent) 50%, var(--border)); + color: color-mix(in srgb, var(--text) 90%, var(--muted)); } .preview h5 { - font-size: 1.05rem; + font-size: 1.1rem; font-weight: 600; + margin-top: 1.3em; + padding-left: 6px; + border-left: 2px solid color-mix(in srgb, var(--accent) 40%, var(--border)); + color: color-mix(in srgb, var(--text) 85%, var(--muted)); } .preview h6 { - font-size: 0.95rem; + font-size: 1rem; font-weight: 600; + margin-top: 1.2em; + padding-left: 6px; + border-left: 2px solid var(--border); color: var(--muted); } +.preview h1:hover, +.preview h2:hover, +.preview h3:hover, +.preview h4:hover, +.preview h5:hover, +.preview h6:hover { + color: var(--accent); + transition: color 0.2s ease; +} + .preview pre { overflow: auto; padding: 14px; diff --git a/src/renderer/src/markdown.ts b/src/renderer/src/markdown.ts index b105c28..5fc7d7a 100644 --- a/src/renderer/src/markdown.ts +++ b/src/renderer/src/markdown.ts @@ -170,9 +170,8 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => { if (isMermaidFence(lang, raw)) { const trimmed = raw.trim() - const escaped = md.utils.escapeHtml(trimmed) const dataSrc = md.utils.escapeHtml(encodeURIComponent(trimmed)) - return `
${escaped}
` + return `
${trimmed}
` } const highlighted = highlightCodeBlock(raw, lang) @@ -184,7 +183,7 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => { const purifyOpts: Parameters[1] = { USE_PROFILES: { html: true }, - ADD_TAGS: ['span', 'mark', 'kbd', 'sup', 'sub', 'details', 'summary', 'font', 'div'], + ADD_TAGS: ['span', 'mark', 'kbd', 'sup', 'sub', 'details', 'summary', 'font', 'div', 'b', 'strong', 'em', 'i', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'], ADD_ATTR: [ 'style', 'color', @@ -228,7 +227,8 @@ export type RenderMarkdownEnv = { } export function renderMarkdown(src: string, env?: RenderMarkdownEnv): string { - const input = typeof src === 'string' ? src : '' + let input = typeof src === 'string' ? src : '' + input = input.replace(/([^\n])(#{1,6}\s)/g, (_match, p1, p2) => `${p1}\n${p2}`) const renderEnv = env ?? {} try { const raw = md.render(input, renderEnv) @@ -274,13 +274,36 @@ export function isValidOutlineHeading(displayText: string, rawLine: string): boo return true } +const FENCE_OPEN_RE = /^(\s*)(`{3,}|~{3,})(.*)$/ + +function isFenceClose(line: string, fenceChar: string, fenceLen: number): boolean { + return new RegExp(`^\\s*\\${fenceChar}{${fenceLen},}\\s*$`).test(line) +} + /** 仅从 ATX `#` 标题提取;`line` 为 0-based。 */ export function extractOutline(src: string): OutlineItem[] { const input = typeof src === 'string' ? src : '' const lines = input.split(/\r?\n/) const out: OutlineItem[] = [] + let inFence = false + for (let i = 0; i < lines.length; i++) { const raw = lines[i] ?? '' + + const fenceOpen = FENCE_OPEN_RE.exec(raw) + if (fenceOpen) { + inFence = true + const fenceChar = fenceOpen[2]![0]! + const fenceLen = fenceOpen[2]!.length + while (i < lines.length && !isFenceClose(lines[i] ?? '', fenceChar, fenceLen)) { + i++ + } + inFence = false + continue + } + + if (inFence) continue + const m = /^(#{1,6})\s+(.+)$/.exec(raw) if (!m) continue const level = m[1]!.length