Add keyboard shortcuts: Cmd+N, Cmd+S, Cmd+W, Cmd+P (M5 Task 3)
Cmd+N opens create note dialog, Cmd+S shows save toast, Cmd+W closes active tab, Cmd+P opens quick open palette. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
43
e2e/keyboard-shortcuts.spec.ts
Normal file
43
e2e/keyboard-shortcuts.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Cmd+N opens create note dialog', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.keyboard.press('Meta+n')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('.create-dialog')).toBeVisible()
|
||||
await expect(page.locator('.create-dialog__title')).toHaveText('Create New Note')
|
||||
})
|
||||
|
||||
test('Cmd+S shows save toast', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.keyboard.press('Meta+s')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await expect(page.locator('.toast')).toBeVisible()
|
||||
await expect(page.locator('.toast')).toHaveText('Saved')
|
||||
|
||||
await page.screenshot({ path: 'test-results/save-toast.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('Cmd+W closes the active tab', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Open a note
|
||||
await page.click('.note-list__item')
|
||||
await page.waitForTimeout(300)
|
||||
await expect(page.locator('.editor__tab--active')).toBeVisible()
|
||||
|
||||
// Close it with Cmd+W
|
||||
await page.keyboard.press('Meta+w')
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Tab should be gone, placeholder should show
|
||||
await expect(page.locator('.editor__tab')).not.toBeVisible()
|
||||
await expect(page.locator('.editor__placeholder')).toBeVisible()
|
||||
})
|
||||
@@ -81,8 +81,8 @@ test('quick open: clicking outside closes palette', async ({ page }) => {
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.palette')).toBeVisible()
|
||||
|
||||
// Click the overlay (outside the palette)
|
||||
await page.click('.palette__overlay', { position: { x: 10, y: 10 } })
|
||||
await page.waitForTimeout(100)
|
||||
// Click the overlay area (outside the palette) using mouse click at top-left
|
||||
await page.mouse.click(10, 10)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.locator('.palette')).not.toBeVisible()
|
||||
})
|
||||
|
||||
21
src/App.tsx
21
src/App.tsx
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Sidebar } from './components/Sidebar'
|
||||
import { NoteList } from './components/NoteList'
|
||||
@@ -7,6 +7,7 @@ import { Inspector } from './components/Inspector'
|
||||
import { ResizeHandle } from './components/ResizeHandle'
|
||||
import { CreateNoteDialog, type NoteType } from './components/CreateNoteDialog'
|
||||
import { QuickOpenPalette } from './components/QuickOpenPalette'
|
||||
import { Toast } from './components/Toast'
|
||||
import { isTauri, mockInvoke, addMockEntry } from './mock-tauri'
|
||||
import type { VaultEntry, SidebarSelection, GitCommit } from './types'
|
||||
import './App.css'
|
||||
@@ -29,6 +30,12 @@ function App() {
|
||||
const [gitHistory, setGitHistory] = useState<GitCommit[]>([])
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [showQuickOpen, setShowQuickOpen] = useState(false)
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
||||
|
||||
// Refs for keyboard shortcuts (to avoid stale closures)
|
||||
const activeTabPathRef = useRef(activeTabPath)
|
||||
activeTabPathRef.current = activeTabPath
|
||||
const handleCloseTabRef = useRef<(path: string) => void>(() => {})
|
||||
|
||||
useEffect(() => {
|
||||
const loadVault = async () => {
|
||||
@@ -91,6 +98,16 @@ function App() {
|
||||
if (mod && e.key === 'p') {
|
||||
e.preventDefault()
|
||||
setShowQuickOpen(true)
|
||||
} else if (mod && e.key === 'n') {
|
||||
e.preventDefault()
|
||||
setShowCreateDialog(true)
|
||||
} else if (mod && e.key === 's') {
|
||||
e.preventDefault()
|
||||
setToastMessage('Saved')
|
||||
} else if (mod && e.key === 'w') {
|
||||
e.preventDefault()
|
||||
const path = activeTabPathRef.current
|
||||
if (path) handleCloseTabRef.current(path)
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
@@ -152,6 +169,7 @@ function App() {
|
||||
return next
|
||||
})
|
||||
}, [activeTabPath])
|
||||
handleCloseTabRef.current = handleCloseTab
|
||||
|
||||
const handleSwitchTab = useCallback((path: string) => {
|
||||
setActiveTabPath(path)
|
||||
@@ -273,6 +291,7 @@ function App() {
|
||||
onNavigate={handleNavigateWikilink}
|
||||
/>
|
||||
</div>
|
||||
<Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
|
||||
<QuickOpenPalette
|
||||
open={showQuickOpen}
|
||||
entries={entries}
|
||||
|
||||
25
src/components/Toast.css
Normal file
25
src/components/Toast.css
Normal file
@@ -0,0 +1,25 @@
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 32px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #2a2a4a;
|
||||
color: #e0e0e0;
|
||||
padding: 8px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
z-index: 1100;
|
||||
animation: toast-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes toast-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
23
src/components/Toast.tsx
Normal file
23
src/components/Toast.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useEffect } from 'react'
|
||||
import './Toast.css'
|
||||
|
||||
interface ToastProps {
|
||||
message: string | null
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function Toast({ message, onDismiss }: ToastProps) {
|
||||
useEffect(() => {
|
||||
if (!message) return
|
||||
const timer = setTimeout(onDismiss, 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [message, onDismiss])
|
||||
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div className="toast">
|
||||
{message}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user