"use client"; import { useEffect, useState, type KeyboardEvent, type MouseEvent, } from "react"; import { getRepliesForParent } from "../core/comment-threads"; import type { CommentAnnotation } from "../core/types"; import { Button } from "@prototype/components/ui/button"; import { useCommentStore } from "../react/CommentProvider"; import { CommentAuthorAvatar } from "./comment-author-avatar"; import threadStyles from "./comment-thread.module.scss"; import { cn } from "@prototype/lib/utils"; const THREAD_CANCEL_BUTTON_CLASS = "h-7 border-[var(--tool-chrome-border)] bg-transparent px-2.5 text-xs text-[var(--tool-chrome-text-muted)] shadow-none hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-text)]"; const THREAD_PRIMARY_BUTTON_CLASS = "h-7 px-2.5 text-xs"; function PencilIcon() { return ( ); } function TrashIcon() { return ( ); } type ThreadReplyRowProps = { reply: CommentAnnotation; canModify: boolean; dark?: boolean; }; function ThreadReplyRow({ reply, canModify, dark = false, }: ThreadReplyRowProps) { const { updateAnnotation, deleteAnnotation } = useCommentStore(); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(reply.comment); const [actionsVisible, setActionsVisible] = useState(false); useEffect(() => { setDraft(reply.comment); }, [reply.comment]); function handleSave() { const text = draft.trim(); if (!text) return; updateAnnotation(reply.id, text); setEditing(false); } function handleKeyDown(event: KeyboardEvent) { event.stopPropagation(); if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); handleSave(); } if (event.key === "Escape") { setDraft(reply.comment); setEditing(false); } } return ( setActionsVisible(true)} onMouseLeave={() => setActionsVisible(false)} > {editing ? ( setDraft(event.target.value)} onKeyDown={handleKeyDown} rows={2} className={cn(threadStyles.textarea, dark && threadStyles.textareaDark)} aria-label="Edit reply" onClick={(event) => event.stopPropagation()} /> { setDraft(reply.comment); setEditing(false); }} > Cancel Save ) : ( {reply.comment} )} {canModify && !editing && ( event.stopPropagation()} > { setDraft(reply.comment); setEditing(true); }} > deleteAnnotation(reply.id)} > )} ); } type CommentThreadProps = { rootAnnotation: CommentAnnotation; readOnly?: boolean; /** Attached below the annotation popup on the canvas overlay */ variant?: "inline" | "overlay"; dark?: boolean; onComposerClick?: (event: MouseEvent) => void; }; export function CommentThread({ rootAnnotation, readOnly = false, variant = "inline", dark = false, onComposerClick, }: CommentThreadProps) { const { annotations, addReply } = useCommentStore(); const [showComposer, setShowComposer] = useState(false); const [draft, setDraft] = useState(""); const replies = getRepliesForParent(annotations, rootAnnotation.id); const isResolved = rootAnnotation.status === "resolved"; const canModify = !readOnly && !isResolved; function handleSubmit() { const text = draft.trim(); if (!text) return; addReply(rootAnnotation.id, text); setDraft(""); setShowComposer(false); } function handleKeyDown(event: KeyboardEvent) { event.stopPropagation(); if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); handleSubmit(); } if (event.key === "Escape") { setDraft(""); setShowComposer(false); } } if (replies.length === 0 && !canModify) { return null; } const content = ( event.stopPropagation()} > {replies.length > 0 && ( {replies.map((reply) => ( ))} )} {canModify && ( <> {!showComposer ? ( setShowComposer(true)} > Reply ) : ( setDraft(event.target.value)} onKeyDown={handleKeyDown} placeholder="Write a reply…" rows={2} className={cn(threadStyles.textarea, dark && threadStyles.textareaDark)} aria-label="Reply to comment" /> { setDraft(""); setShowComposer(false); }} > Cancel Reply )} > )} ); if (variant === "overlay") { return ( {content} ); } return content; }
{reply.comment}