diff --git a/apps/docs/app/diff-examples/Annotations.tsx b/apps/docs/app/diff-examples/Annotations.tsx index 2cd210a36..d56431aac 100644 --- a/apps/docs/app/diff-examples/Annotations.tsx +++ b/apps/docs/app/diff-examples/Annotations.tsx @@ -2,8 +2,14 @@ import { FileDiff } from '@/components/diff-ui/FileDiff'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import type { FileContents } from '@pierre/precision-diffs'; -import { CornerDownRight } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import type { + AnnotationSide, + DiffLineAnnotation, + FileContents, +} from '@pierre/precision-diffs'; +import { CornerDownRight, Plus } from 'lucide-react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { FeatureHeader } from './FeatureHeader'; @@ -42,24 +48,249 @@ export default function Home() { `, }; +interface AnnotationMetadata { + key: string; + isThread: boolean; +} + export function Annotations() { + const [annotations, setAnnotations] = useState< + DiffLineAnnotation[] + >([ + { + side: 'additions', + lineNumber: 8, + metadata: { + key: 'additions-8', + isThread: true, + }, + }, + ]); + const [buttonPosition, setButtonPosition] = useState<{ + top: number; + left: number; + } | null>(null); + const [hoveredLine, setHoveredLine] = useState<{ + side: AnnotationSide; + lineNumber: number; + } | null>(null); + const containerRef = useRef(null); + + const handleLineEnter = useCallback( + (props: { + lineElement: HTMLElement; + annotationSide: AnnotationSide; + lineNumber: number; + }) => { + const lineElement = props.lineElement; + const container = containerRef.current; + + if (container == null) return; + + const { annotationSide, lineNumber } = props; + + // Don't show button if there's already an annotation on this line + const hasAnnotation = annotations.some( + (ann) => ann.side === annotationSide && ann.lineNumber === lineNumber + ); + + if (hasAnnotation) { + setButtonPosition(null); + setHoveredLine(null); + return; + } + + // Get the position of the line element relative to the container + const containerRect = container.getBoundingClientRect(); + const lineRect = lineElement.getBoundingClientRect(); + + setButtonPosition({ + top: lineRect.top - containerRect.top + lineRect.height / 2, + left: 16, // Fixed position from left edge + }); + + setHoveredLine({ side: annotationSide, lineNumber }); + }, + [annotations] + ); + + const handleLineLeave = useCallback(() => { + setButtonPosition(null); + setHoveredLine(null); + }, []); + + const handleContainerMouseLeave = useCallback(() => { + setButtonPosition(null); + setHoveredLine(null); + }, []); + + const handleAddComment = useCallback(() => { + if (hoveredLine != null) { + setAnnotations((prev) => [ + ...prev, + { + side: hoveredLine.side, + lineNumber: hoveredLine.lineNumber, + metadata: { + key: `${hoveredLine.side}-${hoveredLine.lineNumber}`, + isThread: false, // Start as a form, not a thread yet + }, + }, + ]); + setButtonPosition(null); + setHoveredLine(null); + } + }, [hoveredLine]); + + const handleSubmitComment = useCallback( + (side: AnnotationSide, lineNumber: number) => { + // TODO: Implement + console.log('submit comment', side, lineNumber); + }, + [] + ); + + const handleCancelComment = useCallback( + (side: AnnotationSide, lineNumber: number) => { + setAnnotations((prev) => + prev.filter( + (ann) => !(ann.side === side && ann.lineNumber === lineNumber) + ) + ); + }, + [] + ); + return (
- } - /> +
+ {buttonPosition != null && ( + + )} + + annotation.metadata.isThread ? ( + + ) : ( + + ) + } + /> +
+
+ ); +} + +function CommentForm({ + side, + lineNumber, + onSubmit, + onCancel, +}: { + side: AnnotationSide; + lineNumber: number; + onSubmit: (side: AnnotationSide, lineNumber: number) => void; + onCancel: (side: AnnotationSide, lineNumber: number) => void; +}) { + const textareaRef = useRef(null); + + useEffect(() => { + setTimeout(() => { + textareaRef.current?.focus(); + }, 0); + }, []); + + const handleSubmit = useCallback(() => { + onSubmit(side, lineNumber); + }, [side, lineNumber, onSubmit]); + + const handleCancel = useCallback(() => { + onCancel(side, lineNumber); + }, [side, lineNumber, onCancel]); + + return ( +
+
+
+
+ + + Y + +
+
+