A web app to help you design things, local, offline, on device. In your browser.
TypeScript
53.1%
JSON
45.6%
CSS
0.9%
Markdown
0.3%
JavaScript
0.1%
'use client';
import { useEffect, useCallback } from 'react';
import { useEditorStore } from '@/lib/store/editor-store';
import type { LayerData, LayerType } from '@/types';
interface ClipboardData {
layers: Array<{
type: LayerType;
name: string;
size: { width: number; height: number };
data: LayerData;
opacity: number;
rotation: number;
}>;
}
// Store clipboard data in memory (browser clipboard API can't handle complex objects)
let clipboardData: ClipboardData | null = null;
export function useKeyboardShortcuts() {
const {
selectedLayerIds,
layers,
deleteLayer,
duplicateLayer,
updateLayer,
addLayer,
selectLayer,
deselectAll,
undo,
redo,
canvas,
setZoom,
saveToHistory,
} = useEditorStore();
// Check if user is typing in an input
const checkInputFocus = useCallback(() => {
const activeElement = document.activeElement;
const tagName = activeElement?.tagName.toLowerCase();
return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
}, []);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
// Don't interfere with input fields
if (checkInputFocus()) return;
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const cmdKey = isMac ? e.metaKey : e.ctrlKey;
// Delete selected layers
if ((e.key === 'Delete' || e.key === 'Backspace') && !cmdKey) {
e.preventDefault();
selectedLayerIds.forEach((id) => deleteLayer(id));
return;
}
// Undo
if (cmdKey && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
undo();
return;
}
// Redo
if (cmdKey && e.key === 'z' && e.shiftKey) {
e.preventDefault();
redo();
return;
}
// Copy
if (cmdKey && e.key === 'c') {
e.preventDefault();
const selectedLayers = selectedLayerIds
.map((id) => layers.find((l) => l.id === id))
.filter(Boolean);
if (selectedLayers.length > 0) {
clipboardData = {
layers: selectedLayers.map((layer) => ({
type: layer!.type,
name: layer!.name,
size: { ...layer!.size },
data: JSON.parse(JSON.stringify(layer!.data)) as LayerData,
opacity: layer!.opacity,
rotation: layer!.rotation,
})),
};
}
return;
}
// Cut
if (cmdKey && e.key === 'x') {
e.preventDefault();
const selectedLayers = selectedLayerIds
.map((id) => layers.find((l) => l.id === id))
.filter(Boolean);
if (selectedLayers.length > 0) {
clipboardData = {
layers: selectedLayers.map((layer) => ({
type: layer!.type,
name: layer!.name,
size: { ...layer!.size },
data: JSON.parse(JSON.stringify(layer!.data)) as LayerData,
opacity: layer!.opacity,
rotation: layer!.rotation,
})),
};
selectedLayerIds.forEach((id) => deleteLayer(id));
}
return;
}
// Paste
if (cmdKey && e.key === 'v') {
e.preventDefault();
if (clipboardData && clipboardData.layers.length > 0) {
const newIds: string[] = [];
clipboardData.layers.forEach((layerData, index) => {
const newId = addLayer({
type: layerData.type,
name: `${layerData.name} (copy)`,
visible: true,
locked: false,
opacity: layerData.opacity,
position: {
x: canvas.width / 2 - layerData.size.width / 2 + index * 20,
y: canvas.height / 2 - layerData.size.height / 2 + index * 20,
},
size: { ...layerData.size },
rotation: layerData.rotation,
data: layerData.data,
});
newIds.push(newId);
});
// Select the pasted layers
deselectAll();
newIds.forEach((id, i) => selectLayer(id, i > 0));
saveToHistory('Pasted layers');
}
return;
}
// Duplicate
if (cmdKey && e.key === 'd') {
e.preventDefault();
selectedLayerIds.forEach((id) => duplicateLayer(id));
return;
}
// Select all
if (cmdKey && e.key === 'a') {
e.preventDefault();
deselectAll();
layers.forEach((layer, i) => selectLayer(layer.id, i > 0));
return;
}
// Escape - deselect all
if (e.key === 'Escape') {
e.preventDefault();
deselectAll();
return;
}
// Arrow keys - nudge
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
if (selectedLayerIds.length === 0) return;
e.preventDefault();
const nudgeAmount = e.shiftKey ? 10 : 1;
const delta = { x: 0, y: 0 };
switch (e.key) {
case 'ArrowUp':
delta.y = -nudgeAmount;
break;
case 'ArrowDown':
delta.y = nudgeAmount;
break;
case 'ArrowLeft':
delta.x = -nudgeAmount;
break;
case 'ArrowRight':
delta.x = nudgeAmount;
break;
}
selectedLayerIds.forEach((id) => {
const layer = layers.find((l) => l.id === id);
if (layer && !layer.locked) {
updateLayer(id, {
position: {
x: layer.position.x + delta.x,
y: layer.position.y + delta.y,
},
});
}
});
saveToHistory('Nudged layers');
return;
}
// Zoom shortcuts
if (cmdKey && (e.key === '=' || e.key === '+')) {
e.preventDefault();
setZoom(canvas.zoom + 0.1);
return;
}
if (cmdKey && e.key === '-') {
e.preventDefault();
setZoom(canvas.zoom - 0.1);
return;
}
// Zoom to 100%
if (cmdKey && e.key === '1') {
e.preventDefault();
setZoom(1);
return;
}
// Zoom to fit (approximate)
if (cmdKey && e.key === '0') {
e.preventDefault();
const container = document.querySelector('.flex-1.overflow-hidden');
if (container) {
const containerWidth = container.clientWidth - 100;
const containerHeight = container.clientHeight - 100;
const fitZoom = Math.min(
containerWidth / canvas.width,
containerHeight / canvas.height
);
setZoom(Math.min(fitZoom, 1));
}
return;
}
},
[
selectedLayerIds,
layers,
deleteLayer,
duplicateLayer,
updateLayer,
addLayer,
selectLayer,
deselectAll,
undo,
redo,
canvas,
setZoom,
saveToHistory,
checkInputFocus,
]
);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
}
About
A web app to help you design things, local, offline, on device. In your browser.
0 stars
0 forks