feat: layer effects, bucket fill, simple brush

This commit is contained in:
2026-05-21 00:02:12 +07:00
parent bcf4650c70
commit e7dd9420e7
47 changed files with 1675 additions and 1038 deletions
+43 -11
View File
@@ -2,14 +2,20 @@ import React from "react";
type UseEditorShortcutsOptions = {
onSave: () => void;
onUndo: () => void;
onRedo: () => void;
onCopy: () => void;
onCut: () => void;
onPaste: () => void;
onPaste: (e?: ClipboardEvent) => void;
onDelete: () => void;
};
function isEditableTarget(e: Event) {
return e.target instanceof HTMLElement && /^(input|textarea|select)$/i.test(e.target.tagName);
}
export function useEditorShortcuts(options: UseEditorShortcutsOptions) {
const { onSave, onCopy, onCut, onPaste, onDelete } = options;
const { onSave, onUndo, onRedo, onCopy, onCut, onPaste, onDelete } = options;
React.useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
@@ -19,31 +25,57 @@ export function useEditorShortcuts(options: UseEditorShortcutsOptions) {
onSave();
return;
}
if (e.key.toLowerCase() === "c") {
if (e.key.toLowerCase() === "z" && !e.shiftKey) {
e.preventDefault();
onCopy();
onUndo();
return;
}
if (e.key.toLowerCase() === "x") {
if (e.key.toLowerCase() === "z" && e.shiftKey) {
e.preventDefault();
onCut();
onRedo();
return;
}
if (e.key.toLowerCase() === "v") {
if (e.key.toLowerCase() === "y") {
e.preventDefault();
onPaste();
onRedo();
return;
}
}
if (e.key === "Delete" || e.key === "Backspace") {
if (!(e.target instanceof HTMLElement) || /^(input|textarea|select)$/i.test(e.target.tagName)) return;
if (isEditableTarget(e)) return;
e.preventDefault();
onDelete();
}
}
function handleCopy(e: ClipboardEvent) {
if (isEditableTarget(e)) return;
e.preventDefault();
onCopy();
}
function handleCut(e: ClipboardEvent) {
if (isEditableTarget(e)) return;
e.preventDefault();
onCut();
}
function handlePaste(e: ClipboardEvent) {
if (isEditableTarget(e)) return;
e.preventDefault();
onPaste(e);
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onCopy, onCut, onDelete, onPaste, onSave]);
window.addEventListener("copy", handleCopy);
window.addEventListener("cut", handleCut);
window.addEventListener("paste", handlePaste);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("copy", handleCopy);
window.removeEventListener("cut", handleCut);
window.removeEventListener("paste", handlePaste);
};
}, [onCopy, onCut, onDelete, onPaste, onRedo, onSave, onUndo]);
}