Implement full suite of Evernote keyboard shortcuts and wire KeyboardShortcutsModal

This commit is contained in:
chrisf committed 2026-10-23 07:59:39 -06:00
1 parent 49ec3bc645
commit 99522b3e09
1 file changed
+217 -23
+217 -23
View File
@@ -47,6 +47,7 @@ import {
GitMerge,
History,
Users,
Keyboard,
} from 'lucide-react';
import {
SidebarData,
@@ -61,6 +62,7 @@ import TriageModal from './TriageModal';
import MergeNotebookModal from './MergeNotebookModal';
import ShareNotebookModal from './ShareNotebookModal';
import NoteHistoryModal from './NoteHistoryModal';
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
import { PdfAutoThumbnail, InlinePdfViewerCard } from './PdfNoteComponents';
export default function EvercloneWorkspace() {
@@ -268,18 +270,6 @@ export default function EvercloneWorkspace() {
return () => clearTimeout(t);
}, [searchQuery]);
// Keyboard shortcut Ctrl+K for search, Alt+N for new note
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
searchInputRef.current?.focus();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
const handleNavigateToNote = useCallback(
(targetNoteId: string) => {
if (!targetNoteId || targetNoteId === selectedNoteId) return;
@@ -841,6 +831,192 @@ export default function EvercloneWorkspace() {
await fetchNotesList();
};
// Stable refs for keyboard shortcut handlers
const activeNoteRef = useRef(activeNote);
activeNoteRef.current = activeNote;
const handleCreateNoteRef = useRef(handleCreateNote);
handleCreateNoteRef.current = handleCreateNote;
const handleImmediatePatchRef = useRef(handleImmediatePatch);
handleImmediatePatchRef.current = handleImmediatePatch;
const handleDeleteOrTrashNoteRef = useRef(handleDeleteOrTrashNote);
handleDeleteOrTrashNoteRef.current = handleDeleteOrTrashNote;
const handleCopyNoteLinkRef = useRef(handleCopyNoteLink);
handleCopyNoteLinkRef.current = handleCopyNoteLink;
const handleHistoryBackRef = useRef(handleHistoryBack);
handleHistoryBackRef.current = handleHistoryBack;
const handleHistoryForwardRef = useRef(handleHistoryForward);
handleHistoryForwardRef.current = handleHistoryForward;
// Global Keyboard Shortcuts Suite (Evernote compatible)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const target = e.target as HTMLElement | null;
const isInput =
target &&
(target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable ||
target.closest('.ProseMirror'));
// 1. Esc: Close open modals or blur focused inputs
if (e.key === 'Escape') {
if (shortcutsModalOpen) {
e.preventDefault();
setShortcutsModalOpen(false);
return;
}
if (mergeModalOpen) {
e.preventDefault();
setMergeModalOpen(false);
return;
}
if (shareModalOpen) {
e.preventDefault();
setShareModalOpen(false);
return;
}
if (historyModalOpen) {
e.preventDefault();
setHistoryModalOpen(false);
return;
}
if (triageModalOpen) {
e.preventDefault();
setTriageModalOpen(false);
return;
}
if (thumbnailEditorOpen) {
e.preventDefault();
setThumbnailEditorOpen(false);
return;
}
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) {
target.blur();
return;
}
}
// 2. Alt + N: Create New Note
if (e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === 'n') {
e.preventDefault();
handleCreateNoteRef.current();
return;
}
// 3. Ctrl + K / Cmd + K: Focus Search (when not inside editor)
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k') {
if (!target?.closest('.ProseMirror')) {
e.preventDefault();
searchInputRef.current?.focus();
searchInputRef.current?.select();
return;
}
}
// 4. "/" : Jump to search (only when NOT in an input/editor)
if (!isInput && e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
searchInputRef.current?.focus();
searchInputRef.current?.select();
return;
}
// 5. "?" or Ctrl + /: Open Shortcuts Modal
if (
(!isInput && e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) ||
((e.ctrlKey || e.metaKey) && e.key === '/')
) {
e.preventDefault();
setShortcutsModalOpen((v) => !v);
return;
}
// 6. Ctrl + \ : Toggle Sidebar Collapse
if ((e.ctrlKey || e.metaKey) && e.key === '\\') {
e.preventDefault();
setSidebarCollapsed((v) => !v);
return;
}
// 7. Alt + Left / Alt + Right : Note Navigation History (Back / Forward)
if (e.altKey && !e.ctrlKey && !e.metaKey && (e.key === 'ArrowLeft' || e.key === 'Left')) {
e.preventDefault();
handleHistoryBackRef.current();
return;
}
if (e.altKey && !e.ctrlKey && !e.metaKey && (e.key === 'ArrowRight' || e.key === 'Right')) {
e.preventDefault();
handleHistoryForwardRef.current();
return;
}
// 8. Ctrl + Alt + L : Copy Internal Note Link
if ((e.ctrlKey || e.metaKey) && e.altKey && e.key.toLowerCase() === 'l') {
e.preventDefault();
handleCopyNoteLinkRef.current();
return;
}
// 9. Ctrl + Shift + P : Pin / Unpin Note
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'p') {
const cur = activeNoteRef.current;
if (cur) {
e.preventDefault();
handleImmediatePatchRef.current({ isPinned: !cur.isPinned });
}
return;
}
// 10. Ctrl + Shift + S : Star / Add to Shortcuts
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 's') {
const cur = activeNoteRef.current;
if (cur) {
e.preventDefault();
handleImmediatePatchRef.current({ isShortcut: !cur.isShortcut });
}
return;
}
// 11. Ctrl + Shift + T : Focus Tag Input
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 't') {
e.preventDefault();
const tagEl = document.getElementById('everclone-add-tag-input');
tagEl?.focus();
return;
}
// 12. Ctrl + Shift + Del / Backspace : Move Note to Trash
if (
(e.ctrlKey || e.metaKey) &&
e.shiftKey &&
(e.key === 'Delete' || e.key === 'Backspace')
) {
const cur = activeNoteRef.current;
if (cur && !cur.isTrashed) {
e.preventDefault();
handleDeleteOrTrashNoteRef.current(false);
}
return;
}
// 13. Ctrl + S : Prevent default browser webpage save
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 's') {
e.preventDefault();
return;
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [
shortcutsModalOpen,
mergeModalOpen,
shareModalOpen,
historyModalOpen,
triageModalOpen,
thumbnailEditorOpen,
]);
const handleMergedNotebook = useCallback(
async (
sourceName: string,
@@ -1935,17 +2111,29 @@ export default function EvercloneWorkspace() {
<div className="flex items-center justify-between text-[11px] text-gray-500 px-1">
<span>Everclone v1.2</span>
<button
type="button"
onClick={() => {
setActiveView('settings');
setMobileMenuOpen(false);
}}
className="hover:text-gray-300 underline"
title="Open Import, Export, Clipper & Email Settings"
>
Settings
</button>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setShortcutsModalOpen(true)}
className="hover:text-gray-300 flex items-center gap-1 transition"
title="Keyboard Shortcuts Cheat Sheet (? or Ctrl+/)"
>
<Keyboard className="w-3 h-3 text-gray-400" />
<span>Shortcuts</span>
</button>
<span>·</span>
<button
type="button"
onClick={() => {
setActiveView('settings');
setMobileMenuOpen(false);
}}
className="hover:text-gray-300 underline"
title="Open Import, Export, Clipper & Email Settings"
>
Settings
</button>
</div>
</div>
</div>
)}
@@ -3467,6 +3655,7 @@ export default function EvercloneWorkspace() {
className="inline-flex items-center"
>
<input
id="everclone-add-tag-input"
type="text"
list="everclone-tags-datalist"
value={tagInput}
@@ -3547,6 +3736,11 @@ export default function EvercloneWorkspace() {
}}
/>
)}
<KeyboardShortcutsModal
isOpen={shortcutsModalOpen}
onClose={() => setShortcutsModalOpen(false)}
/>
</>
)}
{/* Floating PWA Install Banner on Mobile */}