feat(M5/M1): FR-A12/V4 过渡动效+最近库下拉——styles.css 全局 transition+panel-slide-in keyframe+按钮悬停(A12); SettingsPanel/CheatSheet 加 panel-slide-in; Toolbar 最近库下拉快速切换(最多5)+高亮当前库(V4); recents-transition E2E 1 项
This commit is contained in:
@@ -14,7 +14,7 @@ export function CheatSheet({ onClose }: { onClose: () => void }) {
|
|||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
return (
|
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 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: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
|
<div className="panel-slide-in" 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>
|
<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>
|
||||||
{SCOPE_ORDER.map(scope => {
|
{SCOPE_ORDER.map(scope => {
|
||||||
const items = bindings.filter(b => b.scope === scope);
|
const items = bindings.filter(b => b.scope === scope);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function SettingsPanel({ onClose }: { onClose: () => void }) {
|
|||||||
}, [recId, onClose]);
|
}, [recId, onClose]);
|
||||||
return (
|
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 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: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
|
<div className="panel-slide-in" 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 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
<strong style={{ fontSize: 15 }}>设置</strong>
|
<strong style={{ fontSize: 15 }}>设置</strong>
|
||||||
<button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 16 }}>✕</button>
|
<button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 16 }}>✕</button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { invoke } from '../../ipc-client';
|
|||||||
import { IPC } from '@hunter/shared';
|
import { IPC } from '@hunter/shared';
|
||||||
import { useDialog } from '../dialog/dialog-store';
|
import { useDialog } from '../dialog/dialog-store';
|
||||||
import { useTabs } from '../tabs/tab-store';
|
import { useTabs } from '../tabs/tab-store';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
// 极简浮层通知(FR-B1,后续抽组件)
|
// 极简浮层通知(FR-B1,后续抽组件)
|
||||||
function notify(msg: string) {
|
function notify(msg: string) {
|
||||||
@@ -15,8 +16,9 @@ function notify(msg: string) {
|
|||||||
const alert2 = notify;
|
const alert2 = notify;
|
||||||
|
|
||||||
export function Toolbar() {
|
export function Toolbar() {
|
||||||
const { loadTree, loadVault, openDoc } = useStore();
|
const { loadTree, loadVault, openDoc, vault } = useStore();
|
||||||
const { showInput } = useDialog();
|
const { showInput } = useDialog();
|
||||||
|
const [recentsOpen, setRecentsOpen] = useState(false);
|
||||||
const newDoc = async () => {
|
const newDoc = async () => {
|
||||||
const name = await showInput('新建文档', '输入文档标题…');
|
const name = await showInput('新建文档', '输入文档标题…');
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
@@ -51,6 +53,24 @@ export function Toolbar() {
|
|||||||
<button onClick={newFolder} style={btn}>📁 新建文件夹</button>
|
<button onClick={newFolder} style={btn}>📁 新建文件夹</button>
|
||||||
<button onClick={importFiles} style={btn}>📥 导入</button>
|
<button onClick={importFiles} style={btn}>📥 导入</button>
|
||||||
<button onClick={switchVault} style={btn}>⇄ 切换库</button>
|
<button onClick={switchVault} style={btn}>⇄ 切换库</button>
|
||||||
|
{/* FR-V4:最近库下拉快速切换 */}
|
||||||
|
{vault?.recents && vault.recents.length > 1 && (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<button onClick={() => setRecentsOpen(o => !o)} style={btn} data-testid="recents-toggle">最近库 ▾</button>
|
||||||
|
{recentsOpen && (
|
||||||
|
<div style={{ position: 'absolute', left: 0, top: 30, background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 6, padding: 4, zIndex: 500, minWidth: 200, boxShadow: '0 4px 12px rgba(0,0,0,0.3)' }} onMouseLeave={() => setRecentsOpen(false)}>
|
||||||
|
{vault.recents.map(r => (
|
||||||
|
<div key={r} onClick={async () => { setRecentsOpen(false); await useStore.getState().saveNow(); await invoke(IPC.vaultSwitch, { root: r }); useTabs.getState().closeAll(); useStore.getState().resetEditor(); await loadVault(); await loadTree(); }}
|
||||||
|
style={{ padding: '6px 10px', cursor: 'pointer', borderRadius: 4, fontSize: 12, color: r === vault.currentRoot ? 'var(--accent)' : 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.background = 'var(--control-border)')}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}>
|
||||||
|
{r === vault.currentRoot ? '★ ' : ''}{r.split(/[\\/]/).pop()}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { _electron as electron } from 'playwright';
|
||||||
|
import { promises as fs } from 'node:fs';
|
||||||
|
import nodePath from 'node:path';
|
||||||
|
import os from 'node:os';
|
||||||
|
|
||||||
|
const MAIN_JS = nodePath.join(process.cwd(), 'dist-electron/main/index.js');
|
||||||
|
|
||||||
|
async function launch(userData: string) {
|
||||||
|
const app = await electron.launch({ args: ['--user-data-dir=' + userData, MAIN_JS] });
|
||||||
|
const page = await app.firstWindow();
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
const invoke = (ch: string, args: any) => page.evaluate(({ ch, args }) => (globalThis as any).dsh.invoke(ch, args), { ch, args });
|
||||||
|
return { app, page, invoke };
|
||||||
|
}
|
||||||
|
|
||||||
|
// FR-V4:最近库下拉快速切换
|
||||||
|
test('FR-V4:最近库下拉快速切换', async () => {
|
||||||
|
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-v4-'));
|
||||||
|
const ud = nodePath.join(tmp, 'ud');
|
||||||
|
const v1 = nodePath.join(tmp, 'vault1');
|
||||||
|
const v2 = nodePath.join(tmp, 'vault2');
|
||||||
|
await fs.mkdir(ud, { recursive: true });
|
||||||
|
await fs.mkdir(v1, { recursive: true });
|
||||||
|
await fs.mkdir(v2, { recursive: true });
|
||||||
|
|
||||||
|
const a = await launch(ud);
|
||||||
|
// 切换到 v1(自动加入 recents)— 通过 UI 工具栏
|
||||||
|
await a.invoke('vault:switch', { root: v1 });
|
||||||
|
await a.page.waitForTimeout(500);
|
||||||
|
// 切换到 v2
|
||||||
|
await a.invoke('vault:switch', { root: v2 });
|
||||||
|
await a.page.waitForTimeout(500);
|
||||||
|
// 刷新渲染层 vault 元数据
|
||||||
|
await a.page.evaluate(() => (window as any).__STORE__?.getState?.().loadVault?.());
|
||||||
|
await a.page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// 现在 recents 应有 v1 和 v2
|
||||||
|
const meta = await a.invoke('vault:list', null as any);
|
||||||
|
expect(meta.recents.length).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
|
// 点击「最近库」下拉
|
||||||
|
await a.page.getByTestId('recents-toggle').click();
|
||||||
|
await a.page.waitForTimeout(300);
|
||||||
|
// 应显示两个库
|
||||||
|
const dropdown = a.page.locator('div').filter({ has: a.page.getByText(v1.split(/[\\/]/).pop()!) });
|
||||||
|
await expect(dropdown.first()).toBeVisible({ timeout: 3000 });
|
||||||
|
|
||||||
|
await a.app.close();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user