diff --git a/packages/ui/src/components/comments/InlineCommentCard.tsx b/packages/ui/src/components/comments/InlineCommentCard.tsx new file mode 100644 index 00000000..4a892abc --- /dev/null +++ b/packages/ui/src/components/comments/InlineCommentCard.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import { RiMoreLine, RiDeleteBinLine, RiEditLine, RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'; +import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore'; +import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; + +interface InlineCommentCardProps { + draft: InlineCommentDraft; + onEdit: () => void; + onDelete: () => void; + className?: string; +} + +export function InlineCommentCard({ + draft, + onEdit, + onDelete, + className, +}: InlineCommentCardProps) { + const themeContext = useOptionalThemeSystem(); + const currentTheme = themeContext?.currentTheme; + const [isOpen, setIsOpen] = useState(false); + + // Check if content is long enough to warrant collapsing (rough estimate) + // In a real app we might measure line height, but length check is a good proxy for now + const isLongContent = draft.text.length > 150 || draft.text.split('\n').length > 3; + + return ( +
+
+
+
+ + {draft.fileLabel} + + + Lines {draft.startLine}-{draft.endLine} + {draft.side && ({draft.side})} +
+ + +
+ {draft.text} +
+ + {isLongContent && ( + + + + )} + + + {/* Used for animation purposes if we want to animate height */} + +
+
+ + + + + + + + + Edit comment + + + + Delete comment + + + +
+
+ ); +} diff --git a/packages/ui/src/components/comments/InlineCommentInput.tsx b/packages/ui/src/components/comments/InlineCommentInput.tsx new file mode 100644 index 00000000..995f011e --- /dev/null +++ b/packages/ui/src/components/comments/InlineCommentInput.tsx @@ -0,0 +1,138 @@ +import React, { useRef, useEffect } from 'react'; +import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; +import { useDeviceInfo } from '@/lib/device'; + +export interface InlineCommentInputProps { + initialText?: string; + onSave: (text: string, range?: { start: number; end: number; side?: 'additions' | 'deletions' }) => void; + onCancel: () => void; + fileLabel?: string; + lineRange?: { start: number; end: number; side?: 'additions' | 'deletions' }; + isEditing?: boolean; + className?: string; +} + +export function InlineCommentInput({ + initialText = '', + onSave, + onCancel, + fileLabel, + lineRange, + isEditing = false, + className, +}: InlineCommentInputProps) { + const themeContext = useOptionalThemeSystem(); + const currentTheme = themeContext?.currentTheme; + const { isMobile } = useDeviceInfo(); + const [text, setText] = React.useState(initialText); + const textareaRef = useRef(null); + + // Stable range snapshot to prevent race with selection clearing + const stableRangeRef = useRef(lineRange); + useEffect(() => { + if (lineRange) { + stableRangeRef.current = lineRange; + } + }, [lineRange]); + + // Focus on mount (desktop only) or when becoming visible + useEffect(() => { + if (!isMobile && textareaRef.current) { + textareaRef.current.focus(); + // Move cursor to end + const len = textareaRef.current.value.length; + textareaRef.current.setSelectionRange(len, len); + } else if (isMobile && textareaRef.current) { + // Scroll into view on mobile + textareaRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, [isMobile]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { + e.preventDefault(); + if (text.trim()) { + onSave(text, stableRangeRef.current); + } + } else if (e.key === 'Escape') { + e.preventDefault(); + onCancel(); + } + }; + + const handleSaveClick = (e: React.MouseEvent | React.TouchEvent | React.PointerEvent) => { + // Stop propagation to prevent parent selection clearing before save + e.stopPropagation(); + if (text.trim()) { + onSave(text, stableRangeRef.current); + } + }; + + return ( +
e.stopPropagation()} + onTouchStart={(e) => e.stopPropagation()} + > +
+ {(fileLabel || lineRange) && ( +
+ {fileLabel && {fileLabel}} + {fileLabel && lineRange && } + {lineRange && Lines {lineRange.start}-{lineRange.end}} +
+ )} + +