refactor: enhance comment components with improved styles and functionality

- Updated placeholder text in CommentComposer for clarity.
- Implemented auto-resizing for the textarea in CommentComposer.
- Adjusted styles for CommentPanel and CommentSheet for better layout and usability.
- Enhanced CommentActions button visibility on hover for improved interaction.
- Refined comment item display logic to simplify user experience.
This commit is contained in:
Anish Sarkar 2026-01-20 19:49:34 +05:30
parent 26ca751a9d
commit 3fb83e4c3a
8 changed files with 84 additions and 75 deletions

View file

@ -191,7 +191,7 @@ export const AssistantMessage: FC = () => {
{/* Mobile & Medium screen comment trigger - shown below lg breakpoint */}
{showCommentTrigger && !isDesktop && (
<div className="mt-2 flex justify-start">
<div className="ml-2 mt-1 flex justify-start">
<button
type="button"
onClick={handleCommentTriggerClick}
@ -234,7 +234,7 @@ const AssistantActionBar: FC = () => {
hideWhenRunning
autohide="not-last"
autohideFloat="single-branch"
className="aui-assistant-action-bar-root -ml-1 col-start-3 row-start-2 flex gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm"
className="aui-assistant-action-bar-root -ml-1 col-start-3 row-start-2 flex gap-1 text-muted-foreground md:data-floating:absolute md:data-floating:rounded-md md:data-floating:border md:data-floating:bg-background md:data-floating:p-1 md:data-floating:shadow-sm [&>button]:opacity-100 md:[&>button]:opacity-[var(--aui-button-opacity,1)]"
>
<ActionBarPrimitive.Copy asChild>
<TooltipIconButton tooltip="Copy">

View file

@ -44,7 +44,6 @@ function findMentionTrigger(
return { isActive: false, query: "", startIndex: 0 };
}
const fullMatch = mentionMatch[0];
const query = mentionMatch[1];
const atIndex = cursorPos - query.length - 1;
@ -80,7 +79,7 @@ function findMentionTrigger(
export function CommentComposer({
members,
membersLoading = false,
placeholder = "Write a comment...",
placeholder = "Comment or @mention",
submitLabel = "Send",
isSubmitting = false,
onSubmit,
@ -145,6 +144,13 @@ export function CommentComposer({
const cursorPos = e.target.selectionStart;
setDisplayContent(value);
// Auto-resize textarea on content change
requestAnimationFrame(() => {
const textarea = e.target;
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
});
const triggerResult = findMentionTrigger(value, cursorPos, insertedMentions);
if (triggerResult.isActive) {
@ -208,9 +214,9 @@ export function CommentComposer({
const mentionPattern = /@([^\s@]+(?:\s+[^\s@]+)*?)(?=\s|$|[.,!?;:]|@)/g;
const foundMentions: InsertedMention[] = [];
let match: RegExpExecArray | null;
const matches = initialValue.matchAll(mentionPattern);
while ((match = mentionPattern.exec(initialValue)) !== null) {
for (const match of matches) {
const displayName = match[1];
const member = members.find(
(m) => m.displayName === displayName || m.email.split("@")[0] === displayName
@ -237,6 +243,19 @@ export function CommentComposer({
const canSubmit = displayContent.trim().length > 0 && !isSubmitting;
// Auto-resize textarea
const adjustTextareaHeight = useCallback(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}
}, []);
useEffect(() => {
adjustTextareaHeight();
}, [adjustTextareaHeight]);
return (
<div className="flex flex-col gap-2">
<Popover
@ -251,7 +270,8 @@ export function CommentComposer({
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className="min-h-[80px] resize-none"
className="min-h-[40px] max-h-[200px] resize-none overflow-y-auto scrollbar-thin"
rows={1}
disabled={isSubmitting}
/>
</PopoverAnchor>

View file

@ -21,7 +21,7 @@ export function CommentActions({ canEdit, canDelete, onEdit, onDelete }: Comment
<Button
variant="ghost"
size="icon"
className="size-7 opacity-0 group-hover:opacity-100 transition-opacity"
className="size-7 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity"
>
<MoreHorizontal className="size-4" />
</Button>

View file

@ -1,8 +1,6 @@
"use client";
import { useAtom } from "jotai";
import { MessageSquare } from "lucide-react";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@ -115,12 +113,7 @@ export function CommentItem({
members = [],
membersLoading = false,
}: CommentItemProps) {
const [{ data: currentUser }] = useAtom(currentUserAtom);
const isCurrentUser = currentUser?.id === comment.author?.id;
const displayName = isCurrentUser
? "Me"
: comment.author?.displayName || comment.author?.email.split("@")[0] || "Unknown";
const displayName = comment.author?.displayName || comment.author?.email.split("@")[0] || "Unknown";
const email = comment.author?.email || "";
const handleEditSubmit = (content: string) => {

View file

@ -1,13 +1,25 @@
"use client";
import { MessageSquarePlus } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useAtom } from "jotai";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
import { CommentComposer } from "../comment-composer/comment-composer";
import { CommentThread } from "../comment-thread/comment-thread";
import type { CommentPanelProps } from "./types";
function getInitials(name: string | null | undefined, email: string): string {
if (name) {
return name
.split(" ")
.map((part) => part[0])
.join("")
.toUpperCase()
.slice(0, 2);
}
return email[0].toUpperCase();
}
export function CommentPanel({
threads,
members,
@ -21,15 +33,10 @@ export function CommentPanel({
maxHeight,
variant = "desktop",
}: CommentPanelProps) {
const [isComposerOpen, setIsComposerOpen] = useState(false);
const [{ data: currentUser }] = useAtom(currentUserAtom);
const handleCommentSubmit = (content: string) => {
onCreateComment(content);
setIsComposerOpen(false);
};
const handleComposerCancel = () => {
setIsComposerOpen(false);
};
const isMobile = variant === "mobile";
@ -51,7 +58,6 @@ export function CommentPanel({
}
const hasThreads = threads.length > 0;
const showEmptyState = !hasThreads && !isComposerOpen;
// Ensure minimum usable height for empty state + composer button
const minHeight = 180;
@ -81,38 +87,30 @@ export function CommentPanel({
</div>
)}
{showEmptyState && (
<div className="flex min-h-[120px] flex-col items-center justify-center gap-2 p-4 text-center">
<MessageSquarePlus className="size-8 text-muted-foreground/50" />
<p className="text-sm text-muted-foreground">No comments yet</p>
<p className="text-xs text-muted-foreground/70">
Start a conversation about this response
</p>
{!hasThreads && currentUser && (
<div className="flex items-center gap-3 px-4 pt-4 pb-1">
<Avatar className="size-10">
<AvatarImage src={currentUser.avatar_url ?? undefined} alt={currentUser.display_name ?? currentUser.email} />
<AvatarFallback className="bg-primary/10 text-primary text-sm font-medium">
{getInitials(currentUser.display_name, currentUser.email)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="text-sm font-medium">{currentUser.display_name ?? currentUser.email}</span>
</div>
</div>
)}
<div className={cn("p-3", showEmptyState && !isMobile && "border-t", isMobile && "border-t")}>
{isComposerOpen ? (
<CommentComposer
members={members}
membersLoading={membersLoading}
placeholder="Write a comment..."
submitLabel="Comment"
isSubmitting={isSubmitting}
onSubmit={handleCommentSubmit}
onCancel={handleComposerCancel}
autoFocus
/>
) : (
<Button
variant="ghost"
className="w-full justify-start text-muted-foreground hover:text-foreground"
onClick={() => setIsComposerOpen(true)}
>
<MessageSquarePlus className="mr-2 size-4" />
Add a comment...
</Button>
)}
<div className="p-3">
<CommentComposer
members={members}
membersLoading={membersLoading}
placeholder="Comment or @mention"
submitLabel="Comment"
isSubmitting={isSubmitting}
onSubmit={handleCommentSubmit}
autoFocus={!hasThreads}
/>
</div>
</div>
);

View file

@ -20,7 +20,7 @@ export function CommentSheet({
<SheetContent
side={side}
className={cn(
"flex flex-col p-0",
"flex flex-col gap-0 overflow-hidden p-0",
isBottomSheet ? "h-[85vh] max-h-[85vh] rounded-t-xl" : "h-full w-full max-w-md"
)}
>
@ -41,7 +41,7 @@ export function CommentSheet({
)}
</SheetTitle>
</SheetHeader>
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-thin">
<CommentPanelContainer messageId={messageId} isOpen={true} variant="mobile" />
</div>
</SheetContent>

View file

@ -128,23 +128,21 @@ export function CommentThread({
{/* Reply composer or button */}
{isReplyComposerOpen ? (
<>
<div className="pt-3">
<CommentComposer
members={members}
membersLoading={membersLoading}
placeholder="Write a reply..."
submitLabel="Reply"
isSubmitting={isSubmitting}
onSubmit={handleReplySubmit}
onCancel={handleReplyCancel}
autoFocus
/>
</div>
</>
<div className="pt-3">
<CommentComposer
members={members}
membersLoading={membersLoading}
placeholder="Reply or @mention"
submitLabel="Reply"
isSubmitting={isSubmitting}
onSubmit={handleReplySubmit}
onCancel={handleReplyCancel}
autoFocus
/>
</div>
) : (
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs" onClick={handleReply}>
<MessageSquare className="mr-1.5 size-3" />
<MessageSquare className="mr-1 size-3" />
Reply
</Button>
)}
@ -156,7 +154,7 @@ export function CommentThread({
{!hasReplies && !isReplyComposerOpen && (
<div className="ml-7 mt-1">
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs" onClick={handleReply}>
<MessageSquare className="mr-1.5 size-3" />
<MessageSquare className="mr-1 size-3" />
Reply
</Button>
</div>

View file

@ -1,6 +1,6 @@
"use client";
import { MessageSquare } from "lucide-react";
import { MessageSquarePlus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type { CommentTriggerProps } from "./types";
@ -25,7 +25,7 @@ export function CommentTrigger({ commentCount, isOpen, onClick, disabled }: Comm
)}
onClick={onClick}
>
<MessageSquare className={cn("size-5", (hasComments || isOpen) && "fill-current")} />
<MessageSquarePlus className={cn("size-5", (hasComments || isOpen) && "fill-current")} />
{hasComments && (
<span className="absolute -top-1 -right-1 flex size-5 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-foreground">
{commentCount > 9 ? "9+" : commentCount}