// lib/shortcuts.ts
interface Shortcut {
id: string;
label: string;
category: 'file' | 'edit' | 'view'
| 'tools' | 'filters';
defaultKey: string;
// e.g. "mod+s", "shift+r", "["
action: () => void;
}
const isMac = navigator.platform
.includes('Mac');
function parseCombo(combo: string) {
const parts = combo.toLowerCase()
.split('+');
return {
mod: parts.includes('mod'),
shift: parts.includes('shift'),
alt: parts.includes('alt'),
key: parts[parts.length - 1],
};
}
function matchesEvent(
combo: string,
e: KeyboardEvent
): boolean {
const c = parseCombo(combo);
const modPressed = isMac
? e.metaKey : e.ctrlKey;
return (
c.mod === modPressed &&
c.shift === e.shiftKey &&
c.alt === e.altKey &&
c.key === e.key.toLowerCase()
);
}
// lib/defaultShortcuts.ts
const defaults: Shortcut[] = [
// File
{ id: 'save', label: 'Save',
category: 'file',
defaultKey: 'mod+s',
action: () => saveProject() },
{ id: 'export', label: 'Export',
category: 'file',
defaultKey: 'mod+shift+e',
action: () => openExportDialog() },
{ id: 'open', label: 'Open',
category: 'file',
defaultKey: 'mod+o',
action: () => openFile() },
// Edit
{ id: 'undo', label: 'Undo',
category: 'edit',
defaultKey: 'mod+z',
action: () => undo() },
{ id: 'redo', label: 'Redo',
category: 'edit',
defaultKey: 'mod+shift+z',
action: () => redo() },
{ id: 'copy', label: 'Copy',
category: 'edit',
defaultKey: 'mod+c',
action: () => copySelection() },
// Tools
{ id: 'select', label: 'Select',
category: 'tools',
defaultKey: 'v',
action: () => setTool('select') },
{ id: 'crop', label: 'Crop',
category: 'tools',
defaultKey: 'c',
action: () => setTool('crop') },
{ id: 'eyedropper',
label: 'Eyedropper',
category: 'tools',
defaultKey: 'i',
action: () =>
setTool('eyedropper') },
// View
{ id: 'zoomIn', label: 'Zoom In',
category: 'view',
defaultKey: 'mod+=',
action: () => zoomIn() },
{ id: 'zoomOut', label: 'Zoom Out',
category: 'view',
defaultKey: 'mod+-',
action: () => zoomOut() },
{ id: 'fitScreen',
label: 'Fit to Screen',
category: 'view',
defaultKey: 'mod+0',
action: () => fitToScreen() },
];
// hooks/useShortcuts.ts
function useShortcuts(
shortcuts: Shortcut[]
) {
const customBindings =
usePrefsStore(
s => s.customShortcuts);
useEffect(() => {
function handleKeyDown(
e: KeyboardEvent
) {
// Don't trigger in input fields
const tag =
(e.target as HTMLElement)
.tagName;
if (tag === 'INPUT'
|| tag === 'TEXTAREA')
return;
for (const shortcut
of shortcuts) {
const key =
customBindings[shortcut.id]
?? shortcut.defaultKey;
if (matchesEvent(key, e)) {
e.preventDefault();
shortcut.action();
return;
}
}
}
window.addEventListener(
'keydown', handleKeyDown);
return () =>
window.removeEventListener(
'keydown', handleKeyDown);
}, [shortcuts, customBindings]);
}
function ShortcutOverlay({
isOpen, onClose
}: { isOpen: boolean;
onClose: () => void }) {
if (!isOpen) return null;
const grouped =
groupBy(shortcuts, 'category');
const labels: Record<string,string>
= {
file: 'File', edit: 'Edit',
view: 'View', tools: 'Tools',
filters: 'Filters',
};
return (
<div className="shortcut-overlay"
onClick={onClose}>
<div className="shortcut-grid">
{Object.entries(grouped)
.map(([cat, items]) => (
<div key={cat}>
<h3>{labels[cat]}</h3>
{items.map(s => (
<div key={s.id}
className="shortcut-row">
<span>{s.label}</span>
<kbd>
{formatKey(s.defaultKey)}
</kbd>
</div>
))}
</div>
))}
</div>
</div>
);
}
function formatKey(combo: string) {
return combo
.replace('mod',
isMac ? '⌘' : 'Ctrl')
.replace('shift', '⇧')
.replace('alt',
isMac ? '⌥' : 'Alt')
.replace('+', ' + ')
.toUpperCase();
// "mod+shift+e" → "⌘ + ⇧ + E"
}
git switch -c feature/PIXELCRAFT-098-shortcuts
git add src/lib/shortcuts.ts src/hooks/ src/components/
git commit -m "Add keyboard shortcut system + cheat sheet (PIXELCRAFT-098)"
git push origin feature/PIXELCRAFT-098-shortcuts
# PR → Review → Merge → Close ticket ✅
Keyboard shortcuts are a study in human-computer interaction.
Good shortcuts follow Fitts's Law inverted: the fastest interaction has zero distance — your fingers are already on the keyboard.