From 343b075b074424261106d3560efa357bea5ee25d Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 14 Feb 2026 21:19:38 +0100 Subject: [PATCH] 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 --- e2e/keyboard-shortcuts.spec.ts | 43 ++++++++++++++++++++++++++++++++++ e2e/quick-open.spec.ts | 6 ++--- src/App.tsx | 21 ++++++++++++++++- src/components/Toast.css | 25 ++++++++++++++++++++ src/components/Toast.tsx | 23 ++++++++++++++++++ 5 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 e2e/keyboard-shortcuts.spec.ts create mode 100644 src/components/Toast.css create mode 100644 src/components/Toast.tsx diff --git a/e2e/keyboard-shortcuts.spec.ts b/e2e/keyboard-shortcuts.spec.ts new file mode 100644 index 00000000..c0e92be8 --- /dev/null +++ b/e2e/keyboard-shortcuts.spec.ts @@ -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() +}) diff --git a/e2e/quick-open.spec.ts b/e2e/quick-open.spec.ts index 91d4c4cf..d0380131 100644 --- a/e2e/quick-open.spec.ts +++ b/e2e/quick-open.spec.ts @@ -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() }) diff --git a/src/App.tsx b/src/App.tsx index d7e69fe2..0b272853 100644 --- a/src/App.tsx +++ b/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([]) const [showCreateDialog, setShowCreateDialog] = useState(false) const [showQuickOpen, setShowQuickOpen] = useState(false) + const [toastMessage, setToastMessage] = useState(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} /> + setToastMessage(null)} /> 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 ( +
+ {message} +
+ ) +}