From ef4e2d94a0cd9e192070664e54b83861830a383e Mon Sep 17 00:00:00 2001 From: Jiayuan Date: Tue, 31 Mar 2026 14:33:09 +0800 Subject: [PATCH] feat(issues): add collapsible toggle for comment replies Wrap the replies section in a Collapsible component so users can collapse/expand replies on a comment thread. The parent comment and reply input remain always visible. A chevron trigger shows the reply count (e.g. "3 replies") and rotates on open. Default state is expanded to preserve existing behavior. --- .../issues/components/comment-card.tsx | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/apps/web/features/issues/components/comment-card.tsx b/apps/web/features/issues/components/comment-card.tsx index b72baec2..948e0f86 100644 --- a/apps/web/features/issues/components/comment-card.tsx +++ b/apps/web/features/issues/components/comment-card.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Copy, MoreHorizontal, Pencil, Trash2 } from "lucide-react"; +import { Copy, MoreHorizontal, Pencil, Trash2, ChevronRight } from "lucide-react"; import { toast } from "sonner"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -13,9 +13,11 @@ import { DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; +import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; import { ActorAvatar } from "@/components/common/actor-avatar"; import { ReactionBar } from "@/components/common/reaction-bar"; import { Markdown } from "@/components/markdown"; +import { cn } from "@/lib/utils"; import { useActorName } from "@/features/workspace"; import { timeAgo } from "@/shared/utils"; import { ReplyInput } from "./reply-input"; @@ -189,6 +191,8 @@ function CommentCard({ onDelete, onToggleReaction, }: CommentCardProps) { + const [repliesOpen, setRepliesOpen] = useState(true); + // Collect all nested replies recursively into a flat list const allNestedReplies: TimelineEntry[] = []; const collectReplies = (parentId: string) => { @@ -200,6 +204,8 @@ function CommentCard({ }; collectReplies(entry.id); + const replyCount = allNestedReplies.length; + return ( {/* Parent comment */} @@ -213,18 +219,32 @@ function CommentCard({ /> - {/* Replies — flat, separated by border */} - {allNestedReplies.map((reply) => ( -
- -
- ))} + {/* Replies — collapsible when there are replies */} + {replyCount > 0 && ( + +
+ + + + {replyCount} {replyCount === 1 ? "reply" : "replies"} + + +
+ + {allNestedReplies.map((reply) => ( +
+ +
+ ))} +
+
+ )} {/* Reply input — always visible at bottom */}