diff --git a/packages/ui/components/AnnotationToolbar.tsx b/packages/ui/components/AnnotationToolbar.tsx index 12c470d..7fbbb67 100644 --- a/packages/ui/components/AnnotationToolbar.tsx +++ b/packages/ui/components/AnnotationToolbar.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useRef } from "react"; -import { AnnotationType, type ImageAttachment } from "../types"; +import { AnnotationType } from "../types"; import { createPortal } from "react-dom"; -import { AttachmentsButton } from "./AttachmentsButton"; import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape"; type PositionMode = 'center-above' | 'top-right'; @@ -16,22 +15,19 @@ const isEditableElement = (node: EventTarget | Element | null): boolean => { interface AnnotationToolbarProps { element: HTMLElement; positionMode: PositionMode; - onAnnotate: (type: AnnotationType, text?: string, images?: ImageAttachment[]) => void; + onAnnotate: (type: AnnotationType) => void; onClose: () => void; + /** Called when user wants to write a comment (opens CommentPopover in parent) */ + onRequestComment?: (initialChar?: string) => void; /** Text to copy (for text selection, pass source.text) */ copyText?: string; - /** Close toolbar when element scrolls out of viewport (only in menu step) */ + /** Close toolbar when element scrolls out of viewport */ closeOnScrollOut?: boolean; /** Exit animation state */ isExiting?: boolean; /** Hover callbacks for code block behavior */ onMouseEnter?: () => void; onMouseLeave?: () => void; - onLockChange?: (locked: boolean) => void; - /** Start in input step instead of menu (for comment mode) */ - initialStep?: 'menu' | 'input'; - /** Pre-set annotation type when starting in input step */ - initialType?: AnnotationType; } export const AnnotationToolbar: React.FC = ({ @@ -39,26 +35,18 @@ export const AnnotationToolbar: React.FC = ({ positionMode, onAnnotate, onClose, + onRequestComment, copyText, closeOnScrollOut = false, isExiting = false, onMouseEnter, onMouseLeave, - onLockChange, - initialStep = 'menu', - initialType, }) => { - const [step, setStep] = useState<"menu" | "input">(initialStep); - const [activeType, setActiveType] = useState(initialType ?? null); - const [inputValue, setInputValue] = useState(""); - const [images, setImages] = useState([]); 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 let textToCopy = copyText; if (!textToCopy) { const codeEl = element.querySelector('code'); @@ -69,43 +57,17 @@ export const AnnotationToolbar: React.FC = ({ setTimeout(() => setCopied(false), 1500); }; - // Focus input when entering input step (including on mount with initialStep='input') + // Reset copied state when element changes useEffect(() => { - if (step === "input") { - // Use setTimeout to ensure DOM is fully ready (portals can have timing issues) - const timeoutId = setTimeout(() => { - const input = inputRef.current; - if (input) { - input.focus(); - // Move cursor to end (for type-to-comment with pre-populated char) - input.selectionStart = input.selectionEnd = input.value.length; - } - }, 0); - return () => clearTimeout(timeoutId); - } - }, [step, element]); // Also re-run when element changes (new selection) - - // Reset state when element changes - useEffect(() => { - setStep(initialStep); - setActiveType(initialType ?? null); - setInputValue(""); - setImages([]); setCopied(false); - }, [element, initialStep, initialType]); - - // Notify parent when locked (in input mode) - useEffect(() => { - onLockChange?.(step === "input"); - }, [step, onLockChange]); + }, [element]); // Update position on scroll/resize useEffect(() => { const updatePosition = () => { const rect = element.getBoundingClientRect(); - // Close if scrolled out of viewport (only in menu step if enabled) - if (closeOnScrollOut && step === "menu" && (rect.bottom < 0 || rect.top > window.innerHeight)) { + if (closeOnScrollOut && (rect.bottom < 0 || rect.top > window.innerHeight)) { onClose(); return; } @@ -131,34 +93,24 @@ export const AnnotationToolbar: React.FC = ({ window.removeEventListener("scroll", updatePosition, true); window.removeEventListener("resize", updatePosition); }; - }, [element, positionMode, closeOnScrollOut, step, onClose]); + }, [element, positionMode, closeOnScrollOut, onClose]); - // Type-to-comment: start typing in menu step → auto-transition to comment input + // Type-to-comment: typing opens CommentPopover via parent useEffect(() => { - if (step !== "menu") return; - const handleKeyDown = (e: KeyboardEvent) => { - // Protect global comment and any editable field focus from type-to-comment capture. if (e.isComposing) return; if (isEditableElement(e.target) || isEditableElement(document.activeElement)) return; - // Escape closes the toolbar if (e.key === "Escape") { onClose(); return; } - // Ignore if modifier keys are held (except shift for capitals) if (e.ctrlKey || e.metaKey || e.altKey) return; - // Ignore special keys if (e.key === "Tab" || e.key === "Enter") return; - // Only trigger on printable characters (single char keys) if (e.key.length !== 1) return; - // Transition to comment mode with the typed character - setActiveType(AnnotationType.COMMENT); - setInputValue(e.key); - setStep("input"); + onRequestComment?.(e.key); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [step, onClose]); + }, [onClose, onRequestComment]); useDismissOnOutsideAndEscape({ enabled: true, @@ -172,15 +124,7 @@ export const AnnotationToolbar: React.FC = ({ if (type === AnnotationType.DELETION) { onAnnotate(type); } else { - setActiveType(type); - setStep("input"); - } - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (activeType && (inputValue.trim() || images.length > 0)) { - onAnnotate(activeType, inputValue || undefined, images.length > 0 ? images : undefined); + onRequestComment?.(); } }; @@ -216,77 +160,34 @@ export const AnnotationToolbar: React.FC = ({ to { opacity: 0; transform: translateY(8px)${translateX}; } } `} - {step === "menu" ? ( -
- : } - label={copied ? "Copied!" : "Copy"} - className={copied ? "text-success" : "text-muted-foreground hover:bg-muted hover:text-foreground"} - /> -
- handleTypeSelect(AnnotationType.DELETION)} - icon={} - label="Delete" - className="text-destructive hover:bg-destructive/10" - /> - handleTypeSelect(AnnotationType.COMMENT)} - icon={} - label="Comment" - className="text-accent hover:bg-accent/10" - /> -
- } - label="Cancel" - className="text-muted-foreground hover:bg-muted" - /> -
- ) : ( -
-