feat: Add full suite of Evernote-compatible keyboard shortcuts and visual cheat sheet modal

This commit is contained in:
chrisf committed 2026-10-22 17:59:31 -06:00
1 parent 3de36ff39b
commit 8907b4c494
2 files changed
+286

No files matched your search

+1
View File
@@ -105,6 +105,7 @@ export default function EvercloneWorkspace() {
const [shareToast, setShareToast] = useState<string | null>(null);
const [historyStack, setHistoryStack] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState<number>(-1);
const [shortcutsModalOpen, setShortcutsModalOpen] = useState(false);
const activeFilterCount =
(filterNotebookId ? 1 : 0) +
+285
View File
@@ -0,0 +1,285 @@
'use client';
import React from 'react';
import {
Keyboard,
X,
FileText,
Search,
ArrowLeft,
ArrowRight,
Pin,
Star,
Trash2,
Tag,
Link2,
PanelLeft,
Bold,
Italic,
Underline,
List,
ListOrdered,
CheckSquare,
Code,
Highlighter,
Save,
} from 'lucide-react';
interface KeyboardShortcutsModalProps {
isOpen: boolean;
onClose: () => void;
}
interface ShortcutItem {
keys: string[];
description: string;
icon?: React.ReactNode;
}
export default function KeyboardShortcutsModal({
isOpen,
onClose,
}: KeyboardShortcutsModalProps) {
if (!isOpen) return null;
const navigationShortcuts: ShortcutItem[] = [
{
keys: ['Alt', 'N'],
description: 'Create New Note',
icon: <FileText className="w-3.5 h-3.5 text-evernote-green" />,
},
{
keys: ['Ctrl', 'K'],
description: 'Focus Search / Link',
icon: <Search className="w-3.5 h-3.5 text-blue-500" />,
},
{
keys: ['/'],
description: 'Jump to Search (when not editing)',
icon: <Search className="w-3.5 h-3.5 text-blue-500" />,
},
{
keys: ['Alt', '←'],
description: 'Previous Visited Note (Back)',
icon: <ArrowLeft className="w-3.5 h-3.5 text-amber-500" />,
},
{
keys: ['Alt', '→'],
description: 'Next Visited Note (Forward)',
icon: <ArrowRight className="w-3.5 h-3.5 text-amber-500" />,
},
{
keys: ['Ctrl', '\\'],
description: 'Toggle Left Sidebar',
icon: <PanelLeft className="w-3.5 h-3.5 text-gray-500" />,
},
{
keys: ['Ctrl', 'Alt', 'L'],
description: 'Copy Internal Note Link',
icon: <Link2 className="w-3.5 h-3.5 text-evernote-green" />,
},
{
keys: ['Ctrl', 'Shift', 'P'],
description: 'Pin / Unpin Note',
icon: <Pin className="w-3.5 h-3.5 text-evernote-green" />,
},
{
keys: ['Ctrl', 'Shift', 'S'],
description: 'Add / Remove Shortcut (Star)',
icon: <Star className="w-3.5 h-3.5 text-amber-400" />,
},
{
keys: ['Ctrl', 'Shift', 'T'],
description: 'Focus Add Tag Input',
icon: <Tag className="w-3.5 h-3.5 text-emerald-500" />,
},
{
keys: ['Ctrl', 'Shift', 'Del'],
description: 'Move Note to Trash',
icon: <Trash2 className="w-3.5 h-3.5 text-red-500" />,
},
{
keys: ['?'],
description: 'Show Shortcuts Cheat Sheet',
icon: <Keyboard className="w-3.5 h-3.5 text-gray-500" />,
},
{
keys: ['Esc'],
description: 'Close Modals / Deselect',
},
];
const editorShortcuts: ShortcutItem[] = [
{
keys: ['Ctrl', 'K'],
description: 'Insert Link / Internal Note Link',
icon: <Link2 className="w-3.5 h-3.5 text-evernote-green" />,
},
{
keys: ['Ctrl', 'S'],
description: 'Save Edits Immediately',
icon: <Save className="w-3.5 h-3.5 text-emerald-600" />,
},
{
keys: ['Ctrl', 'B'],
description: 'Bold Text',
icon: <Bold className="w-3.5 h-3.5" />,
},
{
keys: ['Ctrl', 'I'],
description: 'Italic Text',
icon: <Italic className="w-3.5 h-3.5" />,
},
{
keys: ['Ctrl', 'U'],
description: 'Underline Text',
icon: <Underline className="w-3.5 h-3.5" />,
},
{
keys: ['Ctrl', 'Shift', 'X'],
description: 'Strikethrough',
},
{
keys: ['Ctrl', 'Shift', 'H'],
description: 'Highlight Text',
icon: <Highlighter className="w-3.5 h-3.5 text-amber-500" />,
},
{
keys: ['Ctrl', 'Shift', '7'],
description: 'Bulleted List',
icon: <List className="w-3.5 h-3.5" />,
},
{
keys: ['Ctrl', 'Shift', '8'],
description: 'Numbered List',
icon: <ListOrdered className="w-3.5 h-3.5" />,
},
{
keys: ['Ctrl', 'Shift', '9'],
description: 'Task Checklist',
icon: <CheckSquare className="w-3.5 h-3.5 text-evernote-green" />,
},
{
keys: ['Ctrl', 'Shift', 'C'],
description: 'Code Block',
icon: <Code className="w-3.5 h-3.5 text-blue-500" />,
},
{
keys: ['Ctrl', 'Alt', '1-3'],
description: 'Heading 1, 2, 3',
},
{
keys: ['Ctrl', 'Z'],
description: 'Undo Edit',
},
{
keys: ['Ctrl', 'Y'],
description: 'Redo Edit',
},
];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-fade-in">
<div className="bg-white rounded-2xl shadow-2xl border border-gray-200 w-full max-w-2xl max-h-[85vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gradient-to-r from-emerald-50 to-teal-50">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-evernote-green/15 text-evernote-green flex items-center justify-center">
<Keyboard className="w-4 h-4" />
</div>
<div>
<h3 className="font-bold text-gray-900 text-sm">
Keyboard Shortcuts
</h3>
<p className="text-[11px] text-gray-500">
Evernote-compatible hotkeys for fast navigation and editing
</p>
</div>
</div>
<button
type="button"
onClick={onClose}
className="p-1 text-gray-400 hover:text-gray-700 rounded-lg hover:bg-white transition"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Content Body: 2 Columns */}
<div className="flex-1 overflow-y-auto p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Column 1: Navigation & Library */}
<div className="space-y-3">
<h4 className="text-xs font-bold uppercase tracking-wider text-gray-500 pb-1 border-b border-gray-100 flex items-center gap-1.5">
<span>Navigation & Library</span>
</h4>
<div className="space-y-2">
{navigationShortcuts.map((sc, i) => (
<div
key={i}
className="flex items-center justify-between gap-2 text-xs py-1"
>
<div className="flex items-center gap-2 text-gray-700 min-w-0">
{sc.icon && <span className="shrink-0">{sc.icon}</span>}
<span className="truncate">{sc.description}</span>
</div>
<div className="flex items-center gap-1 shrink-0">
{sc.keys.map((k, ki) => (
<kbd
key={ki}
className="px-2 py-0.5 text-[11px] font-semibold bg-gray-100 text-gray-800 border border-gray-300 rounded shadow-2xs font-mono"
>
{k}
</kbd>
))}
</div>
</div>
))}
</div>
</div>
{/* Column 2: Editor & Formatting */}
<div className="space-y-3">
<h4 className="text-xs font-bold uppercase tracking-wider text-gray-500 pb-1 border-b border-gray-100 flex items-center gap-1.5">
<span>Editor & Formatting</span>
</h4>
<div className="space-y-2">
{editorShortcuts.map((sc, i) => (
<div
key={i}
className="flex items-center justify-between gap-2 text-xs py-1"
>
<div className="flex items-center gap-2 text-gray-700 min-w-0">
{sc.icon && <span className="shrink-0">{sc.icon}</span>}
<span className="truncate">{sc.description}</span>
</div>
<div className="flex items-center gap-1 shrink-0">
{sc.keys.map((k, ki) => (
<kbd
key={ki}
className="px-2 py-0.5 text-[11px] font-semibold bg-gray-100 text-gray-800 border border-gray-300 rounded shadow-2xs font-mono"
>
{k}
</kbd>
))}
</div>
</div>
))}
</div>
</div>
</div>
{/* Footer */}
<div className="px-6 py-3 bg-gray-50 border-t border-gray-200 flex items-center justify-between text-xs text-gray-500">
<span>Tip: On Mac, use <kbd className="px-1.5 py-0.2 bg-gray-200 rounded font-mono text-[10px]">Cmd</kbd> instead of <kbd className="px-1.5 py-0.2 bg-gray-200 rounded font-mono text-[10px]">Ctrl</kbd></span>
<button
type="button"
onClick={onClose}
className="px-4 py-1.5 bg-white border border-gray-300 hover:bg-gray-100 text-gray-700 rounded-lg font-semibold text-xs transition"
>
Close (Esc)
</button>
</div>
</div>
</div>
);
}