feat: add compact mode to CommentComposer for streamlined input; update UI components for improved layout and user experience

This commit is contained in:
Anish Sarkar 2026-03-10 18:24:28 +05:30
parent 6a88f9e0eb
commit 7035703993
6 changed files with 92 additions and 114 deletions

View file

@ -1,6 +1,6 @@
"use client";
import { Send, X } from "lucide-react";
import { ArrowUp, Send, X } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover";
@ -86,6 +86,7 @@ export function CommentComposer({
onCancel,
autoFocus = false,
initialValue = "",
compact = false,
}: CommentComposerProps) {
const [displayContent, setDisplayContent] = useState(initialValue);
const [insertedMentions, setInsertedMentions] = useState<InsertedMention[]>([]);
@ -257,44 +258,46 @@ export function CommentComposer({
}, [adjustTextareaHeight]);
return (
<div className="flex flex-col gap-2">
<Popover
open={mentionState.isActive}
onOpenChange={(open) => !open && closeMentionPicker()}
modal={false}
>
<PopoverAnchor asChild>
<Textarea
ref={textareaRef}
value={displayContent}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className="min-h-[40px] max-h-[200px] resize-none overflow-y-auto scrollbar-thin"
rows={1}
disabled={isSubmitting}
/>
</PopoverAnchor>
<PopoverContent
side="top"
align="start"
sideOffset={4}
collisionPadding={8}
className="w-72 p-0"
onOpenAutoFocus={(e) => e.preventDefault()}
<div className={cn("flex", compact ? "flex-row items-center gap-2" : "flex-col gap-2")}>
<div className={cn(compact && "flex-1 min-w-0")}>
<Popover
open={mentionState.isActive}
onOpenChange={(open) => !open && closeMentionPicker()}
modal={false}
>
<MemberMentionPicker
members={members}
query={mentionState.query}
highlightedIndex={highlightedIndex}
isLoading={membersLoading}
onSelect={insertMention}
onHighlightChange={setHighlightedIndex}
/>
</PopoverContent>
</Popover>
<PopoverAnchor asChild>
<Textarea
ref={textareaRef}
value={displayContent}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className="min-h-[40px] max-h-[200px] w-full resize-none overflow-y-auto scrollbar-thin border-none shadow-none focus-visible:ring-0 bg-transparent dark:bg-transparent"
rows={1}
disabled={isSubmitting}
/>
</PopoverAnchor>
<PopoverContent
side="top"
align="start"
sideOffset={4}
collisionPadding={8}
className="w-72 p-0"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<MemberMentionPicker
members={members}
query={mentionState.query}
highlightedIndex={highlightedIndex}
isLoading={membersLoading}
onSelect={insertMention}
onHighlightChange={setHighlightedIndex}
/>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center justify-end gap-2">
<div className={cn("flex items-center gap-2", !compact && "justify-end")}>
{onCancel && (
<Button
type="button"
@ -309,13 +312,22 @@ export function CommentComposer({
)}
<Button
type="button"
size="sm"
size={compact ? "icon" : "sm"}
onClick={handleSubmit}
disabled={!canSubmit}
className={cn(!canSubmit && "opacity-50")}
className={cn(
!canSubmit && "opacity-50",
compact && "size-8 shrink-0 rounded-full"
)}
>
<Send className="mr-1 size-4" />
{submitLabel}
{compact ? (
<ArrowUp className="size-4" />
) : (
<>
<Send className="mr-1 size-4" />
{submitLabel}
</>
)}
</Button>
</div>
</div>

View file

@ -10,6 +10,8 @@ export interface CommentComposerProps {
onCancel?: () => void;
autoFocus?: boolean;
initialValue?: string;
/** Compact mode: inline send button with ArrowUp icon, no label */
compact?: boolean;
}
export interface MentionState {

View file

@ -6,7 +6,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import type { CommentActionsProps } from "./types";
@ -34,8 +33,7 @@ export function CommentActions({ canEdit, canDelete, onEdit, onDelete }: Comment
Edit
</DropdownMenuItem>
)}
{canEdit && canDelete && <DropdownMenuSeparator />}
{canDelete && (
{canDelete && (
<DropdownMenuItem onClick={onDelete}>
<Trash2 className="mr-2 size-4" />
Delete

View file

@ -1,25 +1,10 @@
"use client";
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,
@ -33,8 +18,6 @@ export function CommentPanel({
maxHeight,
variant = "desktop",
}: CommentPanelProps) {
const [{ data: currentUser }] = useAtom(currentUserAtom);
const handleCommentSubmit = (content: string) => {
onCreateComment(content);
};
@ -46,16 +29,16 @@ export function CommentPanel({
return (
<div
className={cn(
"flex min-h-[120px] items-center justify-center p-4",
isInline && "w-full rounded-xl border bg-card shadow-lg",
!isMobile && !isInline && "w-96 rounded-lg border bg-card"
)}
>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Loading comments...
</div>
</div>
"flex min-h-[120px] items-center justify-center p-4",
isInline && "w-full rounded-xl border-sidebar-border border bg-sidebar text-sidebar-foreground shadow-lg",
!isMobile && !isInline && "w-96 rounded-lg border-sidebar-border border bg-sidebar text-sidebar-foreground"
)}
>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Loading comments...
</div>
</div>
);
}
@ -70,8 +53,8 @@ export function CommentPanel({
className={cn(
"flex flex-col",
isMobile && "w-full",
isInline && "w-full rounded-xl border bg-card shadow-lg max-h-80",
!isMobile && !isInline && "w-85 rounded-lg border bg-card"
isInline && "w-full rounded-xl border-sidebar-border border bg-sidebar text-sidebar-foreground shadow-lg max-h-80",
!isMobile && !isInline && "w-85 rounded-lg border-sidebar-border border bg-sidebar text-sidebar-foreground"
)}
style={
!isMobile && !isInline && effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined
@ -96,36 +79,18 @@ export function CommentPanel({
</div>
)}
{!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", isMobile && "fixed bottom-0 left-0 right-0 z-50 bg-card border-t")}>
<CommentComposer
members={members}
membersLoading={membersLoading}
placeholder="Comment or @mention"
submitLabel="Comment"
isSubmitting={isSubmitting}
onSubmit={handleCommentSubmit}
autoFocus={!hasThreads}
/>
</div>
<div className={cn("p-3", isMobile && "fixed bottom-0 left-0 right-0 z-50 bg-card border-t")}>
<CommentComposer
members={members}
membersLoading={membersLoading}
placeholder="Comment or @mention"
submitLabel="Comment"
isSubmitting={isSubmitting}
onSubmit={handleCommentSubmit}
autoFocus={!hasThreads}
compact
/>
</div>
</div>
);
}

View file

@ -129,16 +129,17 @@ export function CommentThread({
{isReplyComposerOpen ? (
<div className="pt-3">
<CommentComposer
members={members}
membersLoading={membersLoading}
placeholder="Reply or @mention"
submitLabel="Reply"
isSubmitting={isSubmitting}
onSubmit={handleReplySubmit}
onCancel={handleReplyCancel}
autoFocus
/>
<CommentComposer
members={members}
membersLoading={membersLoading}
placeholder="Reply or @mention"
submitLabel="Reply"
isSubmitting={isSubmitting}
onSubmit={handleReplySubmit}
onCancel={handleReplyCancel}
autoFocus
compact
/>
</div>
) : (
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs" onClick={handleReply}>

View file

@ -29,7 +29,7 @@ export function MemberMentionItem({
type="button"
className={cn(
"flex w-full items-center gap-3 px-3 py-2 text-left transition-colors",
isHighlighted ? "bg-accent" : "hover:bg-accent/50"
isHighlighted ? "bg-primary/15 text-accent-foreground" : "hover:bg-accent/50"
)}
onClick={() => onSelect(member)}
onMouseEnter={onMouseEnter}