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:
lucaronin
2026-02-14 21:19:38 +01:00
parent 218a13b859
commit 343b075b07
5 changed files with 114 additions and 4 deletions

View 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()
})

View File

@@ -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()
})

View File

@@ -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
View 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
View 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>
)
}