mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
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:
parent
26ca751a9d
commit
3fb83e4c3a
8 changed files with 84 additions and 75 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue