add redo and undo

This commit is contained in:
2026-03-08 21:15:40 +01:00
parent 53106cd051
commit fe4ac78c72
4 changed files with 364 additions and 24 deletions

View File

@@ -0,0 +1,97 @@
import React, { useEffect } from 'react'
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarTrigger,
} from '@/components/ui/menubar'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { Download, FolderOpen, Redo2, Undo2 } from 'lucide-react'
type AppMenubarProps = {
onImport: () => void
onExport: () => void
undo: () => void
redo: () => void
canUndo: boolean
canRedo: boolean
}
const UNDO_KEYS = { key: 'z', shiftKey: false }
const REDO_KEYS = { key: 'z', shiftKey: true }
function matchKey(ev: KeyboardEvent, want: { key: string; shiftKey: boolean }) {
const mod = ev.ctrlKey || ev.metaKey
return (
ev.key.toLowerCase() === want.key &&
!!mod &&
!!ev.shiftKey === want.shiftKey
)
}
export function AppMenubar({ onImport, onExport, undo, redo, canUndo, canRedo }: AppMenubarProps) {
useEffect(() => {
const onKeyDown = (ev: KeyboardEvent) => {
if (matchKey(ev, UNDO_KEYS)) {
if (canUndo) {
ev.preventDefault()
ev.stopPropagation()
undo()
}
return
}
if (matchKey(ev, REDO_KEYS)) {
if (canRedo) {
ev.preventDefault()
ev.stopPropagation()
redo()
}
}
}
// Capture phase so we run before CodeMirror/inputs; then graph undo applies even when focus is in an editor
window.addEventListener('keydown', onKeyDown, true)
return () => window.removeEventListener('keydown', onKeyDown, true)
}, [undo, redo, canUndo, canRedo])
return (
<Menubar className="shrink-0 rounded-none border-x-0 border-t-0 border-b-0">
<MenubarMenu>
<MenubarTrigger className="font-medium">Project</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={onImport} className="gap-2">
<FolderOpen className="h-4 w-4" />
Import
</MenubarItem>
<MenubarItem onClick={onExport} className="gap-2">
<Download className="h-4 w-4" />
Export
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger className="font-medium">Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={undo} disabled={!canUndo} className="gap-2">
<Undo2 className="h-4 w-4" />
Undo
<span className="ml-auto pl-4">
<KbdGroup>
<Kbd> + Z</Kbd>
</KbdGroup>
</span>
</MenubarItem>
<MenubarItem onClick={redo} disabled={!canRedo} className="gap-2">
<Redo2 className="h-4 w-4" />
Redo
<span className="ml-auto pl-4">
<KbdGroup>
<Kbd> + + Z</Kbd>
</KbdGroup>
</span>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
)
}

28
src/components/ui/kbd.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { cn } from "@/lib/utils"
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="kbd"
className={cn(
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium",
"[&_svg:not([class*='size-'])]:size-3",
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
className
)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
export { Kbd, KbdGroup }