diff --git a/bun.lock b/bun.lock index 3fff9f83..b0842c0e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "plannotator", "dependencies": { "@pierre/diffs": "^1.0.4", + "diff": "^8.0.3", }, "devDependencies": { "happy-dom": "^20.5.0", @@ -52,7 +54,7 @@ }, "apps/opencode-plugin": { "name": "@plannotator/opencode", - "version": "0.8.5", + "version": "0.9.2", "dependencies": { "@opencode-ai/plugin": "^1.1.10", }, @@ -65,7 +67,7 @@ }, "apps/pi-extension": { "name": "@plannotator/pi-extension", - "version": "0.8.5", + "version": "0.9.2", "peerDependencies": { "@mariozechner/pi-coding-agent": ">=0.53.0", }, @@ -132,7 +134,7 @@ }, "packages/server": { "name": "@plannotator/server", - "version": "0.8.5", + "version": "0.9.2", "peerDependencies": { "bun": ">=1.0.0", }, diff --git a/package.json b/package.json index 692114e8..94b2b26c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "test": "bun test" }, "dependencies": { - "@pierre/diffs": "^1.0.4" + "@pierre/diffs": "^1.0.4", + "diff": "^8.0.3" }, "devDependencies": { "happy-dom": "^20.5.0" diff --git a/packages/review-editor/components/DiffViewer.tsx b/packages/review-editor/components/DiffViewer.tsx index 03e70c2b..bb255ecc 100644 --- a/packages/review-editor/components/DiffViewer.tsx +++ b/packages/review-editor/components/DiffViewer.tsx @@ -1,6 +1,7 @@ import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'; import { PatchDiff } from '@pierre/diffs/react'; import { CodeAnnotation, CodeAnnotationType, SelectedLineRange, DiffAnnotationMetadata } from '@plannotator/ui/types'; +import { useDismissOnOutsideAndEscape } from '@plannotator/ui/hooks/useDismissOnOutsideAndEscape'; import { useTheme } from '@plannotator/ui/components/ThemeProvider'; interface DiffViewerProps { @@ -39,6 +40,7 @@ export const DiffViewer: React.FC = ({ }) => { const { theme } = useTheme(); const containerRef = useRef(null); + const toolbarRef = useRef(null); const [toolbarState, setToolbarState] = useState(null); const [commentText, setCommentText] = useState(''); const [suggestedCode, setSuggestedCode] = useState(''); @@ -140,6 +142,12 @@ export const DiffViewer: React.FC = ({ onLineSelection(null); }, [onLineSelection]); + useDismissOnOutsideAndEscape({ + enabled: !!toolbarState, + ref: toolbarRef, + onDismiss: handleCancel, + }); + // Render annotation in diff - returns React element const renderAnnotation = useCallback((annotation: { side: string; lineNumber: number; metadata?: DiffAnnotationMetadata }) => { if (!annotation.metadata) return null; @@ -294,6 +302,7 @@ export const DiffViewer: React.FC = ({ {/* Annotation toolbar - single-step comment input */} {toolbarState && (
= ({ const [position, setPosition] = useState<{ top: number; left?: number; right?: number } | null>(null); const [copied, setCopied] = useState(false); const inputRef = useRef(null); + const toolbarRef = useRef(null); const handleCopy = async () => { // Use provided copyText, or fall back to code element / element text @@ -158,6 +160,12 @@ export const AnnotationToolbar: React.FC = ({ return () => window.removeEventListener("keydown", handleKeyDown); }, [step, onClose]); + useDismissOnOutsideAndEscape({ + enabled: true, + ref: toolbarRef, + onDismiss: onClose, + }); + if (!position) return null; const handleTypeSelect = (type: AnnotationType) => { @@ -191,6 +199,7 @@ export const AnnotationToolbar: React.FC = ({ return createPortal(
e.stopPropagation()} diff --git a/packages/ui/hooks/useDismissOnOutsideAndEscape.ts b/packages/ui/hooks/useDismissOnOutsideAndEscape.ts new file mode 100644 index 00000000..83ca9967 --- /dev/null +++ b/packages/ui/hooks/useDismissOnOutsideAndEscape.ts @@ -0,0 +1,38 @@ +import { useEffect } from "react"; + +export function useDismissOnOutsideAndEscape({ + enabled, + ref, + onDismiss, +}: { + enabled: boolean; + ref: React.RefObject; + onDismiss: () => void; +}) { + useEffect(() => { + if (!enabled) return; + + const handlePointerDown = (event: PointerEvent) => { + const target = event.target as Node | null; + if (!target) return; + if (ref.current && ref.current.contains(target)) { + return; + } + onDismiss(); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onDismiss(); + } + }; + + document.addEventListener("pointerdown", handlePointerDown, true); + window.addEventListener("keydown", handleKeyDown); + + return () => { + document.removeEventListener("pointerdown", handlePointerDown, true); + window.removeEventListener("keydown", handleKeyDown); + }; + }, [enabled, ref, onDismiss]); +}