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:
@@ -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>
|
||||
|
||||
@@ -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,26 +35,26 @@ 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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,12 +15,12 @@ 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>
|
||||
|
||||
@@ -95,7 +95,7 @@ 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>
|
||||
|
||||
@@ -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,10 +11,10 @@ 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,8 +121,8 @@ 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,20 +14,20 @@ 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 };
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user