feat(M5): FR-A3/A4 主题CSS变量全接线——25 文件 168 处硬编码色替换为 var(--accent/--glow/--accent-text/--text-primary/--text-secondary/--text-tertiary/--control-border/--bg-secondary); 语义色(错误红/确认/警告/成功)保留; hljs 语法色保留; typecheck+E2E 21/21 绿

This commit is contained in:
hunter-dev
2026-08-26 14:07:48 +08:00
parent bd60a8d920
commit 9a89a73518
25 changed files with 118 additions and 118 deletions
@@ -12,19 +12,19 @@ export function AppearancePanel({ onClose }: { onClose: () => void }) {
const colors: [keyof typeof draft, string][] = [['accent','强调色'],['glow','辉光色'],['accentText','强调色文字'],['textPrimary','主要文字'],['textSecondary','辅助文字'],['textTertiary','弱化文字'],['controlBorder','控件边框'],['bgPrimary','工作区背景'],['bgSecondary','浮层背景']];
return (
<div onClick={e => { 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 }}>
<div style={{ width: 620, maxHeight: '85vh', overflow: 'auto', background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
<div style={{ width: 620, maxHeight: '85vh', overflow: 'auto', background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<strong style={{ fontSize: 15 }}>外观定制</strong>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={() => { revert(); onClose(); }} style={btn}>取消</button>
<button onClick={async () => { await save(); onClose(); }} style={{ ...btn, background: '#2a4a6a', color: '#9cf' }}>保存外观</button>
<button onClick={async () => { await save(); onClose(); }} style={{ ...btn, background: 'var(--accent)', color: 'var(--accent-text)' }}>保存外观</button>
</div>
</div>
<div style={{ marginBottom: 12, fontSize: 12, color: isDirty() ? '#e8a' : '#6a8' }}>{isDirty() ? '更改尚未保存' : '所有设置已保存'}</div>
<div style={{ opacity: 0.5, fontSize: 12, marginBottom: 6 }}>主题</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8, marginBottom: 16 }}>
{THEMES.map((t: ThemeDef) => (
<div key={t.id} onClick={() => selectTheme(t.id)} style={{ cursor: 'pointer', padding: 8, borderRadius: 6, border: themeId === t.id ? '2px solid #4a9eff' : '1px solid #3a3a4a', background: t.params.bgPrimary, color: t.params.textPrimary, fontSize: 11, minHeight: 48, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div key={t.id} onClick={() => selectTheme(t.id)} style={{ cursor: 'pointer', padding: 8, borderRadius: 6, border: themeId === t.id ? '2px solid var(--accent)' : '1px solid var(--control-border)', background: t.params.bgPrimary, color: t.params.textPrimary, fontSize: 11, minHeight: 48, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div style={{ fontWeight: 600 }}>{t.name}</div><div style={{ opacity: 0.6 }}>{t.desc}</div>
</div>
))}
@@ -43,13 +43,13 @@ export function AppearancePanel({ onClose }: { onClose: () => void }) {
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
<input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={e => { const f = e.target.files?.[0]; if (f) compressImage(f).then(d => setBgImage(d)); e.target.value = ''; }} />
<button onClick={() => fileRef.current?.click()} style={{ ...btn }}>选择图片</button>
{bgImage && <><img src={bgImage} alt="bg" style={{ width: 64, height: 40, objectFit: 'cover', borderRadius: 4, border: '1px solid #3a3a4a' }} /><button onClick={() => setBgImage(null)} style={{ ...btn }}>移除</button></>}
{!bgImage && <span style={{ fontSize: 12, color: '#666' }}>未设置</span>}
{bgImage && <><img src={bgImage} alt="bg" style={{ width: 64, height: 40, objectFit: 'cover', borderRadius: 4, border: '1px solid var(--control-border)' }} /><button onClick={() => setBgImage(null)} style={{ ...btn }}>移除</button></>}
{!bgImage && <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>未设置</span>}
</div>
<div style={{ opacity: 0.5, fontSize: 12, marginBottom: 6 }}>鼠标特效</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6, marginBottom: 16 }}>
{EFFECTS.map(([t, label]) => (
<button key={t} onClick={() => setEffect(t)} style={{ padding: '8px', fontSize: 12, borderRadius: 4, cursor: 'pointer', border: effect === t ? '2px solid #4a9eff' : '1px solid #3a3a4a', background: effect === t ? '#2a2a3a' : 'transparent', color: effect === t ? '#9cf' : '#aaa' }}>{label}</button>
<button key={t} onClick={() => setEffect(t)} style={{ padding: '8px', fontSize: 12, borderRadius: 4, cursor: 'pointer', border: effect === t ? '2px solid var(--accent)' : '1px solid var(--control-border)', background: effect === t ? 'var(--control-border)' : 'transparent', color: effect === t ? 'var(--accent-text)' : 'var(--text-secondary)' }}>{label}</button>
))}
</div>
<button onClick={() => revert()} style={btn}>恢复(取消改动)</button>
@@ -73,4 +73,4 @@ function normalizeColor(c: string): string {
if (c === 'transparent' || c.startsWith('rgba')) return '#1a1a2a';
return c;
}
const btn: React.CSSProperties = { padding: '6px 14px', border: '1px solid #3a3a4a', background: 'transparent', color: '#aaa', borderRadius: 4, cursor: 'pointer', fontSize: 13 };
const btn: React.CSSProperties = { padding: '6px 14px', border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-secondary)', borderRadius: 4, cursor: 'pointer', fontSize: 13 };
@@ -16,21 +16,21 @@ export function InputDialog() {
return (
<div onClick={e => { 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 }}>
<div style={{ background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 8, padding: 20, minWidth: 360, boxShadow: '0 8px 32px rgba(0,0,0,0.4)' }}>
<div style={{ marginBottom: 12, fontSize: 14, color: '#ddd' }}>{title}</div>
<div 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)' }}>
<div style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-primary)' }}>{title}</div>
{mode === 'confirm' ? (
<div style={{ color: '#e88', fontSize: 13, marginBottom: 16 }}>{message}</div>
) : (
<input ref={inputRef} value={value} onChange={e => setValue(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') submit(); if (e.key === 'Escape') close(null); }}
placeholder={placeholder} style={{ width: '100%', background: '#15151f', border: '1px solid #3a3a4a', color: '#ddd', borderRadius: 4, padding: '8px 12px', fontSize: 14, outline: 'none' }} />
placeholder={placeholder} style={{ width: '100%', background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', color: 'var(--text-primary)', borderRadius: 4, padding: '8px 12px', fontSize: 14, outline: 'none' }} />
)}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
<button onClick={() => close(null)} style={btn}>取消</button>
<button onClick={submit} style={{ ...btn, background: mode === 'confirm' ? '#5a2a2a' : '#2a4a6a', color: mode === 'confirm' ? '#f88' : '#9cf' }}>{mode === 'confirm' ? '确认删除' : '确定'}</button>
<button onClick={submit} style={{ ...btn, background: mode === 'confirm' ? '#5a2a2a' : 'var(--accent)', color: mode === 'confirm' ? '#f88' : 'var(--accent-text)' }}>{mode === 'confirm' ? '确认删除' : '确定'}</button>
</div>
</div>
</div>
);
}
const btn: React.CSSProperties = { padding: '6px 16px', border: '1px solid #3a3a4a', background: 'transparent', color: '#aaa', borderRadius: 4, cursor: 'pointer', fontSize: 13 };
const btn: React.CSSProperties = { padding: '6px 16px', border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-secondary)', borderRadius: 4, cursor: 'pointer', fontSize: 13 };
@@ -1,6 +1,6 @@
import { useNotify } from './notify-store';
export function NotifyLayer() { const { items, remove } = useNotify();
return <div style={{ position: 'fixed', top: 48, right: 20, zIndex: 3000, display: 'flex', flexDirection: 'column', gap: 8 }}>{items.map(n => (
<div key={n.id} onClick={() => 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' : '#2a2a3a', color: n.type === 'success' ? '#9fc' : n.type === 'error' ? '#f88' : '#9cf' }}>{n.msg}</div>
<div key={n.id} onClick={() => 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}</div>
))}</div>;
}
}
@@ -21,7 +21,7 @@ export function DocInfoPanel() {
<div style={{ padding: 16, overflow: 'auto', fontSize: 13 }}>
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
<strong>{title}</strong>
<button onClick={() => toggleStar()} title="星标" style={{ cursor: 'pointer', background: 'none', border: 'none', fontSize: 16, color: meta.star ? '#ffd54a' : '#888' }}>
<button onClick={() => toggleStar()} title="星标" style={{ cursor: 'pointer', background: 'none', border: 'none', fontSize: 16, color: meta.star ? 'var(--accent)' : 'var(--text-tertiary)' }}>
{meta.star ? '★' : '☆'}
</button>
</div>
@@ -33,7 +33,7 @@ export function DocInfoPanel() {
<Section title="标签">
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 6 }}>
{(meta.tags ?? []).map(t => (
<span key={t} style={{ background: '#2a2a3a', borderRadius: 4, padding: '2px 8px', display: 'flex', alignItems: 'center', gap: 4 }}>
<span key={t} style={{ background: 'var(--control-border)', borderRadius: 4, padding: '2px 8px', display: 'flex', alignItems: 'center', gap: 4 }}>
{t}<span style={{ cursor: 'pointer', opacity: 0.6 }} onClick={() => removeTag(t)}>×</span>
</span>
))}
@@ -75,5 +75,5 @@ export function DocInfoPanel() {
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (<div style={{ marginBottom: 16 }}><div style={{ opacity: 0.5, marginBottom: 6 }}>{title}</div>{children}</div>);
}
const inp: React.CSSProperties = { background: '#222', border: '1px solid #444', color: '#ddd', borderRadius: 4, padding: '3px 8px', fontSize: 13, flex: 1 };
const btn: React.CSSProperties = { minWidth: 28, height: 26, border: '1px solid #444', background: 'transparent', color: '#ccc', cursor: 'pointer', borderRadius: 4 };
const inp: React.CSSProperties = { background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', color: 'var(--text-primary)', borderRadius: 4, padding: '3px 8px', fontSize: 13, flex: 1 };
const btn: React.CSSProperties = { minWidth: 28, height: 26, border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-primary)', cursor: 'pointer', borderRadius: 4 };
@@ -29,12 +29,12 @@ export function CodeBlockView({ node, updateAttributes, selected }: NodeViewProp
const language = (node.attrs.language as string) || '';
const codeClass = language ? `language-${language} hljs` : 'hljs';
return (
<NodeViewWrapper as="div" style={{ background: '#161616', borderRadius: 6, margin: '0.6em 0', overflow: 'hidden', outline: selected ? '1px solid #4a6a8a' : 'none' }}>
<div contentEditable={false} style={{ display: 'flex', justifyContent: 'flex-end', padding: '4px 8px', borderBottom: '1px solid #2a2a2a' }}>
<NodeViewWrapper as="div" style={{ background: 'var(--bg-secondary)', borderRadius: 6, margin: '0.6em 0', overflow: 'hidden', outline: selected ? '1px solid #4a6a8a' : 'none' }}>
<div contentEditable={false} style={{ display: 'flex', justifyContent: 'flex-end', padding: '4px 8px', borderBottom: '1px solid var(--control-border)' }}>
<select
value={language || 'plaintext'}
onChange={e => updateAttributes({ language: e.target.value })}
style={{ background: '#222', color: '#ccc', border: '1px solid #444', borderRadius: 4, fontSize: 12, padding: '1px 4px', cursor: 'pointer' }}
style={{ background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--control-border)', borderRadius: 4, fontSize: 12, padding: '1px 4px', cursor: 'pointer' }}
>
{LANGS.map(l => <option key={l.value} value={l.value}>{l.label}</option>)}
</select>
@@ -44,4 +44,4 @@ export function CodeBlockView({ node, updateAttributes, selected }: NodeViewProp
</pre>
</NodeViewWrapper>
);
}
}
@@ -20,7 +20,7 @@ export function Editor() {
}
return (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div style={{ padding: '6px 16px', borderBottom: '1px solid #333', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ padding: '6px 16px', borderBottom: '1px solid var(--control-border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{title} <span style={{ opacity: 0.4, fontSize: 12 }}>{currentPath}</span></span>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 12, opacity: saveState === 'error' ? 1 : 0.5, color: saveState === 'error' ? '#f88' : 'inherit' }}>{saveLabel(saveState)}</span>
@@ -5,7 +5,7 @@ import { useDialog } from '../dialog/dialog-store';
import { invoke } from '../../ipc-client';
import { IPC } from '@hunter/shared';
const btn: React.CSSProperties = { minWidth: 30, height: 28, border: '1px solid #444', background: 'transparent', color: '#ccc', cursor: 'pointer', borderRadius: 4, fontSize: 13 };
const btn: React.CSSProperties = { minWidth: 30, height: 28, border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-primary)', cursor: 'pointer', borderRadius: 4, fontSize: 13 };
function T({ onClick, title, children, disabled }: { onClick: () => void; title: string; children: React.ReactNode; disabled?: boolean }) {
return <button style={{ ...btn, opacity: disabled ? 0.4 : 1 }} onClick={onClick} title={title} disabled={disabled}>{children}</button>;
@@ -35,29 +35,29 @@ export function EditorToolbar() {
editor.chain().focus().setImage({ src: url }).run();
};
return (
<div style={{ padding: '6px 12px', borderBottom: '1px solid #333', display: 'flex', gap: 4, flexWrap: 'wrap', alignItems: 'center' }}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--control-border)', display: 'flex', gap: 4, flexWrap: 'wrap', alignItems: 'center' }}>
<T title="撤销 Ctrl+Z" onClick={cmd(e => e.chain().focus().undo().run())} disabled={d}>↶</T>
<T title="重做 Ctrl+Y" onClick={cmd(e => e.chain().focus().redo().run())} disabled={d}>↷</T>
<span style={{ width: 1, background: '#333', height: 20, margin: '0 4px' }} />
<span style={{ width: 1, background: 'var(--control-border)', height: 20, margin: '0 4px' }} />
<T title="加粗 Ctrl+B" onClick={cmd(e => e.chain().focus().toggleBold().run())} disabled={d}>B</T>
<T title="斜体 Ctrl+I" onClick={cmd(e => e.chain().focus().toggleItalic().run())} disabled={d}>I</T>
<T title="删除线 Alt+Shift+5" onClick={cmd(e => e.chain().focus().toggleStrike().run())} disabled={d}>S</T>
<T title="行内代码 Ctrl+E" onClick={cmd(e => e.chain().focus().toggleCode().run())} disabled={d}><span style={{ fontFamily: 'monospace' }}>code</span></T>
<T title="清除格式" onClick={cmd(e => e.chain().focus().unsetAllMarks().run())} disabled={d}>T×</T>
<T title="引用 Ctrl+Shift+Q" onClick={cmd(e => e.chain().focus().toggleBlockquote().run())} disabled={d}>❝</T>
<span style={{ width: 1, background: '#333', height: 20, margin: '0 4px' }} />
<span style={{ width: 1, background: 'var(--control-border)', height: 20, margin: '0 4px' }} />
<T title="一级标题 Ctrl+1" onClick={cmd(e => e.chain().focus().toggleHeading({ level: 1 }).run())} disabled={d}>H1</T>
<T title="二级标题 Ctrl+2" onClick={cmd(e => e.chain().focus().toggleHeading({ level: 2 }).run())} disabled={d}>H2</T>
<T title="三级标题 Ctrl+3" onClick={cmd(e => e.chain().focus().toggleHeading({ level: 3 }).run())} disabled={d}>H3</T>
<span style={{ width: 1, background: '#333', height: 20, margin: '0 4px' }} />
<span style={{ width: 1, background: 'var(--control-border)', height: 20, margin: '0 4px' }} />
<T title="无序列表 Ctrl+L" onClick={cmd(e => e.chain().focus().toggleBulletList().run())} disabled={d}>•</T>
<T title="有序列表 Ctrl+Shift+O" onClick={cmd(e => e.chain().focus().toggleOrderedList().run())} disabled={d}>1.</T>
<T title="代码块 Ctrl+Shift+M" onClick={cmd(e => e.chain().focus().toggleCodeBlock().run())} disabled={d}>{'</>'}</T>
<T title="表格 Ctrl+Shift+T" onClick={cmd(e => e.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run())} disabled={d}>▦</T>
<span style={{ width: 1, background: '#333', height: 20, margin: '0 4px' }} />
<span style={{ width: 1, background: 'var(--control-border)', height: 20, margin: '0 4px' }} />
<T title="链接" onClick={() => { void link(); }} disabled={d}>链接</T>
<T title="插入图片(本地选图入库)" onClick={() => { void pickImage(); }} disabled={d}>图片</T>
<T title="网络图片(手输网址)" onClick={() => { void imageUrl(); }} disabled={d}>网图</T>
</div>
);
}
}
@@ -80,7 +80,7 @@ export function FindReplace() {
};
return (
<div style={{ padding: '6px 12px', borderBottom: '1px solid #333', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--control-border)', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
<input value={find} onChange={e => setFind(e.target.value)} placeholder="查找" onKeyDown={e => e.key === 'Enter' && doFind()} style={inp} />
<input value={replace} onChange={e => setReplace(e.target.value)} placeholder="替换" style={inp} />
<label style={lab}><input type="checkbox" data-testid="fr-case" checked={opts.caseSensitive} onChange={toggle('caseSensitive')} /> Aa</label>
@@ -95,6 +95,6 @@ export function FindReplace() {
</div>
);
}
const inp: React.CSSProperties = { background: '#222', border: '1px solid #444', color: '#ddd', borderRadius: 4, padding: '3px 8px', width: 110, fontSize: 13 };
const btn: React.CSSProperties = { minWidth: 30, height: 26, border: '1px solid #444', background: 'transparent', color: '#ccc', cursor: 'pointer', borderRadius: 4, fontSize: 12 };
const lab: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 3, fontSize: 12, color: '#aaa', cursor: 'pointer' };
const inp: React.CSSProperties = { background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', color: 'var(--text-primary)', borderRadius: 4, padding: '3px 8px', width: 110, fontSize: 13 };
const btn: React.CSSProperties = { minWidth: 30, height: 26, border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-primary)', cursor: 'pointer', borderRadius: 4, fontSize: 12 };
const lab: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 3, fontSize: 12, color: 'var(--text-secondary)', cursor: 'pointer' };
@@ -17,12 +17,12 @@ export function LinkBubbleMenu({ editor }: { editor: Editor }) {
const remove = () => editor.chain().focus().extendMarkRange('link').unsetLink().run();
return (
<BubbleMenu editor={editor} shouldShow={({ editor }) => editor.isActive('link')} className="link-bubble"
style={{ display: 'flex', gap: 4, alignItems: 'center', background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 6, padding: 4, boxShadow: '0 4px 16px rgba(0,0,0,0.4)' }}>
style={{ display: 'flex', gap: 4, alignItems: 'center', background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 6, padding: 4, boxShadow: '0 4px 16px rgba(0,0,0,0.4)' }}>
<input value={href} onChange={e => setHref(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); apply(); } }}
placeholder="链接地址" style={{ width: 180, background: '#15151f', border: '1px solid #3a3a4a', color: '#ddd', borderRadius: 4, padding: '3px 8px', fontSize: 13, outline: 'none' }} />
placeholder="链接地址" style={{ width: 180, background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', color: 'var(--text-primary)', borderRadius: 4, padding: '3px 8px', fontSize: 13, outline: 'none' }} />
<button onClick={apply} style={btn}>更新</button>
<button onClick={remove} style={btn}>移除</button>
</BubbleMenu>
);
}
const btn: React.CSSProperties = { padding: '3px 10px', border: '1px solid #3a3a4a', background: 'transparent', color: '#ccc', borderRadius: 4, cursor: 'pointer', fontSize: 12 };
const btn: React.CSSProperties = { padding: '3px 10px', border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-primary)', borderRadius: 4, cursor: 'pointer', fontSize: 12 };
@@ -19,9 +19,9 @@ const MathBlockView = (props: NodeViewProps) => {
const tex = props.node.textContent;
const html = renderLatex(tex, true);
return (
<NodeViewWrapper as="div" className="math-block" style={{ margin: '8px 0', padding: '8px', border: '1px solid #3a3a4a', borderRadius: 6, background: '#1a1a26', position: 'relative' }}>
<NodeViewWrapper as="div" className="math-block" style={{ margin: '8px 0', padding: '8px', border: '1px solid var(--control-border)', borderRadius: 6, background: 'var(--bg-secondary)', position: 'relative' }}>
<div className="math-preview" dangerouslySetInnerHTML={{ __html: html }} style={{ minHeight: 24, fontSize: 15 }} />
<NodeViewContent as={'div' as any} className="math-source" contentEditable={true} style={{ fontFamily: 'monospace', fontSize: 13, color: '#8ab4f8', marginTop: tex ? 4 : 0, outline: 'none' }} />
<NodeViewContent as={'div' as any} className="math-source" contentEditable={true} style={{ fontFamily: 'monospace', fontSize: 13, color: 'var(--accent-text)', marginTop: tex ? 4 : 0, outline: 'none' }} />
</NodeViewWrapper>
);
};
@@ -82,4 +82,4 @@ export const InlineMath = Node.create({
insertInlineMath: (tex: string) => ({ chain }: { chain: () => any }) => chain().insertContent({ type: 'inlineMath', attrs: { tex } }).run(),
} as any;
},
});
});
@@ -15,7 +15,7 @@ export function MoreActions() {
<div style={{ position: 'relative' }}>
<button onClick={() => setOpen(o => !o)} style={btn} title="更多操作">⋯</button>
{open && (
<div style={{ position: 'absolute', right: 0, top: 28, background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 6, padding: 4, zIndex: 500, minWidth: 140 }}>
<div style={{ position: 'absolute', right: 0, top: 28, background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 6, padding: 4, zIndex: 500, minWidth: 140 }}>
<Item onClick={copyMd}>复制 Markdown</Item>
<Item onClick={print}>打印 / 另存为 PDF</Item>
<Item onClick={reveal}>在文件管理器中显示</Item>
@@ -26,7 +26,7 @@ export function MoreActions() {
);
}
function Item({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
return <div onClick={onClick} style={{ padding: '6px 12px', cursor: 'pointer', borderRadius: 4, fontSize: 13, color: '#ccc' }} onMouseEnter={e => (e.currentTarget.style.background = '#2a2a3a')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}>{children}</div>;
return <div onClick={onClick} style={{ padding: '6px 12px', cursor: 'pointer', borderRadius: 4, fontSize: 13, color: 'var(--text-primary)' }} onMouseEnter={e => (e.currentTarget.style.background = 'var(--control-border)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}>{children}</div>;
}
const btn: React.CSSProperties = { height: 26, border: '1px solid #3a3a4a', background: 'transparent', color: '#ccc', cursor: 'pointer', borderRadius: 4, fontSize: 14, padding: '0 8px' };
function notify(msg: string) { const el = document.createElement('div'); el.textContent = msg; el.style.cssText = 'position:fixed;top:50px;right:20px;background:#2a4a6a;color:#9cf;padding:10px 16px;border-radius:6px;z-index:3000;font-size:13px'; document.body.appendChild(el); setTimeout(() => el.remove(), 2500); }
const btn: React.CSSProperties = { height: 26, border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-primary)', cursor: 'pointer', borderRadius: 4, fontSize: 14, padding: '0 8px' };
function notify(msg: string) { const el = document.createElement('div'); el.textContent = msg; el.style.cssText = 'position:fixed;top:50px;right:20px;background:var(--accent);color:var(--accent-text);padding:10px 16px;border-radius:6px;z-index:3000;font-size:13px'; document.body.appendChild(el); setTimeout(() => el.remove(), 2500); }
@@ -50,9 +50,9 @@ export function SlashMenu() {
setOpen(false);
};
return (
<div style={{ position: 'fixed', left: pos.x, top: pos.y, background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 6, padding: 4, zIndex: 2000, minWidth: 150, maxHeight: 240, overflow: 'auto' }}>
<div style={{ position: 'fixed', left: pos.x, top: pos.y, background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 6, padding: 4, zIndex: 2000, minWidth: 150, maxHeight: 240, overflow: 'auto' }}>
{items.length === 0 && <div style={{ padding: '6px 10px', opacity: 0.5, fontSize: 12 }}>无匹配</div>}
{items.map(it => <div key={it.label} onClick={() => pick(it)} style={{ padding: '6px 10px', cursor: 'pointer', borderRadius: 4, fontSize: 13 }} onMouseEnter={e => (e.currentTarget.style.background = '#2a2a3a')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}>{it.label}</div>)}
{items.map(it => <div key={it.label} onClick={() => pick(it)} style={{ padding: '6px 10px', cursor: 'pointer', borderRadius: 4, fontSize: 13 }} onMouseEnter={e => (e.currentTarget.style.background = 'var(--control-border)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}>{it.label}</div>)}
</div>
);
}
}
@@ -73,7 +73,7 @@ export function FileTree() {
{isRenaming ? (
<input autoFocus value={renameVal} onChange={e => setRenameVal(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') { setRenaming(null); invoke(IPC.fileRename, { from: n.path, toName: renameVal }).then(r => { useTabs.getState().renamePath(n.path, r.path); syncCurrentPath(n.path, r.path); loadTree(); }); } if (e.key === 'Escape') setRenaming(null); }}
onBlur={() => setRenaming(null)} style={{ flex: 1, background: '#15151f', border: '1px solid #4a6a8a', color: '#ddd', borderRadius: 3, padding: '1px 4px', fontSize: 13 }} />
onBlur={() => setRenaming(null)} style={{ flex: 1, background: 'var(--bg-secondary)', border: '1px solid #4a6a8a', color: 'var(--text-primary)', borderRadius: 3, padding: '1px 4px', fontSize: 13 }} />
) : (
<span style={{ marginLeft: 4, flex: 1 }}>{n.name}</span>
)}
@@ -98,7 +98,7 @@ export function FileTree() {
{roots.map(n => renderNode(n, 0))}
{tree.length === 0 && !treeLoading && <div style={{ opacity: 0.5, fontSize: 12 }}>空库,点击上方「+ 新建文档」开始</div>}
{menu && menuNode && (
<div style={{ position: 'fixed', left: menu.x, top: menu.y, background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 6, padding: 4, zIndex: 2000, minWidth: 120 }} onClick={e => e.stopPropagation()}>
<div style={{ position: 'fixed', left: menu.x, top: menu.y, background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 6, padding: 4, zIndex: 2000, minWidth: 120 }} onClick={e => e.stopPropagation()}>
{menuNode.type === 'doc' && <MenuItem onClick={() => { openDoc(menuNode.path); setMenu(null); }}>打开</MenuItem>}
{menuNode.type === 'folder' && <>
<MenuItem onClick={() => { doCreate(menuNode.path, false); setMenu(null); }}>新建文档</MenuItem>
@@ -111,8 +111,8 @@ export function FileTree() {
);
}
const miniBtn: React.CSSProperties = { border: 'none', background: 'transparent', color: '#888', cursor: 'pointer', padding: '0 3px', fontSize: 12 };
const miniBtn: React.CSSProperties = { border: 'none', background: 'transparent', color: 'var(--text-tertiary)', cursor: 'pointer', padding: '0 3px', fontSize: 12 };
function MenuItem({ children, onClick, danger }: { children: React.ReactNode; onClick: () => void; danger?: boolean }) {
return <div onClick={onClick} style={{ padding: '5px 10px', cursor: 'pointer', borderRadius: 4, fontSize: 13, color: danger ? '#f88' : '#ccc' }} onMouseEnter={e => (e.currentTarget.style.background = '#2a2a3a')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}>{children}</div>;
return <div onClick={onClick} style={{ padding: '5px 10px', cursor: 'pointer', borderRadius: 4, fontSize: 13, color: danger ? '#f88' : 'var(--text-primary)' }} onMouseEnter={e => (e.currentTarget.style.background = 'var(--control-border)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}>{children}</div>;
}
@@ -46,7 +46,7 @@ const MARKMAP_OPTIONS = {
const t = node.payload?.type as NodeType | undefined;
if (t === 'doc') return '#9aa0aa';
if (t === 'folder') return '#5db0ff';
return '#4a9eff';
return 'var(--accent)';
},
};
@@ -92,15 +92,15 @@ function MindmapTree() {
}, [tree]);
return (
<div className="markmap-dark" style={{ flex: 1, background: '#111', position: 'relative', overflow: 'hidden' }}>
<div className="markmap-dark" style={{ flex: 1, background: 'var(--bg-secondary)', position: 'relative', overflow: 'hidden' }}>
<svg ref={svgRef} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }} />
<style>{`
.markmap-foreign { color: #ddd; font-size: 13px; line-height: 1.2; }
.markmap-foreign { color: var(--text-primary); font-size: 13px; line-height: 1.2; }
.mm-node { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.mm-ico { font-size: 13px; opacity: 0.85; }
.mm-name { font-weight: 500; }
.mm-root .mm-name { font-weight: 700; color: #cfe4ff; }
.mm-folder .mm-name { font-weight: 600; color: #cfe4ff; }
.mm-root .mm-name { font-weight: 700; color: var(--accent-text); }
.mm-folder .mm-name { font-weight: 600; color: var(--accent-text); }
.mm-doc .mm-name { color: #9aa0aa; font-weight: 400; }
.mm-doc { cursor: pointer; }
.mm-doc:hover .mm-name { color: #fff; text-decoration: underline; }
@@ -111,13 +111,13 @@ function MindmapTree() {
}
function tabBtn(active: boolean): React.CSSProperties {
return { padding: '3px 10px', fontSize: 12, background: active ? '#2a2a3a' : 'transparent', color: active ? '#fff' : '#888', border: '1px solid #2a2a3a', borderRadius: 4, cursor: 'pointer' };
return { padding: '3px 10px', fontSize: 12, background: active ? 'var(--control-border)' : 'transparent', color: active ? '#fff' : 'var(--text-tertiary)', border: '1px solid var(--control-border)', borderRadius: 4, cursor: 'pointer' };
}
export function GraphView() {
const [mode, setMode] = useState<'mindmap' | 'graph'>('mindmap');
return (
<div style={{ flex: 1, position: 'relative', overflow: 'hidden', background: '#111' }}>
<div style={{ flex: 1, position: 'relative', overflow: 'hidden', background: 'var(--bg-secondary)' }}>
<div style={{ position: 'absolute', top: 8, left: 8, zIndex: 10, display: 'flex', gap: 4 }}>
<button style={tabBtn(mode === 'mindmap')} onClick={() => setMode('mindmap')}>思维导图</button>
<button style={tabBtn(mode === 'graph')} onClick={() => setMode('graph')}>知识图谱</button>
@@ -125,4 +125,4 @@ export function GraphView() {
{mode === 'mindmap' ? <MindmapTree /> : <LinkGraph />}
</div>
);
}
}
@@ -57,7 +57,7 @@ export function LinkGraph() {
.filter((e) => idx.has(e.source) && idx.has(e.target))
.map((e) => ({ source: idx.get(e.source)!, target: idx.get(e.target)! }));
const linkSel = g.append('g').attr('stroke', '#3a3a4a').attr('stroke-opacity', 0.6)
const linkSel = g.append('g').attr('stroke', 'var(--control-border)').attr('stroke-opacity', 0.6)
.selectAll('line').data(links).join('line').attr('stroke-width', 1);
// 力导向模拟(先建,拖拽回调引用之)
@@ -75,11 +75,11 @@ export function LinkGraph() {
// 卡片:外框(描边=分组色)+ 左侧色条 + 标题 + 星标
nodeSel.append('rect').attr('x', -CARD_W / 2).attr('y', -CARD_H / 2).attr('width', CARD_W).attr('height', CARD_H).attr('rx', 5)
.attr('fill', '#1f1f2a').attr('stroke', (d) => d.color || '#3a3a4a').attr('stroke-width', 1.5);
.attr('fill', 'var(--bg-secondary)').attr('stroke', (d) => d.color || 'var(--control-border)').attr('stroke-width', 1.5);
nodeSel.append('rect').attr('x', -CARD_W / 2).attr('y', -CARD_H / 2).attr('width', 4).attr('height', CARD_H).attr('fill', (d) => d.color || '#5db0ff');
nodeSel.append('text').attr('x', -CARD_W / 2 + 12).attr('y', 2).attr('fill', '#ddd').attr('font-size', 12).attr('font-family', 'sans-serif')
nodeSel.append('text').attr('x', -CARD_W / 2 + 12).attr('y', 2).attr('fill', 'var(--text-primary)').attr('font-size', 12).attr('font-family', 'sans-serif')
.text((d) => truncate(d.title, 16));
nodeSel.filter((d) => d.star).append('text').attr('x', CARD_W / 2 - 14).attr('y', 4).attr('fill', '#f5c542').attr('font-size', 12).text('★');
nodeSel.filter((d) => d.star).append('text').attr('x', CARD_W / 2 - 14).attr('y', 4).attr('fill', 'var(--accent)').attr('font-size', 12).text('★');
nodeSel.on('click', (_ev: any, d: any) => { if (d.basePath) openDoc(d.basePath); });
@@ -92,10 +92,10 @@ export function LinkGraph() {
}, [data, openDoc]);
return (
<div style={{ flex: 1, background: '#111', position: 'relative', overflow: 'hidden' }}>
<div style={{ flex: 1, background: 'var(--bg-secondary)', position: 'relative', overflow: 'hidden' }}>
<svg ref={svgRef} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }} />
{err && <div style={{ position: 'absolute', top: 8, left: 8, color: '#f88', fontSize: 12 }}>图谱加载失败:{err}</div>}
{data && data.nodes.length === 0 && <div style={{ position: 'absolute', top: 8, left: 8, color: '#888', fontSize: 12 }}>暂无文档(先建几篇笔记)</div>}
{data && data.nodes.length === 0 && <div style={{ position: 'absolute', top: 8, left: 8, color: 'var(--text-tertiary)', fontSize: 12 }}>暂无文档(先建几篇笔记)</div>}
</div>
);
}
}
@@ -15,15 +15,15 @@ const GROUPS: [string, [string, string][]][] = [
export function CheatSheet({ onClose }: { onClose: () => void }) {
return (
<div onClick={e => { 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 }}>
<div style={{ width: 480, maxHeight: '80vh', overflow: 'auto', background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}><strong style={{ fontSize: 15 }}>快捷键速查</strong><button onClick={onClose} style={{ background: 'none', border: 'none', color: '#888', cursor: 'pointer', fontSize: 16 }}>✕</button></div>
<div style={{ width: 480, maxHeight: '80vh', overflow: 'auto', background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}><strong style={{ fontSize: 15 }}>快捷键速查</strong><button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 16 }}>✕</button></div>
{GROUPS.map(([title, items]) => (
<div key={title} style={{ marginBottom: 14 }}>
<div style={{ opacity: 0.5, fontSize: 12, marginBottom: 6 }}>{title}</div>
{items.map(([k, v]) => (<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '3px 0', fontSize: 13 }}><span style={{ color: '#ccc' }}>{v}</span><code style={{ background: '#15151f', padding: '1px 6px', borderRadius: 3, color: '#9cf' }}>{k}</code></div>))}
{items.map(([k, v]) => (<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '3px 0', fontSize: 13 }}><span style={{ color: 'var(--text-primary)' }}>{v}</span><code style={{ background: 'var(--bg-secondary)', padding: '1px 6px', borderRadius: 3, color: 'var(--accent-text)' }}>{k}</code></div>))}
</div>
))}
</div>
</div>
);
}
}
@@ -95,11 +95,11 @@ export function OutlinePanel() {
className={i === activeIdx ? 'outline-item outline-item-active' : 'outline-item'}
style={{ paddingLeft: 8 + (h.level - 1) * 12, cursor: 'pointer', padding: '4px 4px 4px ' + (8 + (h.level-1)*12) + 'px',
fontSize: 13, borderRadius: 4 }}
onMouseEnter={e => { if (i !== activeIdx) e.currentTarget.style.background = '#2a2a2a'; }}
onMouseEnter={e => { if (i !== activeIdx) e.currentTarget.style.background = 'var(--control-border)'; }}
onMouseLeave={e => { if (i !== activeIdx) e.currentTarget.style.background = 'transparent'; }}>
{h.text || '(空标题)'}
</div>
))}
</div>
);
}
}
@@ -21,10 +21,10 @@ export function SearchBox() {
}, [q]);
return (
<div style={{ padding: 8, borderBottom: '1px solid #333' }}>
<div style={{ padding: 8, borderBottom: '1px solid var(--control-border)' }}>
<div style={{ display: 'flex', gap: 4 }}>
<input value={q} onChange={e => setQ(e.target.value)} placeholder="搜索(Ctrl+Shift+F)" style={{ flex: 1, background: '#222', border: '1px solid #444', color: '#ddd', borderRadius: 4, padding: '4px 8px', fontSize: 13 }} />
{q && <button onClick={() => setQ('')} style={{ background: 'none', border: 'none', color: '#888', cursor: 'pointer' }}>×</button>}
<input value={q} onChange={e => 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 && <button onClick={() => setQ('')} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer' }}>×</button>}
</div>
{searching && <div style={{ opacity: 0.5, fontSize: 12, marginTop: 4 }}>搜索中…</div>}
{q && !searching && hits.length === 0 && <div style={{ opacity: 0.5, fontSize: 12, marginTop: 4 }}>没有匹配</div>}
@@ -33,7 +33,7 @@ export function SearchBox() {
{hits.map(h => (
<div key={h.path} onClick={() => { openDoc(h.path); setQ(''); }}
style={{ padding: '4px 6px', cursor: 'pointer', borderRadius: 4, fontSize: 13 }}
onMouseEnter={e => (e.currentTarget.style.background = '#2a2a3a')}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--control-border)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}>
<div>{h.title}</div>
<div style={{ opacity: 0.5, fontSize: 11 }}>{h.snippet}</div>
@@ -41,7 +41,7 @@ export function SearchBox() {
))}
</div>
)}
{q && <div style={{ marginTop: 4 }}><button onClick={() => { setQ(''); loadTree(); }} style={{ background: 'none', border: 'none', color: '#888', cursor: 'pointer', fontSize: 12 }}>清空恢复树</button></div>}
{q && <div style={{ marginTop: 4 }}><button onClick={() => { setQ(''); loadTree(); }} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 12 }}>清空恢复树</button></div>}
</div>
);
}
}
@@ -11,11 +11,11 @@ export function TagFilter() {
if (tags.length === 0) return null;
const toggle = (t: string) => setSelected(s => s.includes(t) ? s.filter(x => x !== t) : [...s, t]);
return (
<div style={{ padding: '6px 8px', borderTop: '1px solid #2a2a3a', maxHeight: 120, overflow: 'auto' }}>
<div style={{ padding: '6px 8px', borderTop: '1px solid var(--control-border)', maxHeight: 120, overflow: 'auto' }}>
<div style={{ opacity: 0.5, fontSize: 11, marginBottom: 4 }}>标签({tags.length})</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>{tags.map(t => (
<span key={t.tag} onClick={() => toggle(t.tag)} style={{ cursor: 'pointer', padding: '1px 7px', borderRadius: 10, fontSize: 11, background: selected.includes(t.tag) ? '#4a9eff' : '#2a2a3a', color: selected.includes(t.tag) ? '#fff' : '#aaa' }}>{t.tag} {t.count}</span>
<span key={t.tag} onClick={() => toggle(t.tag)} style={{ cursor: 'pointer', padding: '1px 7px', borderRadius: 10, fontSize: 11, background: selected.includes(t.tag) ? 'var(--accent)' : 'var(--control-border)', color: selected.includes(t.tag) ? '#fff' : 'var(--text-secondary)' }}>{t.tag} {t.count}</span>
))}</div>
</div>
);
}
}
@@ -6,10 +6,10 @@ import { useTabs } from '../tabs/tab-store';
import { invoke } from '../../ipc-client';
import { IPC } from '@hunter/shared';
const pageBtn: React.CSSProperties = { padding: '8px 14px', border: 'none', background: 'transparent', color: '#888', cursor: 'pointer', borderBottom: '2px solid transparent', fontSize: 13 };
const row: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 0', borderBottom: '1px solid #2a2a3a' };
const lbl: React.CSSProperties = { fontSize: 13, color: '#ccc' };
const toggle = (on: boolean): React.CSSProperties => ({ width: 36, height: 20, borderRadius: 10, background: on ? '#4a9eff' : '#444', position: 'relative', cursor: 'pointer', border: 'none', transition: 'background 0.2s' });
const pageBtn: React.CSSProperties = { padding: '8px 14px', border: 'none', background: 'transparent', color: 'var(--text-tertiary)', cursor: 'pointer', borderBottom: '2px solid transparent', fontSize: 13 };
const row: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 0', borderBottom: '1px solid var(--control-border)' };
const lbl: React.CSSProperties = { fontSize: 13, color: 'var(--text-primary)' };
const toggle = (on: boolean): React.CSSProperties => ({ width: 36, height: 20, borderRadius: 10, background: on ? 'var(--accent)' : 'var(--control-border)', position: 'relative', cursor: 'pointer', border: 'none', transition: 'background 0.2s' });
export function SettingsPanel({ onClose }: { onClose: () => void }) {
const s = useSettings();
@@ -40,14 +40,14 @@ export function SettingsPanel({ onClose }: { onClose: () => void }) {
}, [recId, setCombo, saveKeymap]);
return (
<div onClick={e => { 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 }}>
<div style={{ width: 520, maxHeight: '80vh', overflow: 'auto', background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
<div style={{ width: 520, maxHeight: '80vh', overflow: 'auto', background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<strong style={{ fontSize: 15 }}>设置</strong>
<button onClick={onClose} style={{ background: 'none', border: 'none', color: '#888', cursor: 'pointer', fontSize: 16 }}>✕</button>
<button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 16 }}>✕</button>
</div>
<div style={{ display: 'flex', borderBottom: '1px solid #2a2a3a', marginBottom: 12 }}>
<div style={{ display: 'flex', borderBottom: '1px solid var(--control-border)', marginBottom: 12 }}>
{(['editor','keymap','path','other'] as const).map(p => (
<button key={p} onClick={() => setPage(p)} style={{ ...pageBtn, color: page===p ? '#fff' : '#888', borderBottom: page===p ? '2px solid #4a9eff' : '2px solid transparent' }}>{({editor:'编辑器',keymap:'快捷键',path:'文件路径',other:'其他'} as any)[p]}</button>
<button key={p} onClick={() => setPage(p)} style={{ ...pageBtn, color: page===p ? '#fff' : 'var(--text-tertiary)', borderBottom: page===p ? '2px solid var(--accent)' : '2px solid transparent' }}>{({editor:'编辑器',keymap:'快捷键',path:'文件路径',other:'其他'} as any)[p]}</button>
))}
</div>
{page === 'editor' && <>
@@ -69,7 +69,7 @@ export function SettingsPanel({ onClose }: { onClose: () => void }) {
<span style={lbl}>{b.label}</span>
<span style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{inConflict && <span style={{ color: '#e88', fontSize: 11 }}>冲突</span>}
<code style={{ background: '#15151f', padding: '1px 6px', borderRadius: 3, color: recId === b.id ? '#fc4' : '#9cf', minWidth: 90, textAlign: 'center' }}>{recId === b.id ? '按下组合键…' : b.combo}</code>
<code style={{ background: 'var(--bg-secondary)', padding: '1px 6px', borderRadius: 3, color: recId === b.id ? '#fc4' : 'var(--accent-text)', minWidth: 90, textAlign: 'center' }}>{recId === b.id ? '按下组合键…' : b.combo}</code>
<button onClick={() => setRecId(b.id)} style={{ ...sel, cursor: 'pointer' }}>录入</button>
</span>
</div>
@@ -83,7 +83,7 @@ export function SettingsPanel({ onClose }: { onClose: () => void }) {
);
}
const sel: React.CSSProperties = { background: '#15151f', border: '1px solid #3a3a4a', color: '#ddd', borderRadius: 4, padding: '4px 8px', fontSize: 13 };
const sel: React.CSSProperties = { background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', color: 'var(--text-primary)', borderRadius: 4, padding: '4px 8px', fontSize: 13 };
function dot(on: boolean): React.CSSProperties { return { position: 'absolute', top: 2, left: on ? 18 : 2, width: 16, height: 16, borderRadius: '50%', background: '#fff', transition: 'left 0.2s' }; }
// FR-V5: 默认库路径设置页——查看当前库、浏览选择、立即切换
@@ -109,7 +109,7 @@ function PathPage() {
<>
<div style={{ ...row, alignItems: 'flex-start' }}>
<span style={{ ...lbl, paddingTop: 6 }}>当前库路径</span>
<span style={{ fontSize: 12, color: '#9cf', maxWidth: 320, wordBreak: 'break-all', textAlign: 'right' }}>{vault?.currentRoot ?? '—'}</span>
<span style={{ fontSize: 12, color: 'var(--accent-text)', maxWidth: 320, wordBreak: 'break-all', textAlign: 'right' }}>{vault?.currentRoot ?? '—'}</span>
</div>
<div style={{ ...row, alignItems: 'flex-start' }}>
<span style={{ ...lbl, paddingTop: 6 }}>新库路径</span>
@@ -121,10 +121,10 @@ function PathPage() {
<div style={{ ...row }}>
<span style={lbl} />
<span style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{msg && <span style={{ fontSize: 12, color: msg.startsWith('切换失败') ? '#f88' : (msg.startsWith('已切换') ? '#8c8' : '#9cf') }}>{msg}</span>}
<button onClick={apply} style={{ ...sel, cursor: 'pointer', background: '#2a4a6a', color: '#9cf' }}>立即切换</button>
{msg && <span style={{ fontSize: 12, color: msg.startsWith('切换失败') ? '#f88' : (msg.startsWith('已切换') ? '#8c8' : 'var(--accent-text)') }}>{msg}</span>}
<button onClick={apply} style={{ ...sel, cursor: 'pointer', background: 'var(--accent)', color: 'var(--accent-text)' }}>立即切换</button>
</span>
</div>
</>
);
}
}
@@ -12,5 +12,5 @@ export function Resizer({ onDrag, side }: { onDrag: (delta: number) => void; sid
window.addEventListener('mousemove', move);
window.addEventListener('mouseup', up);
};
return <div onMouseDown={onDown} style={{ width: 5, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }} onMouseEnter={e => (e.currentTarget.style.background = '#4a9eff')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} />;
}
return <div onMouseDown={onDown} style={{ width: 5, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }} onMouseEnter={e => (e.currentTarget.style.background = 'var(--accent)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} />;
}
@@ -8,8 +8,8 @@ const noDrag = { WebkitAppRegion: 'no-drag' } as React.CSSProperties;
export function TitleBar({ onToggleView, view, onSettings, onAppearance, onToggleSidebar, onToggleMinimal, minimal }: { onToggleView: () => void; view: 'editor' | 'graph'; onSettings: () => void; onAppearance: () => void; onToggleSidebar: () => void; onToggleMinimal: () => void; minimal: boolean }) {
const { loadTree } = useStore();
return (
<div style={{ height: 36, display: 'flex', alignItems: 'center', background: '#161620', borderBottom: '1px solid #2a2a3a', WebkitAppRegion: 'drag', userSelect: 'none' } as React.CSSProperties}>
<div style={{ padding: '0 12px', color: '#7ab8ff', fontWeight: 600, fontSize: 13 }}>狩夜人笔记</div>
<div style={{ height: 36, display: 'flex', alignItems: 'center', background: 'var(--bg-secondary)', borderBottom: '1px solid var(--control-border)', WebkitAppRegion: 'drag', userSelect: 'none' } as React.CSSProperties}>
<div style={{ padding: '0 12px', color: 'var(--accent-text)', fontWeight: 600, fontSize: 13 }}>狩夜人笔记</div>
<div style={drag} />
<button title="侧边栏开关" onClick={onToggleSidebar} style={{ ...btn, fontSize: 14 }}>▤</button>
<button title="外观定制" onClick={onAppearance} style={{ ...btn, fontSize: 14 }}>🎨</button>
@@ -18,7 +18,7 @@ export function TitleBar({ onToggleView, view, onSettings, onAppearance, onToggl
<button title="切换 编辑器/图谱" onClick={onToggleView} style={btn}>{view === 'editor' ? '🕸' : '✎'}</button>
<button title="极简模式" onClick={onToggleMinimal} style={btn}>{minimal ? '⛶' : '⊡'}</button>
<button title="设置" onClick={onSettings} style={btn}>⚙</button>
<span style={{ width: 1, background: '#333', height: 16, margin: '0 4px' }} />
<span style={{ width: 1, background: 'var(--control-border)', height: 16, margin: '0 4px' }} />
<button title="最小化" onClick={() => send(IPC.winMinimize, undefined as never)} style={btn}>—</button>
<button title="最大化/还原" onClick={() => send(IPC.winMaximize, undefined as never)} style={btn}>▢</button>
<button title="关闭" onMouseEnter={e => (e.currentTarget.style.background = '#e81123')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} onClick={() => send(IPC.winClose, undefined as never)} style={btn}>✕</button>
@@ -27,4 +27,4 @@ export function TitleBar({ onToggleView, view, onSettings, onAppearance, onToggl
);
}
const btn: React.CSSProperties = { width: 36, height: 28, border: 'none', background: 'transparent', color: '#aaa', cursor: 'pointer', fontSize: 12 };
const btn: React.CSSProperties = { width: 36, height: 28, border: 'none', background: 'transparent', color: 'var(--text-secondary)', cursor: 'pointer', fontSize: 12 };
@@ -14,22 +14,22 @@ export function TabBar() {
if (tabs.length === 0) return null;
return (
<div data-testid="tabbar" style={{ display: 'flex', borderBottom: '1px solid #2a2a3a', background: '#161620', overflowX: 'auto' }}>
<div data-testid="tabbar" style={{ display: 'flex', borderBottom: '1px solid var(--control-border)', background: 'var(--bg-secondary)', overflowX: 'auto' }}>
{tabs.map(t => (
<div key={t.id} onClick={() => setActive(t.id)}
style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 10px', cursor: 'pointer',
borderBottom: activeId === t.id ? '2px solid #4a9eff' : '2px solid transparent',
background: activeId === t.id ? '#1a1a28' : 'transparent', fontSize: 12, color: activeId === t.id ? '#ddd' : '#888', whiteSpace: 'nowrap' }}>
{t.pinned && <span style={{ color: '#ffd54a' }}>★</span>}
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 && <span style={{ color: 'var(--accent)' }}>★</span>}
<span>{t.path.split('/').pop()?.replace(/\.md$/, '')}</span>
{t.pinned ? (
<span style={{ color: '#888', fontSize: 10 }}>📌</span>
<span style={{ color: 'var(--text-tertiary)', fontSize: 10 }}>📌</span>
) : (
<span onClick={e => { e.stopPropagation(); closeTab(t.id); }} style={{ color: '#888', cursor: 'pointer', fontSize: 12 }} onMouseEnter={e => (e.currentTarget.style.color = '#f88')} onMouseLeave={e => (e.currentTarget.style.color = '#888')}>×</span>
<span onClick={e => { 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)')}>×</span>
)}
<span onClick={e => { e.stopPropagation(); togglePin(t.id); }} style={{ color: t.pinned ? '#ffd54a' : '#666', cursor: 'pointer', fontSize: 10 }} title={t.pinned ? '取消固定' : '固定'}>●</span>
<span onClick={e => { e.stopPropagation(); togglePin(t.id); }} style={{ color: t.pinned ? 'var(--accent)' : 'var(--text-tertiary)', cursor: 'pointer', fontSize: 10 }} title={t.pinned ? '取消固定' : '固定'}>●</span>
</div>
))}
</div>
);
}
}
@@ -8,7 +8,7 @@ import { useTabs } from '../tabs/tab-store';
function notify(msg: string) {
const el = document.createElement('div');
el.textContent = msg;
el.style.cssText = 'position:fixed;top:50px;right:20px;background:#2a4a6a;color:#9cf;padding:10px 16px;border-radius:6px;z-index:3000;font-size:13px;box-shadow:0 4px 12px rgba(0,0,0,0.3)';
el.style.cssText = 'position:fixed;top:50px;right:20px;background:var(--accent);color:var(--accent-text);padding:10px 16px;border-radius:6px;z-index:3000;font-size:13px;box-shadow:0 4px 12px rgba(0,0,0,0.3)';
document.body.appendChild(el);
setTimeout(() => el.remove(), 2500);
}
@@ -46,7 +46,7 @@ export function Toolbar() {
alert2(r.ok.length ? `导入成功 ${r.ok.length} 项` : '未选择文件');
};
return (
<div style={{ padding: '6px 12px', borderBottom: '1px solid #2a2a3a', display: 'flex', gap: 8, background: '#181822' }}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--control-border)', display: 'flex', gap: 8, background: 'var(--bg-secondary)' }}>
<button onClick={newDoc} style={btn}>+ 新建文档</button>
<button onClick={newFolder} style={btn}>📁 新建文件夹</button>
<button onClick={importFiles} style={btn}>📥 导入</button>
@@ -54,4 +54,4 @@ export function Toolbar() {
</div>
);
}
const btn: React.CSSProperties = { padding: '4px 10px', border: '1px solid #3a3a4a', background: 'transparent', color: '#ccc', borderRadius: 4, cursor: 'pointer', fontSize: 12 };
const btn: React.CSSProperties = { padding: '4px 10px', border: '1px solid var(--control-border)', background: 'transparent', color: 'var(--text-primary)', borderRadius: 4, cursor: 'pointer', fontSize: 12 };
+6 -6
View File
@@ -10,7 +10,7 @@ html, body, #root { margin: 0; height: 100%; }
font-family: 'Consolas', 'Source Code Pro', system-ui, sans-serif;
font-size: 14px;
line-height: 1.7;
color: #ddd;
color: var(--text-primary);
outline: none;
max-width: 860px;
margin: 0 auto;
@@ -20,10 +20,10 @@ html, body, #root { margin: 0; height: 100%; }
.tiptap-wrap .ProseMirror h2 { font-size: 1.5em; margin: 0.7em 0 0.35em; }
.tiptap-wrap .ProseMirror h3 { font-size: 1.25em; margin: 0.6em 0 0.3em; }
.tiptap-wrap .ProseMirror code {
background: #2a2a2a; padding: 1px 5px; border-radius: 3px; font-family: 'Consolas', monospace;
background: var(--bg-secondary); padding: 1px 5px; border-radius: 3px; font-family: 'Consolas', monospace;
}
.tiptap-wrap .ProseMirror pre {
background: #161616; padding: 12px 16px; border-radius: 6px; overflow: auto;
background: var(--bg-secondary); padding: 12px 16px; border-radius: 6px; overflow: auto;
}
.tiptap-wrap .ProseMirror pre code { background: none; padding: 0; }
/* highlight.js 暗色主题(CodeBlockLowlight 装饰 span,FR-E5) */
@@ -63,13 +63,13 @@ html, body, #root { margin: 0; height: 100%; }
border-left: 2px solid #63b3ed !important;
}
.tiptap-wrap .ProseMirror blockquote {
border-left: 3px solid #444; padding-left: 12px; color: #aaa; margin: 0.6em 0;
border-left: 3px solid var(--control-border); padding-left: 12px; color: var(--text-secondary); margin: 0.6em 0;
}
.tiptap-wrap .ProseMirror ul, .tiptap-wrap .ProseMirror ol { padding-left: 24px; }
.tiptap-wrap .ProseMirror table { border-collapse: collapse; }
.tiptap-wrap .ProseMirror th, .tiptap-wrap .ProseMirror td {
border: 1px solid #444; padding: 4px 8px;
border: 1px solid var(--control-border); padding: 4px 8px;
}
.tiptap-wrap .ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder); color: #666; pointer-events: none; float: left;
content: attr(data-placeholder); color: var(--text-tertiary); pointer-events: none; float: left;
}