From c0ba8df9b654912e268937c610b912226c2a3a35 Mon Sep 17 00:00:00 2001 From: hunter-dev Date: Thu, 10 Sep 2026 00:41:24 +0800 Subject: [PATCH] =?UTF-8?q?perf(renderer):=20=E6=95=B4=E4=BD=93=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=93=8D=E5=BA=94=E9=80=9F=E5=BA=A6=20=E2=80=94=20rAF?= =?UTF-8?q?=20=E5=90=88=E5=B9=B6=E7=BC=96=E8=BE=91=E5=99=A8=E5=88=B7?= =?UTF-8?q?=E6=96=B0/=E8=87=AA=E5=8A=A8=E4=BF=9D=E5=AD=98=E7=BC=93?= =?UTF-8?q?=E5=AD=98/=E7=B2=92=E5=AD=90=E5=BC=95=E6=93=8E=E7=A9=BA?= =?UTF-8?q?=E9=97=B2=E8=87=AA=E5=81=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同时: 不透明窗口 + 精简主题(8到2)、TabBar 简化、图谱宽松布局、MoreActions 层级修复 --- .gitignore | 4 + apps/desktop/src/main/index.ts | 4 +- apps/desktop/src/renderer/App.tsx | 9 +- .../features/appearance/AppearancePanel.tsx | 2 +- .../features/appearance/appearance-store.ts | 2 +- .../renderer/features/appearance/themes.ts | 65 +++++-- .../renderer/features/dialog/InputDialog.tsx | 2 +- .../renderer/features/dialog/NotifyLayer.tsx | 2 +- .../features/doc-info/DocInfoPanel.tsx | 2 +- .../src/renderer/features/editor/Editor.tsx | 2 +- .../renderer/features/editor/TiptapEditor.tsx | 14 +- .../features/effects/effect-engine.ts | 26 ++- .../renderer/features/file-tree/FileTree.tsx | 17 +- .../src/renderer/features/graph/GraphView.tsx | 67 ++++--- .../src/renderer/features/graph/LinkGraph.tsx | 164 +++++++++++++----- .../renderer/features/search/SearchBox.tsx | 9 +- .../features/settings/SettingsPanel.tsx | 2 +- .../src/renderer/features/shell/StatusBar.tsx | 2 +- .../src/renderer/features/shell/TitleBar.tsx | 2 +- .../src/renderer/features/tabs/TabBar.tsx | 19 +- .../src/renderer/features/toolbar/Toolbar.tsx | 6 +- apps/desktop/src/renderer/store.ts | 5 +- apps/desktop/src/renderer/styles.css | 117 ++++++++++++- apps/desktop/tests/e2e/button-all.spec.ts | 15 +- apps/desktop/tests/e2e/graph-render.spec.ts | 7 +- apps/desktop/tests/e2e/graph.spec.ts | 3 +- .../tests/e2e/transparency-check.spec.ts | 26 +-- 27 files changed, 417 insertions(+), 178 deletions(-) diff --git a/.gitignore b/.gitignore index 201eb1a..4b68355 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ test-results/ playwright-report/ apps/desktop/tests/e2e/.report/ release/ + +# stray temp dir from os.tmpdir() env quirk +apps/desktop/undefined/ + diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index f3e9968..d9a34a6 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -22,8 +22,8 @@ function createWindow() { const w = new BrowserWindow({ width: 1200, height: 760, minWidth: 900, minHeight: 580, center: true, frame: false, show: false, - transparent: true, backgroundColor: '#00000000', backgroundMaterial: 'none', - // 真透明:透明主题下窗口直接透出下层软件(不用 acrylic 模糊,保证可清晰看到背后内容) + transparent: false, backgroundColor: '#f6f7f9', backgroundMaterial: 'none', + // UI 重新设计:窗口不透明,backgroundColor 用 L0 空间底色,稳定呈现玻璃材质(玻璃透过的是窗口内的空间层) webPreferences: { preload: nodePath.join(__dirname, '../preload/index.mjs'), contextIsolation: true, nodeIntegration: false, sandbox: false diff --git a/apps/desktop/src/renderer/App.tsx b/apps/desktop/src/renderer/App.tsx index 7fb86d7..e1bbbd2 100644 --- a/apps/desktop/src/renderer/App.tsx +++ b/apps/desktop/src/renderer/App.tsx @@ -138,7 +138,8 @@ export function App() { // 编辑器样式随设置生效(FR-编辑器设置真实生效) useEffect(() => { document.documentElement.style.setProperty('--editor-font-size', settings.fontSize + 'px'); document.documentElement.style.setProperty('--editor-font-family', settings.fontFamily); document.documentElement.style.setProperty('--editor-white-space', settings.wordWrap ? 'pre-wrap' : 'pre'); }, [settings.fontSize, settings.fontFamily, settings.wordWrap]); return ( -
+
+
{appearance.bgImage &&
}
setView(v => v === 'editor' ? 'graph' : 'editor')} onSettings={() => setShowSettings(true)} onAppearance={() => setShowAppearance(true)} onToggleSidebar={() => layout.toggleSidebar()} onToggleMinimal={() => layout.toggleMinimal()} onToggleTabbar={() => layout.toggleTabbar()} minimal={layout.minimal} tabbarVisible={layout.tabbarVisible} /> @@ -147,7 +148,7 @@ export function App() {
{layout.minimal ? () : (<> {layout.sidebarVisible && (<> -
+
@@ -157,8 +158,8 @@ export function App() { {view === 'editor' ? : } {view === 'editor' && layout.outlineVisible && (<> layout.setOutlineWidth(layout.outlineWidth + d)} /> -
-
+
+
diff --git a/apps/desktop/src/renderer/features/appearance/AppearancePanel.tsx b/apps/desktop/src/renderer/features/appearance/AppearancePanel.tsx index 193f6c2..cf3a9b1 100644 --- a/apps/desktop/src/renderer/features/appearance/AppearancePanel.tsx +++ b/apps/desktop/src/renderer/features/appearance/AppearancePanel.tsx @@ -18,7 +18,7 @@ export function AppearancePanel({ onClose }: { onClose: () => void }) { const colors: [keyof typeof draft, string][] = [['accent','强调色'],['glow','辉光色'],['accentText','强调色文字'],['textPrimary','主要文字'],['textSecondary','辅助文字'],['textTertiary','弱化文字'],['controlBorder','控件边框'],['bgPrimary','工作区背景'],['bgSecondary','浮层背景']]; return (
{ if (e.target === e.currentTarget) { revert(); onClose(); } }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1500 }}> -
+
外观定制
diff --git a/apps/desktop/src/renderer/features/appearance/appearance-store.ts b/apps/desktop/src/renderer/features/appearance/appearance-store.ts index 1f6b574..95fbf01 100644 --- a/apps/desktop/src/renderer/features/appearance/appearance-store.ts +++ b/apps/desktop/src/renderer/features/appearance/appearance-store.ts @@ -23,7 +23,7 @@ interface AppearanceState { isDirty: () => boolean; } -const defaultTheme = THEMES[1]; // 午夜蓝为默认 +const defaultTheme = THEMES[0]; // 亮色档为默认(UI 重新设计:默认亮色 + 暗色档) // 降采样取像素(渲染层 canvas 可用) function sampleImage(dataUrl: string, size = 48): Promise { diff --git a/apps/desktop/src/renderer/features/appearance/themes.ts b/apps/desktop/src/renderer/features/appearance/themes.ts index b9c7677..58edea0 100644 --- a/apps/desktop/src/renderer/features/appearance/themes.ts +++ b/apps/desktop/src/renderer/features/appearance/themes.ts @@ -1,4 +1,7 @@ -// 8 套主题定义(FR-A1/A2)——CSS 变量驱动 +// 两档主题(亮/暗)——克制的液态玻璃质感 · 黑白灰基调(UI 重新设计方案) +// 设计 Token 见 styles.css 与设计文档: +// L0 space / L1 glass / L2 paper / L3 float +// ink 墨色阶 / line 分隔线 / radius / shadow / ease export interface ThemeParams { accent: string; glow: string; accentText: string; textPrimary: string; textSecondary: string; textTertiary: string; @@ -8,21 +11,33 @@ export interface ThemeParams { export interface ThemeDef { id: string; name: string; group: 'dark' | 'light'; desc: string; params: ThemeParams; } export const THEMES: ThemeDef[] = [ - { id: 'transparent', name: '全透明', group: 'dark', desc: '窗口直接透出桌面', params: { accent:'#4a9eff', glow:'#6a8cff', accentText:'#7ab8ff', textPrimary:'#e8e8e8', textSecondary:'#b8b8c8', textTertiary:'#8a8a9a', controlBorder:'#333', bgPrimary:'transparent', bgSecondary:'rgba(20,20,28,0.5)', blurPx:20, surfaceOpacity:0.5, radius:8 } }, - { id: 'midnight', name: '午夜蓝', group: 'dark', desc: '深邃午夜蓝调', params: { accent:'#4a9eff', glow:'#6a8cff', accentText:'#7ab8ff', textPrimary:'#e8e8e8', textSecondary:'#b8b8c8', textTertiary:'#8a8a9a', controlBorder:'#2a3548', bgPrimary:'#0d1525', bgSecondary:'#141d30', blurPx:16, surfaceOpacity:0.6, radius:8 } }, - { id: 'purple-night', name: '深紫夜', group: 'dark', desc: '神秘紫夜', params: { accent:'#a06aff', glow:'#b88aff', accentText:'#c4a3ff', textPrimary:'#e8e8f0', textSecondary:'#b8b8c8', textTertiary:'#8a8a9a', controlBorder:'#2a2438', bgPrimary:'#1a1228', bgSecondary:'#241a35', blurPx:16, surfaceOpacity:0.6, radius:8 } }, - { id: 'forest', name: '森林绿', group: 'dark', desc: '静谧森林', params: { accent:'#5abf7a', glow:'#7ad99a', accentText:'#7ad99a', textPrimary:'#e8f0e8', textSecondary:'#b8c8b8', textTertiary:'#8a9a8a', controlBorder:'#243028', bgPrimary:'#102018', bgSecondary:'#1a2a22', blurPx:16, surfaceOpacity:0.6, radius:8 } }, - { id: 'amber', name: '暖琥珀', group: 'dark', desc: '温暖琥珀光', params: { accent:'#e0a040', glow:'#f0c060', accentText:'#f0c060', textPrimary:'#f0e8d8', textSecondary:'#c8c0b0', textTertiary:'#9a8a7a', controlBorder:'#3a3028', bgPrimary:'#1f1810', bgSecondary:'#2a2018', blurPx:16, surfaceOpacity:0.6, radius:8 } }, - { id: 'latte', name: '奶霜拿铁', group: 'light', desc: '柔润奶白', params: { accent:'#a06840', glow:'#c08860', accentText:'#8a5830', textPrimary:'#3a3028', textSecondary:'#6a5a4a', textTertiary:'#6a5a4a', controlBorder:'#d8c8b8', bgPrimary:'#f5efe6', bgSecondary:'#ede5d8', blurPx:10, surfaceOpacity:0.8, radius:8 } }, - { id: 'daylight', name: '晴空白昼', group: 'light', desc: '清爽天蓝', params: { accent:'#2070c0', glow:'#4090e0', accentText:'#1860a8', textPrimary:'#2a3a4a', textSecondary:'#4a5a6a', textTertiary:'#5a6a7a', controlBorder:'#c8d8e8', bgPrimary:'#f0f5fa', bgSecondary:'#e0eaf2', blurPx:10, surfaceOpacity:0.85, radius:8 } }, - { id: 'plain', name: '素纸简白', group: 'light', desc: '极简素纸', params: { accent:'#555', glow:'#888', accentText:'#333', textPrimary:'#222', textSecondary:'#555', textTertiary:'#666', controlBorder:'#ddd', bgPrimary:'#fff', bgSecondary:'#f5f5f5', blurPx:0, surfaceOpacity:0.9, radius:6 } }, + { + id: 'light', name: '亮色', group: 'light', desc: '通透玻璃 · 亮色档(默认)', + params: { + accent: '#262a33', glow: '#3c414b', accentText: '#ffffff', + textPrimary: '#16181d', textSecondary: '#3c414b', textTertiary: '#6d7480', + controlBorder: 'rgba(20,22,28,0.10)', bgPrimary: '#eef0f4', bgSecondary: 'rgba(255,255,255,0.62)', + blurPx: 18, surfaceOpacity: 0.62, radius: 16, + }, + }, + { + id: 'dark', name: '暗色', group: 'dark', desc: '通透玻璃 · 暗色档', + params: { + accent: '#e6e9ef', glow: '#c8ccd4', accentText: '#16181d', + textPrimary: '#eef0f4', textSecondary: '#c8ccd4', textTertiary: '#8a909c', + controlBorder: 'rgba(255,255,255,0.10)', bgPrimary: '#131519', bgSecondary: 'rgba(255,255,255,0.06)', + blurPx: 18, surfaceOpacity: 0.5, radius: 16, + }, + }, ]; export function applyTheme(params: ThemeParams, group?: 'dark' | 'light') { const root = document.documentElement; + const g = group ?? 'dark'; // 设置主题组(dark/light)供 CSS 浅色语法高亮等区分 - root.setAttribute('data-theme-group', group ?? 'dark'); + root.setAttribute('data-theme-group', g); + // 语义 token(组件沿用,收敛为黑白灰墨色阶) root.style.setProperty('--accent', params.accent); root.style.setProperty('--glow', params.glow); root.style.setProperty('--accent-text', params.accentText); @@ -35,12 +50,34 @@ export function applyTheme(params: ThemeParams, group?: 'dark' | 'light') { root.style.setProperty('--blur-px', params.blurPx + 'px'); root.style.setProperty('--surface-opacity', String(params.surfaceOpacity)); root.style.setProperty('--radius', params.radius + 'px'); - // ---- 设计 token(A档-4):圆角阶梯 + 间距,组件统一引用 ---- - root.style.setProperty('--radius-sm', '4px'); // 输入框/小元素 - root.style.setProperty('--radius-md', '6px'); // 按钮/菜单项 - root.style.setProperty('--radius-lg', params.radius + 'px'); // 面板/卡片 + // 圆角阶梯 + 间距(组件统一引用) + root.style.setProperty('--radius-sm', '10px'); + root.style.setProperty('--radius-md', '12px'); + root.style.setProperty('--radius-lg', params.radius + 'px'); root.style.setProperty('--space-1', '4px'); root.style.setProperty('--space-2', '8px'); root.style.setProperty('--space-3', '12px'); root.style.setProperty('--space-4', '16px'); + + // ---- 设计系统 Token(分层材质)---- + const L = g === 'light'; + const set = (k: string, v: string) => root.style.setProperty(k, v); + set('--space', L ? '#f6f7f9' : '#141519'); // L0 窗口底色(更白更高级) + // L0 环境氛围(Apple 一束顶光渐变),亮/暗两档 + set('--ambient', L + ? 'radial-gradient(120% 95% at 12% -10%, #ffffff 0%, rgba(255,255,255,0) 55%), radial-gradient(70% 60% at 100% 5%, #f7f8fa 0%, rgba(247,248,250,0) 55%), linear-gradient(168deg, #fafbfc 0%, #f2f4f6 100%)' + : 'radial-gradient(120% 95% at 12% -10%, #2b2e34 0%, rgba(43,46,52,0) 55%), radial-gradient(70% 60% at 100% 5%, #22252a 0%, rgba(34,37,42,0) 55%), linear-gradient(168deg, #191a1e 0%, #101114 100%)'); + set('--glass-side', L ? 'rgba(255,255,255,0.55)' : 'rgba(255,255,255,0.06)'); // L1 侧栏 + set('--glass-bar', L ? 'rgba(255,255,255,0.62)' : 'rgba(255,255,255,0.08)'); // L1 栏 + set('--paper', L ? '#ffffff' : '#1b1d22'); // L2 纸(编辑正文) + set('--float', L ? 'rgba(255,255,255,0.98)' : '#23262c'); // L3 悬浮 + set('--ink', L ? '#16181d' : '#eef0f4'); + set('--ink-2', L ? '#3c414b' : '#c8ccd4'); + set('--ink-3', L ? '#6d7480' : '#8a909c'); + set('--ink-4', L ? '#9aa1ad' : '#5c616b'); + set('--line', L ? 'rgba(20,22,28,0.10)' : 'rgba(255,255,255,0.10)'); + set('--line-strong', L ? 'rgba(20,22,28,0.16)' : 'rgba(255,255,255,0.16)'); + set('--ease', 'cubic-bezier(.22,.61,.36,1)'); + set('--shadow-soft', L ? '0 1px 2px rgba(20,22,28,0.04), 0 4px 16px rgba(20,22,28,0.06)' : '0 1px 2px rgba(0,0,0,0.3), 0 4px 16px rgba(0,0,0,0.35)'); + set('--shadow-lift', L ? '0 8px 32px rgba(20,22,28,0.14), 0 2px 8px rgba(20,22,28,0.08)' : '0 8px 32px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.35)'); } diff --git a/apps/desktop/src/renderer/features/dialog/InputDialog.tsx b/apps/desktop/src/renderer/features/dialog/InputDialog.tsx index 9113e47..4243e55 100644 --- a/apps/desktop/src/renderer/features/dialog/InputDialog.tsx +++ b/apps/desktop/src/renderer/features/dialog/InputDialog.tsx @@ -17,7 +17,7 @@ export function InputDialog() { return (
{ if (e.target === e.currentTarget) close(null); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}> -
{ if (e.key === 'Escape') close(null); }} style={{ background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 8, padding: 20, minWidth: 360, boxShadow: '0 8px 32px rgba(0,0,0,0.4)' }}> +
{ if (e.key === 'Escape') close(null); }} className="float panel-enter" style={{ borderRadius: 'var(--radius-lg)', padding: 20, minWidth: 360, border: '1px solid var(--line)' }}>
{title}
{mode === 'confirm' || mode === 'alert' ? (
{message}
diff --git a/apps/desktop/src/renderer/features/dialog/NotifyLayer.tsx b/apps/desktop/src/renderer/features/dialog/NotifyLayer.tsx index 9b2c100..4afca05 100644 --- a/apps/desktop/src/renderer/features/dialog/NotifyLayer.tsx +++ b/apps/desktop/src/renderer/features/dialog/NotifyLayer.tsx @@ -1,6 +1,6 @@ import { useNotify } from './notify-store'; export function NotifyLayer() { const { items, remove } = useNotify(); return
{items.map(n => ( -
remove(n.id)} style={{ padding: '10px 16px', borderRadius: 6, fontSize: 13, cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.3)', background: n.type === 'success' ? '#2a4a3a' : n.type === 'error' ? '#4a2a2a' : 'var(--control-border)', color: n.type === 'success' ? '#9fc' : n.type === 'error' ? '#f88' : 'var(--accent-text)' }}>{n.msg}
+
remove(n.id)} className="float panel-enter" style={{ padding: '10px 16px', borderRadius: 'var(--radius-md)', fontSize: 13, cursor: 'pointer', background: n.type === 'success' ? '#2a4a3a' : n.type === 'error' ? '#4a2a2a' : 'var(--float)', color: n.type === 'success' ? '#9fc' : n.type === 'error' ? '#f88' : 'var(--text-primary)' }}>{n.msg}
))}
; } \ No newline at end of file diff --git a/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx b/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx index ebeb5f6..757e717 100644 --- a/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx +++ b/apps/desktop/src/renderer/features/doc-info/DocInfoPanel.tsx @@ -86,7 +86,7 @@ export function DocInfoPanel() { {/* 快速双向关联 */}
- + setQ(e.target.value)} placeholder="搜索(Ctrl+Shift+F)" style={{ flex: 1, background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', color: 'var(--text-primary)', borderRadius: 4, padding: '4px 8px', fontSize: 13 }} /> - {q && } +
+
+ + setQ(e.target.value)} placeholder="搜索" style={{ width: '100%', background: 'color-mix(in srgb, var(--ink) 6%, transparent)', border: '1px solid var(--line)', color: 'var(--ink)', borderRadius: 9, padding: '7px 10px 7px 30px', fontSize: 13, boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)', outline: 'none' }} /> + {q && }
{searching &&
搜索中…
} {q && !searching && hits.length === 0 &&
没有匹配
} diff --git a/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx b/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx index bb4cfdd..b3ead1c 100644 --- a/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx +++ b/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx @@ -47,7 +47,7 @@ export function SettingsPanel({ onClose }: { onClose: () => void }) { }, [recId, onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1500 }}> -
+
设置 diff --git a/apps/desktop/src/renderer/features/shell/StatusBar.tsx b/apps/desktop/src/renderer/features/shell/StatusBar.tsx index fee7041..2fb80b3 100644 --- a/apps/desktop/src/renderer/features/shell/StatusBar.tsx +++ b/apps/desktop/src/renderer/features/shell/StatusBar.tsx @@ -12,7 +12,7 @@ export function StatusBar({ view }: { view: 'editor' | 'graph' }) { const saveText = saveState === 'saving' ? '保存中…' : saveState === 'error' ? '保存失败' : '已保存'; const saveDot = saveState === 'error' ? '#e06c75' : saveState === 'saving' ? 'var(--accent)' : 'var(--text-tertiary)'; return ( -
+
{currentPath ? {chars} 字符 · {linesCount} 行 : —}
diff --git a/apps/desktop/src/renderer/features/shell/TitleBar.tsx b/apps/desktop/src/renderer/features/shell/TitleBar.tsx index 8aac245..fd4d3dc 100644 --- a/apps/desktop/src/renderer/features/shell/TitleBar.tsx +++ b/apps/desktop/src/renderer/features/shell/TitleBar.tsx @@ -16,7 +16,7 @@ export function TitleBar({ onToggleView, view, onSettings, onAppearance, onToggl const docName = currentPath ? decodeURIComponent(currentPath.split('/').pop() || '') : ''; const vaultName = vault?.currentRoot ? vault.currentRoot.split(/[\\/]/).filter(Boolean).pop() || vault.currentRoot : ''; return ( -
+
{/* 品牌区:双击最大化 */}
diff --git a/apps/desktop/src/renderer/features/tabs/TabBar.tsx b/apps/desktop/src/renderer/features/tabs/TabBar.tsx index 1e9fec6..88ff503 100644 --- a/apps/desktop/src/renderer/features/tabs/TabBar.tsx +++ b/apps/desktop/src/renderer/features/tabs/TabBar.tsx @@ -1,13 +1,12 @@ import { useTabs } from './tab-store'; import { useStore } from '../../store'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; export function TabBar() { - const { tabs, activeId, setActive, closeTab, togglePin } = useTabs(); + const { tabs, activeId, setActive, closeTab } = useTabs(); const openDoc = useStore(s => s.openDoc); const currentPath = useStore(s => s.currentPath); const resetEditor = useStore(s => s.resetEditor); - const [hoveredTab, setHoveredTab] = useState(null); // 当 activeId 变化,加载对应标签的文档;标签全部关闭时清空编辑器 useEffect(() => { @@ -18,24 +17,16 @@ export function TabBar() { if (tabs.length === 0) return null; return ( -
+
{tabs.map(t => (
setActive(t.id)} - onMouseEnter={() => setHoveredTab(t.id)} onMouseLeave={() => setHoveredTab(null)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 10px', cursor: 'pointer', borderBottom: activeId === t.id ? '2px solid var(--accent)' : '2px solid transparent', background: activeId === t.id ? 'var(--bg-secondary)' : 'transparent', fontSize: 12, color: activeId === t.id ? 'var(--text-primary)' : 'var(--text-tertiary)', whiteSpace: 'nowrap' }}> - {t.pinned && ★} {t.path.split('/').pop()?.replace(/\.md$/, '')} - {t.pinned ? ( - // FR-T4:固定标签悬停时浮现关闭按钮 - { e.stopPropagation(); closeTab(t.id); }} style={{ color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 12, opacity: hoveredTab === t.id ? 1 : 0 }} onMouseEnter={e => (e.currentTarget.style.color = '#f88')} onMouseLeave={e => (e.currentTarget.style.color = 'var(--text-tertiary)')}>x - ) : ( - { e.stopPropagation(); closeTab(t.id); }} style={{ color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 12 }} onMouseEnter={e => (e.currentTarget.style.color = '#f88')} onMouseLeave={e => (e.currentTarget.style.color = 'var(--text-tertiary)')}>x - )} - { e.stopPropagation(); togglePin(t.id); }} style={{ color: t.pinned ? 'var(--accent)' : 'var(--text-tertiary)', cursor: 'pointer', fontSize: 10 }} title={t.pinned ? '取消固定' : '固定'}>● + { e.stopPropagation(); closeTab(t.id); }} style={{ color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 12 }} onMouseEnter={e => (e.currentTarget.style.color = '#f88')} onMouseLeave={e => (e.currentTarget.style.color = 'var(--text-tertiary)')}>x
))}
); -} +} \ No newline at end of file diff --git a/apps/desktop/src/renderer/features/toolbar/Toolbar.tsx b/apps/desktop/src/renderer/features/toolbar/Toolbar.tsx index e0348b9..8234df2 100644 --- a/apps/desktop/src/renderer/features/toolbar/Toolbar.tsx +++ b/apps/desktop/src/renderer/features/toolbar/Toolbar.tsx @@ -49,17 +49,17 @@ export function Toolbar() { alert2(r.ok.length ? `导入成功 ${r.ok.length} 项` : '未选择文件'); }; return ( -
+
{/* FR-V4:最近库下拉快速切换 */} - {vault?.recents && vault.recents.length > 1 && ( + {vault?.recents && vault.recents.length >= 1 && (
{recentsOpen && ( -
setRecentsOpen(false)}> +
setRecentsOpen(false)}> {vault.recents.map(r => (
{ setRecentsOpen(false); await useStore.getState().saveNow(); await invoke(IPC.vaultSwitch, { root: r }); useTabs.getState().closeAll(); useStore.getState().resetEditor(); await loadVault(); await loadTree(); }} style={{ padding: '6px 10px', cursor: 'pointer', borderRadius: 4, fontSize: 12, color: r === vault.currentRoot ? 'var(--accent)' : 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} diff --git a/apps/desktop/src/renderer/store.ts b/apps/desktop/src/renderer/store.ts index cae3cda..7ca6a09 100644 --- a/apps/desktop/src/renderer/store.ts +++ b/apps/desktop/src/renderer/store.ts @@ -3,6 +3,7 @@ import { IPC } from '@hunter/shared'; import type { TreeNode, VaultMeta, DocMeta, ReadDocResult } from '@hunter/shared'; import { invoke } from './ipc-client'; import { useTabs } from './features/tabs/tab-store'; +import { useSettings } from './features/settings/settings-store'; export type SaveState = 'saved' | 'saving' | 'error'; @@ -108,9 +109,9 @@ export const useStore = create((set, get) => ({ }, setBody: (body: string) => { set({ body, saveState: 'saving' }); - // FR-S1:防抖自动保存——间隔读 settings.autoSaveInterval(秒→毫秒) + // FR-S1:防抖自动保存——间隔读 settings.autoSaveInterval(秒→毫秒);直接取已解析的设置,避免每次按键重复解析 localStorage if (saveTimer) clearTimeout(saveTimer); - const interval = (() => { try { const s = JSON.parse(localStorage.getItem('dsh-settings') ?? '{}'); return (s.autoSaveInterval ?? 3) * 1000; } catch { return 3000; } })(); + const interval = (useSettings.getState().autoSaveInterval ?? 1) * 1000; saveTimer = setTimeout(() => { get().saveNow(); }, interval); }, diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index 893aed9..dcb6987 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -1,21 +1,111 @@ * { box-sizing: border-box; } -html, body, #root { margin: 0; height: 100%; } +html, body, #root { margin: 0; height: 100%; overflow: hidden; } +/* ========================================================= + UI 重新设计 · 液态玻璃(分层材质)设计系统 + L0 space / L1 glass / L2 paper / L3 float + 黑白灰墨色阶 · 克制动效 · 统一线条图标 + ========================================================= */ /* 全局统一字体栈——消灭按钮/输入框回退到 Arial 的观感不一致(A档-1) */ body { font-family: system-ui, -apple-system, 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC', sans-serif; color: var(--text-primary); -webkit-font-smoothing: antialiased; + background: var(--space, var(--bg-primary)); } button, input, select, textarea { font-family: inherit; } svg { display: block; } -/* 自定义滚动条——细+半透明+圆角 */ -::-webkit-scrollbar { width: 6px; height: 6px; } +/* 统一线条图标——细描边,形成一致的线性语言(stroke-width 1.6) */ +svg.lucide, .lucide { stroke-width: 1.6; } + +/* 自定义滚动条——细+圆角,墨色阶常显(不再悬停才出现,避免“看不见滚动条”),hover 加深 */ +::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--control-border); border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); } -* { scrollbar-width: thin; scrollbar-color: var(--control-border) transparent; } +::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--line-strong); } +::-webkit-scrollbar-corner { background: transparent; } +* { scrollbar-width: thin; scrollbar-color: var(--line) transparent; } +*:hover { scrollbar-color: var(--line-strong) transparent; } + +/* ---- 高级滑动条(range):细轨道 + 白玻璃圆滑块,墨色系 ---- */ +input[type="range"] { + -webkit-appearance: none; appearance: none; + width: 120px; height: 18px; background: transparent; cursor: pointer; +} +input[type="range"]::-webkit-slider-runnable-track { + height: 4px; border-radius: 999px; + background: color-mix(in srgb, var(--ink) 12%, transparent); +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; appearance: none; + width: 14px; height: 14px; border-radius: 50%; + background: var(--paper); border: 1px solid var(--line-strong); + margin-top: -5px; + box-shadow: 0 1px 3px rgba(20,22,28,0.16), 0 1px 1px rgba(20,22,28,0.08); + transition: transform .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease); +} +input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.15); border-color: var(--ink-3); } +input[type="range"]:active::-webkit-slider-thumb { background: var(--ink); border-color: var(--ink); } + +/* ---- 分层材质基类 ---- */ +/* L1 玻璃:半透明 + 模糊 + 顶部高光线 */ +.glass { + background: var(--glass-bar); + -webkit-backdrop-filter: blur(var(--blur-px, 18px)) saturate(1.2); + backdrop-filter: blur(var(--blur-px, 18px)) saturate(1.2); + position: relative; +} +.glass-side { + background: var(--glass-side); + -webkit-backdrop-filter: blur(var(--blur-px, 18px)) saturate(1.2); + backdrop-filter: blur(var(--blur-px, 18px)) saturate(1.2); + position: relative; +} +/* 顶部高光线(玻璃上缘一道亮边) */ +.glass::before, .glass-side::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, color-mix(in srgb, #fff 70%, transparent), transparent); + pointer-events: none; +} +/* L2 纸:编辑正文,最清晰 */ +.paper { + background: var(--paper); +} +/* L3 悬浮:弹窗/节点卡片,近实白 + 强投影 */ +.float { + background: var(--float); + box-shadow: var(--shadow-lift); +} + +/* L0 空间层:环境氛围——一束顶光 + 极淡噪点纹理(Apple 原生纵深感) */ +.ambient-bg { + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background-image: var(--ambient, linear-gradient(168deg, #f2f4f7 0%, #e9ecf1 100%)); +} +.ambient-bg::after { + content: ''; + position: absolute; + inset: 0; + opacity: .5; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E"); +} + +/* 克制动效:面板进入 fade+上移(380ms ease) */ +@keyframes panel-enter { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} +.panel-enter { animation: panel-enter 380ms var(--ease) both; } + +/* 柔和 hover 过渡 */ +.hover-soft { transition: background 160ms var(--ease), color 160ms var(--ease), opacity 160ms var(--ease), box-shadow 160ms var(--ease), transform 160ms var(--ease); } /* Tiptap 编辑器正文(B档-7 排版打磨:CJK 阅读优化、标题层级、选区、引用/链接/表格) */ .tiptap-wrap .ProseMirror { font-size: var(--editor-font-size, 14px) !important; @@ -44,8 +134,9 @@ svg { display: block; } .tiptap-wrap .ProseMirror h3 { font-size: 1.25em; margin: 0.8em 0 0.4em; font-weight: 600; color: var(--text-primary); } .tiptap-wrap .ProseMirror h4, .tiptap-wrap .ProseMirror h5, .tiptap-wrap .ProseMirror h6 { margin: 0.7em 0 0.35em; font-weight: 600; } .tiptap-wrap .ProseMirror code { - background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent-text); - padding: 1px 5px; border-radius: 4px; font-family: 'Consolas', 'Source Code Pro', monospace; + background: color-mix(in srgb, var(--ink) 7%, transparent); color: var(--ink); + padding: 2px 6px; border-radius: 5px; border: 1px solid var(--line); + font-family: 'Consolas', 'Source Code Pro', monospace; font-weight: 500; } .tiptap-wrap .ProseMirror pre { background: var(--bg-secondary); padding: 12px 16px; border-radius: var(--radius-md); overflow: auto; @@ -144,4 +235,12 @@ button:hover { opacity: 0.85; } from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } } -.panel-slide-in { animation: panel-slide-in 0.2s ease-out; } \ No newline at end of file +.panel-slide-in { animation: panel-slide-in 0.2s ease-out; } + +/* ---- 编辑区:L2 纸面,最清晰 ---- */ +.tiptap-wrap { background: var(--paper); } +.tiptap-wrap .ProseMirror pre { + background: color-mix(in srgb, var(--ink) 5%, var(--paper)); + border: 1px solid var(--line); + box-shadow: var(--shadow-soft); +} diff --git a/apps/desktop/tests/e2e/button-all.spec.ts b/apps/desktop/tests/e2e/button-all.spec.ts index ea1fb45..fa7be48 100644 --- a/apps/desktop/tests/e2e/button-all.spec.ts +++ b/apps/desktop/tests/e2e/button-all.spec.ts @@ -299,22 +299,15 @@ test('Graph: 控件+模式切换可用', async () => { await a.app.close(); }); -// === TabBar pin/close === -test('TabBar: 固定+关闭可用', async () => { +// === TabBar close === +test('TabBar: 关闭标签可用(pin 点已移除)', async () => { const a = await launch(nodePath.join(os.tmpdir(), 'hn-tab-' + Date.now())); const { v } = await setupVault(a, [{ name: 'Tab1', body: 't1' }, { name: 'Tab2', body: 't2' }]); await a.page.getByTestId('filetree').getByText('Tab1').click(); await a.page.waitForTimeout(300); await a.page.getByTestId('filetree').getByText('Tab2').click(); await a.page.waitForTimeout(300); - // 固定 Tab1(pin 按钮是 span[title="固定"]●) - const tab1 = a.page.getByTestId('tabbar').locator('div').filter({ hasText: 'Tab1' }).first(); - await tab1.locator('span[title="固定"]').click(); - await a.page.waitForTimeout(300); - // 取消固定 - await tab1.locator('span[title="取消固定"]').click(); - await a.page.waitForTimeout(300); - // 关闭 Tab2(close 按钮是 span 文本 x) + // 关闭 Tab2(close 按钮是 span 文本 x;pin 点按钮已按 UI 需求移除) const tab2 = a.page.getByTestId('tabbar').locator('div').filter({ hasText: 'Tab2' }).first(); await tab2.locator('span').filter({ hasText: 'x' }).click(); await a.page.waitForTimeout(300); @@ -354,4 +347,4 @@ test('OutlinePanel: 切换文档后大纲自动更新', async () => { await expect(a.page.locator('.outline-item', { hasText: 'Title B' })).toBeVisible({ timeout: 5000 }); await expect(a.page.locator('.outline-item', { hasText: 'Deep B' })).toBeVisible({ timeout: 5000 }); await a.app.close(); -}); +}); \ No newline at end of file diff --git a/apps/desktop/tests/e2e/graph-render.spec.ts b/apps/desktop/tests/e2e/graph-render.spec.ts index 790a71f..76aac6c 100644 --- a/apps/desktop/tests/e2e/graph-render.spec.ts +++ b/apps/desktop/tests/e2e/graph-render.spec.ts @@ -60,18 +60,19 @@ test('思维导图+知识图谱渲染验证(含可见性)', async () => { const rects = svg.querySelectorAll('rect').length; const texts = svg.querySelectorAll('text').length; const lines = svg.querySelectorAll('line').length; + const linkPaths = svg.querySelectorAll('path').length; // Check a node card is visible within the svg const visibleCard = Array.from(svg.querySelectorAll('rect')).some((r) => { const b = r.getBoundingClientRect(); return b.width > 0 && b.height > 0; }); - return { found: true, w: rect.width, h: rect.height, rects, texts, lines, visibleCard }; + return { found: true, w: rect.width, h: rect.height, rects, texts, lines, linkPaths, visibleCard }; }); console.log('Knowledge graph:', JSON.stringify(graph)); expect(graph.found).toBe(true); expect(graph.h).toBeGreaterThan(100); - expect(graph.rects).toBeGreaterThanOrEqual(5); - expect(graph.lines).toBeGreaterThanOrEqual(1); + expect(graph.rects).toBeGreaterThanOrEqual(3); // 2 卡 rect + 1 背景 rect(demo 卡片无左侧色条) + expect(graph.linkPaths).toBeGreaterThanOrEqual(1); // 曲线连线为 path expect(graph.visibleCard).toBe(true); await app.close(); diff --git a/apps/desktop/tests/e2e/graph.spec.ts b/apps/desktop/tests/e2e/graph.spec.ts index 9a3a00b..a55b82c 100644 --- a/apps/desktop/tests/e2e/graph.spec.ts +++ b/apps/desktop/tests/e2e/graph.spec.ts @@ -38,7 +38,8 @@ test('知识图谱:卡片+连线(消费 IPC.graph)', async () => { await a.page.getByTitle('切换 编辑器/图谱').click(); await a.page.getByRole('button', { name: '知识图谱' }).click(); // 限定在知识图谱画布内统计卡片 rect(标题栏等 UI 图标也是 svg,不能全页统计) - await expect(a.page.locator('[data-testid="graph-canvas"] rect')).toHaveCount(5, { timeout: 10000 }); + // 2 张卡片(2 卡 rect)+ 1 背景 rect = 3(demo 卡片无左侧色条) + await expect(a.page.locator('[data-testid="graph-canvas"] rect')).toHaveCount(3, { timeout: 10000 }); await a.app.close(); }); diff --git a/apps/desktop/tests/e2e/transparency-check.spec.ts b/apps/desktop/tests/e2e/transparency-check.spec.ts index 4d4b234..e8da64a 100644 --- a/apps/desktop/tests/e2e/transparency-check.spec.ts +++ b/apps/desktop/tests/e2e/transparency-check.spec.ts @@ -6,8 +6,8 @@ import os from 'node:os'; const MAIN_JS = nodePath.join(process.cwd(), 'dist-electron/main/index.js'); -// 透明主题真透明:bgPrimary=transparent 时根背景必须是 alpha=0(配合主进程 transparent:true + backgroundColor:#00000000 + backgroundMaterial:'none' 即可透出下层软件) -test('透明主题:根背景 alpha=0(真透明,可见下层软件)', async () => { +// UI 重新设计:窗口不透明(transparent:false + backgroundColor 空间底色),根背景应为不透明的 L0 空间色 +test('窗口不透明:根背景为 L0 空间底色(alpha=1,非透明)', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-tr-')); const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); @@ -15,15 +15,7 @@ test('透明主题:根背景 alpha=0(真透明,可见下层软件)', asy const page = await app.firstWindow(); await page.waitForLoadState('domcontentloaded'); await page.evaluate(({ root }) => (globalThis as any).dsh.invoke('vault:switch', { root }), { root: v }); - // 应用透明主题的 CSS 变量(bgPrimary='transparent'),并移除背景图遮罩 - await page.evaluate(() => { - const el = document.documentElement; - el.style.setProperty('--bg-primary', 'transparent'); - el.style.setProperty('--bg-secondary', 'rgba(20,20,28,0.5)'); - el.style.setProperty('--text-primary', '#ddd'); - document.querySelectorAll('div[style*="background-image"]').forEach((n: any) => n.remove()); - }); - await page.waitForTimeout(400); + await page.waitForTimeout(500); const rootBg = await page.evaluate(() => { const root = document.querySelector('#root > div') as HTMLElement; if (!root) return null; @@ -31,7 +23,15 @@ test('透明主题:根背景 alpha=0(真透明,可见下层软件)', asy }); console.log('ROOT_BG', rootBg); expect(rootBg).not.toBeNull(); - // 透明背景应为 alpha=0(rgba(0,0,0,0) 或 transparent) - expect(rootBg).toMatch(/rgba\(0, 0, 0, 0\)|transparent/i); + // 窗口不透明:根背景为不透明空间底色(alpha=1,非 rgba(...,0) / transparent) + const m = rootBg!.match(/rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+)\)|^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/); + expect(m).not.toBeNull(); + // 不透明:要么是 rgb(...)(隐式 alpha=1),要么 rgba alpha=1 + if (rootBg!.startsWith('rgba')) { + const alpha = parseFloat(m![4]); + expect(alpha).toBe(1); + } else { + expect(rootBg!.startsWith('rgb(')).toBe(true); + } await app.close(); }); \ No newline at end of file