chore(desktop): polish reply design surfaces

Align the header, timeline, thread panel, composer, and sidebar shells so the reply design branch reads as one consistent visual system.

Made-with: Cursor
This commit is contained in:
Thomas Petersen
2026-04-14 14:01:18 -04:00
parent 0f5f306364
commit 497006f10a
12 changed files with 265 additions and 259 deletions
@@ -107,8 +107,8 @@ export const ChannelPane = React.memo(function ChannelPane({
isSending;
return (
<div className="flex min-h-0 flex-1 overflow-hidden">
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
<div className="flex min-h-0 min-w-0 flex-1 flex-row overflow-hidden">
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<MessageTimeline
channelId={activeChannel?.id}
activeReplyTargetId={openThreadHeadId}
@@ -139,33 +139,36 @@ export const ChannelPane = React.memo(function ChannelPane({
onToggleReaction={onToggleReaction}
targetMessageId={targetMessageId}
/>
<TypingIndicatorRow
channel={activeChannel}
currentPubkey={currentPubkey}
profiles={profiles}
typingPubkeys={typingPubkeys}
/>
<MessageComposer
channelId={activeChannel?.id ?? null}
channelName={activeChannel?.name ?? "channel"}
disabled={isComposerDisabled}
editTarget={editTarget}
isSending={isSending}
onCancelEdit={onCancelEdit}
onEditSave={onEditSave}
onSend={onSendMessage}
placeholder={
activeChannel?.archivedAt
? "Archived channels are read-only."
: activeChannel && !activeChannel.isMember
? "Join this channel to message."
: activeChannel?.channelType === "forum"
? "Forum posting is not wired in this pass."
: activeChannel
? `Message #${activeChannel.name}`
: "Select a channel"
}
/>
<div className="relative z-10 -mt-10 shrink-0">
<TypingIndicatorRow
channel={activeChannel}
currentPubkey={currentPubkey}
profiles={profiles}
typingPubkeys={typingPubkeys}
/>
<MessageComposer
channelId={activeChannel?.id ?? null}
channelName={activeChannel?.name ?? "channel"}
disabled={isComposerDisabled}
editTarget={editTarget}
isSending={isSending}
onCancelEdit={onCancelEdit}
onEditSave={onEditSave}
onSend={onSendMessage}
placeholder={
activeChannel?.archivedAt
? "Archived channels are read-only."
: activeChannel && !activeChannel.isMember
? "Join this channel to message."
: activeChannel?.channelType === "forum"
? "Forum posting is not wired in this pass."
: activeChannel
? `Message #${activeChannel.name}`
: "Select a channel"
}
showTopBorder={false}
/>
</div>
</div>
{threadHeadMessage ? (
@@ -364,91 +364,97 @@ export function ChannelScreen({
return (
<>
<ChatHeader
actions={
activeChannel ? (
<ChannelMembersBar
channel={activeChannel}
currentPubkey={currentPubkey}
onManageChannel={openChannelManagement}
onToggleMembers={() => setIsMembersSidebarOpen((prev) => !prev)}
/>
) : null
}
channelType={activeChannel?.channelType}
visibility={activeChannel?.visibility}
description={channelDescription}
statusBadge={headerStatusBadge}
title={activeChannelTitle}
/>
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div className="absolute inset-x-0 top-0 z-20">
<ChatHeader
actions={
activeChannel ? (
<ChannelMembersBar
channel={activeChannel}
currentPubkey={currentPubkey}
onManageChannel={openChannelManagement}
onToggleMembers={() =>
setIsMembersSidebarOpen((prev) => !prev)
}
/>
) : null
}
channelType={activeChannel?.channelType}
visibility={activeChannel?.visibility}
description={channelDescription}
statusBadge={headerStatusBadge}
title={activeChannelTitle}
/>
</div>
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{activeChannel ? (
activeChannel.channelType === "forum" ? (
<React.Suspense fallback={<ViewLoadingFallback kind="forum" />}>
<ForumView
channel={activeChannel}
currentPubkey={currentPubkey}
onClosePost={onCloseForumPost}
onSelectPost={onSelectForumPost}
selectedPostId={selectedForumPostId}
targetReplyId={targetForumReplyId}
/>
</React.Suspense>
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{activeChannel ? (
activeChannel.channelType === "forum" ? (
<React.Suspense fallback={<ViewLoadingFallback kind="forum" />}>
<ForumView
channel={activeChannel}
currentPubkey={currentPubkey}
onClosePost={onCloseForumPost}
onSelectPost={onSelectForumPost}
selectedPostId={selectedForumPostId}
targetReplyId={targetForumReplyId}
/>
</React.Suspense>
) : (
<React.Suspense fallback={<ViewLoadingFallback kind="channel" />}>
<ChannelPane
activeChannel={activeChannel}
currentPubkey={currentPubkey}
fetchOlder={fetchOlder}
hasOlderMessages={hasOlderMessages}
isFetchingOlder={isFetchingOlder}
editTarget={
editTargetMessage
? {
author: editTargetMessage.author,
body: editTargetMessage.body,
id: editTargetMessage.id,
}
: null
}
isSending={sendMessageMutation.isPending}
isTimelineLoading={isTimelineLoading}
messages={timelineMessages}
onCancelEdit={handleCancelEdit}
onCancelThreadReply={handleCancelThreadReply}
onBackThread={handleBackThread}
onCloseThread={handleCloseThread}
onDelete={handleDelete}
onEdit={handleEdit}
onEditSave={handleEditSave}
onOpenNestedThread={handleOpenNestedThread}
onOpenThread={handleOpenThread}
onSendMessage={handleSendMessage}
onSendThreadReply={handleSendThreadReply}
onToggleReaction={effectiveToggleReaction}
canGoBackThread={threadHeadPath.length > 1}
openThreadHeadId={openThreadHeadId}
personaLookup={personaLookup}
profiles={messageProfiles}
targetMessageId={targetMessageId}
threadHeadMessage={openThreadHeadMessage}
threadMessages={threadMessages}
threadTypingPubkeys={threadTypingPubkeys}
threadTotalReplyCount={threadTotalReplyCount}
threadReplyTargetId={threadReplyTargetId}
threadReplyTargetMessage={threadReplyTargetMessage}
typingPubkeys={mainTypingPubkeys}
/>
</React.Suspense>
)
) : (
<React.Suspense fallback={<ViewLoadingFallback kind="channel" />}>
<ChannelPane
activeChannel={activeChannel}
currentPubkey={currentPubkey}
fetchOlder={fetchOlder}
hasOlderMessages={hasOlderMessages}
isFetchingOlder={isFetchingOlder}
editTarget={
editTargetMessage
? {
author: editTargetMessage.author,
body: editTargetMessage.body,
id: editTargetMessage.id,
}
: null
}
isSending={sendMessageMutation.isPending}
isTimelineLoading={isTimelineLoading}
messages={timelineMessages}
onCancelEdit={handleCancelEdit}
onCancelThreadReply={handleCancelThreadReply}
onBackThread={handleBackThread}
onCloseThread={handleCloseThread}
onDelete={handleDelete}
onEdit={handleEdit}
onEditSave={handleEditSave}
onOpenNestedThread={handleOpenNestedThread}
onOpenThread={handleOpenThread}
onSendMessage={handleSendMessage}
onSendThreadReply={handleSendThreadReply}
onToggleReaction={effectiveToggleReaction}
canGoBackThread={threadHeadPath.length > 1}
openThreadHeadId={openThreadHeadId}
personaLookup={personaLookup}
profiles={messageProfiles}
targetMessageId={targetMessageId}
threadHeadMessage={openThreadHeadMessage}
threadMessages={threadMessages}
threadTypingPubkeys={threadTypingPubkeys}
threadTotalReplyCount={threadTotalReplyCount}
threadReplyTargetId={threadReplyTargetId}
threadReplyTargetMessage={threadReplyTargetMessage}
typingPubkeys={mainTypingPubkeys}
/>
</React.Suspense>
)
) : (
<div className="flex min-h-0 flex-1 items-center justify-center px-6 py-8">
<p className="text-sm text-muted-foreground">
Select a channel to view messages.
</p>
</div>
)}
<div className="flex min-h-0 flex-1 items-center justify-center px-6 py-8">
<p className="text-sm text-muted-foreground">
Select a channel to view messages.
</p>
</div>
)}
</div>
</div>
<MembersSidebar
+4 -7
View File
@@ -71,9 +71,11 @@ export function ChatHeader({
mode = "channel",
statusBadge,
}: ChatHeaderProps) {
const trimmedDescription = description.trim();
return (
<header
className="flex min-w-0 items-center gap-3 border-b border-border/80 bg-background px-4 pb-3 pt-8 sm:px-6"
className="relative z-20 flex min-w-0 shrink-0 items-center gap-3 bg-background/25 px-4 pb-2 pt-6 shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/20 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)] sm:px-6"
data-testid="chat-header"
data-tauri-drag-region
>
@@ -87,6 +89,7 @@ export function ChatHeader({
<h1
className="min-w-0 truncate text-lg font-semibold tracking-tight"
data-testid="chat-title"
title={trimmedDescription || undefined}
>
{title}
</h1>
@@ -96,12 +99,6 @@ export function ChatHeader({
</div>
) : null}
</div>
<p
className="truncate text-sm text-muted-foreground"
data-testid="chat-description"
>
{description}
</p>
</div>
{actions ? <div className="shrink-0">{actions}</div> : null}
@@ -508,13 +508,13 @@ export function MessageComposer({
return (
<footer
className={cn(
"bg-background p-4",
showTopBorder ? "border-t border-border/80" : "",
"shrink-0 bg-transparent px-4 pb-4 pt-0",
showTopBorder ? "border-t border-border/40 pt-3" : "",
)}
>
<div className="flex w-full flex-col gap-3">
<div className="mx-auto flex w-full max-w-4xl flex-col gap-3">
<form
className="relative rounded-2xl border border-input bg-card px-3 py-4 shadow-sm sm:px-4"
className="relative isolate rounded-2xl border border-border/50 bg-background/25 px-3 py-4 shadow-[0_4px_24px_rgba(0,0,0,0.08)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/20 dark:shadow-[0_4px_24px_rgba(0,0,0,0.35)] sm:px-4"
data-testid="message-composer"
onDragOver={media.handleDragOver}
onDrop={(e) => {
@@ -1,5 +1,5 @@
import * as React from "react";
import { AtSign, Paperclip, SendHorizontal } from "lucide-react";
import { ArrowUp, AtSign, Paperclip } from "lucide-react";
import { Button } from "@/shared/ui/button";
import { ComposerEmojiPicker } from "./ComposerEmojiPicker";
@@ -67,14 +67,22 @@ export const MessageComposerToolbar = React.memo(
</div>
<Button
className="gap-2"
aria-label={isSending ? "Sending" : "Send message"}
className="rounded-full"
data-testid="send-message"
disabled={sendDisabled || isSending}
title="Send (Enter)"
size="icon"
title={isSending ? "Sending..." : "Send (Enter)"}
type="submit"
>
<SendHorizontal className="h-4 w-4" />
{isSending ? "Sending" : "Send"}
{isSending ? (
<span
aria-hidden
className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"
/>
) : (
<ArrowUp aria-hidden className="h-4 w-4" />
)}
</Button>
</div>
);
+63 -65
View File
@@ -161,7 +161,7 @@ export const MessageRow = React.memo(
<article
className={cn(
"group/message flex items-start gap-2.5 rounded-2xl px-2 py-1.5 transition-colors",
"group/message flex items-start gap-2.5 rounded-2xl px-2 py-1 transition-colors",
highlighted ? "bg-primary/10 ring-1 ring-primary/30" : "",
)}
data-message-id={message.id}
@@ -181,7 +181,7 @@ export const MessageRow = React.memo(
<UserAvatar
accent={message.accent}
avatarUrl={message.avatarUrl ?? null}
className="!h-[42px] !w-[42px]"
className="!h-8 !w-8"
displayName={message.author}
testId="message-avatar"
/>
@@ -191,81 +191,79 @@ export const MessageRow = React.memo(
<UserAvatar
accent={message.accent}
avatarUrl={message.avatarUrl ?? null}
className="shrink-0 !h-[42px] !w-[42px]"
className="shrink-0 !h-8 !w-8"
displayName={message.author}
testId="message-avatar"
/>
)}
</div>
<div className="min-w-0 flex-1 space-y-1">
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
{message.pubkey ? (
<UserProfilePopover
pubkey={message.pubkey}
role={message.role}
botIdenticonValue={message.author}
>
<button
className="truncate rounded text-sm font-semibold tracking-tight hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
type="button"
<div className="relative min-w-0 flex-1 space-y-0">
<div className="flex min-w-0 flex-nowrap items-start gap-x-2 pr-[9.5rem] sm:pr-40">
<div className="flex min-w-0 flex-1 items-center gap-x-2">
{message.pubkey ? (
<UserProfilePopover
pubkey={message.pubkey}
role={message.role}
botIdenticonValue={message.author}
>
<button
className="min-w-0 truncate rounded pt-px text-left text-sm font-semibold leading-tight tracking-tight hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
type="button"
>
{message.author}
</button>
</UserProfilePopover>
) : (
<h3 className="min-w-0 truncate pt-px text-sm font-semibold leading-tight tracking-tight">
{message.author}
</button>
</UserProfilePopover>
) : (
<h3 className="truncate text-sm font-semibold tracking-tight">
{message.author}
</h3>
)}
{message.personaDisplayName &&
message.personaDisplayName !== message.author ? (
<span className="text-xs text-muted-foreground">
{message.personaDisplayName}
</span>
) : message.role ? (
<p className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
{message.role}
</p>
) : null}
<div className="ml-auto flex items-center gap-2 text-xs text-muted-foreground">
<div className="relative">
<div className="absolute right-0 top-1/2 -translate-y-1/2">
<MessageActionBar
activeReplyTargetId={activeReplyTargetId}
message={message}
onDelete={onDelete}
onEdit={onEdit}
onReactionSelect={
canToggleReactions ? handleReactionSelect : undefined
}
onReply={onReply}
reactionErrorMessage={reactionErrorMessage}
reactionPending={reactionPending}
reactions={reactions}
/>
</div>
</div>
{message.pending ? (
<p className="font-medium uppercase tracking-[0.14em] text-primary/80">
Sending
</h3>
)}
{message.personaDisplayName &&
message.personaDisplayName !== message.author ? (
<span className="truncate text-xs text-muted-foreground">
{message.personaDisplayName}
</span>
) : message.role ? (
<p className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
{message.role}
</p>
) : null}
{message.edited ? (
<p
className="text-muted-foreground/70"
title="This message has been edited"
>
(edited)
</p>
) : null}
<MessageTimestamp
createdAt={message.createdAt}
time={message.time}
/>
</div>
</div>
{renderBody()}
<div className="absolute right-0 top-0 z-10 flex items-start justify-end gap-2 pt-px text-xs text-muted-foreground">
<MessageActionBar
activeReplyTargetId={activeReplyTargetId}
message={message}
onDelete={onDelete}
onEdit={onEdit}
onReactionSelect={
canToggleReactions ? handleReactionSelect : undefined
}
onReply={onReply}
reactionErrorMessage={reactionErrorMessage}
reactionPending={reactionPending}
reactions={reactions}
/>
{message.pending ? (
<p className="shrink-0 font-medium uppercase tracking-[0.14em] text-primary/80">
Sending
</p>
) : null}
{message.edited ? (
<p
className="shrink-0 text-muted-foreground/70"
title="This message has been edited"
>
(edited)
</p>
) : null}
<MessageTimestamp
createdAt={message.createdAt}
time={message.time}
/>
</div>
<div className="pt-1">{renderBody()}</div>
<MessageReactions
messageId={message.id}
reactions={reactions}
@@ -90,24 +90,26 @@ export function MessageThreadPanel({
return (
<aside
className="hidden h-full w-[380px] shrink-0 flex-col border-l border-border/80 bg-background lg:flex"
className="relative z-10 hidden h-full min-h-0 w-[min(100%,420px)] shrink-0 flex-col border-l border-border/60 bg-muted/20 pt-0 lg:flex"
data-testid="message-thread-panel"
>
<div className="flex items-center gap-3 px-4 py-3">
{canGoBack ? (
<Button
aria-label="Back"
data-testid="message-thread-back"
onClick={onBack}
size="icon"
type="button"
variant="ghost"
>
<ArrowLeft className="h-4 w-4" />
</Button>
) : null}
<div className="min-w-0 flex-1">
<h2 className="text-sm font-semibold tracking-tight">Thread</h2>
<div className="relative z-20 flex shrink-0 items-center justify-between bg-background/25 px-2 py-1 shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/20 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)]">
<div className="flex min-w-0 items-center gap-2">
{canGoBack ? (
<Button
aria-label="Back"
data-testid="message-thread-back"
onClick={onBack}
size="icon"
type="button"
variant="ghost"
>
<ArrowLeft className="h-4 w-4" />
</Button>
) : null}
<div className="min-w-0">
<h2 className="text-sm font-semibold tracking-tight">Thread</h2>
</div>
</div>
<Button
aria-label="Close thread"
@@ -122,26 +124,28 @@ export function MessageThreadPanel({
</div>
<div
className="min-h-0 flex-1 overflow-y-auto"
className="min-h-0 flex-1 overflow-y-auto overscroll-contain"
data-testid="message-thread-body"
>
<div className="px-3 py-3" data-testid="message-thread-head">
<MessageRow
activeReplyTargetId={replyTargetId}
message={threadHead}
onDelete={
onDelete && canManageMessage(threadHead, currentPubkey)
? onDelete
: undefined
}
onToggleReaction={onToggleReaction}
profiles={profiles}
/>
<div className="px-4 py-3" data-testid="message-thread-head">
<div className="mx-auto w-full max-w-[23rem]">
<MessageRow
activeReplyTargetId={replyTargetId}
message={threadHead}
onDelete={
onDelete && canManageMessage(threadHead, currentPubkey)
? onDelete
: undefined
}
onToggleReaction={onToggleReaction}
profiles={profiles}
/>
</div>
</div>
<div className="px-3 py-3" data-testid="message-thread-replies">
<div className="px-4 pb-2 pt-3" data-testid="message-thread-replies">
{threadReplies.length > 0 ? (
<div className="space-y-2">
<div className="mx-auto w-full max-w-[23rem] space-y-2">
{threadReplies.map((entry) => (
<div key={entry.message.id}>
<MessageRow
@@ -166,20 +170,11 @@ export function MessageThreadPanel({
</div>
))}
</div>
) : (
<div className="rounded-2xl border border-dashed border-border/70 bg-card/40 px-4 py-6 text-center">
<p className="text-sm font-medium text-foreground/80">
No replies in this branch yet
</p>
<p className="mt-1 text-xs text-muted-foreground">
Reply in the thread to continue this branch.
</p>
</div>
)}
) : null}
</div>
</div>
<div className="p-4">
<div className="relative z-10 -mt-10 shrink-0">
<TypingIndicatorRow
channel={channel}
currentPubkey={currentPubkey}
@@ -35,7 +35,7 @@ export function MessageThreadSummaryRow({
}) {
return (
<button
className="ml-8 flex w-fit max-w-full items-center gap-2 rounded-xl px-3 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
className="-mt-1 ml-8 flex w-fit max-w-full items-center gap-2 rounded-xl px-3 py-1 text-left text-sm text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
data-thread-head-id={message.id}
data-testid="message-thread-summary"
onClick={() => onOpenThread(message)}
@@ -92,25 +92,28 @@ export const MessageTimeline = React.memo(function MessageTimeline({
return (
<TooltipProvider delayDuration={200}>
<div className="relative min-h-0 flex-1">
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{stickyDayLabel && !isAtBottom ? (
<div
className="pointer-events-none absolute inset-x-0 top-0 z-10 flex justify-center px-4 pt-2 sm:px-6"
className="pointer-events-none absolute inset-x-0 top-0 z-10 flex justify-center px-4 pt-1 sm:px-6"
data-testid="message-timeline-sticky-day"
>
<p className="rounded-full bg-muted/90 px-3 py-1 text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground shadow-sm backdrop-blur-sm">
<p className="rounded-md bg-muted/60 px-2 py-0.5 text-[11px] font-medium leading-none tracking-normal text-muted-foreground/75 shadow-sm backdrop-blur-sm">
{stickyDayLabel}
</p>
</div>
) : null}
<div
className="h-full overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-3 [overflow-anchor:none] sm:px-6"
className="absolute inset-0 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pb-3 pt-1 [overflow-anchor:none] sm:px-6"
data-scroll-restoration-id="message-timeline"
data-testid="message-timeline"
onScroll={syncScrollState}
ref={scrollContainerRef}
>
<div className="flex w-full flex-col gap-3 px-4" ref={contentRef}>
<div
className="mx-auto flex w-full max-w-4xl flex-col gap-2 pb-36 pt-10"
ref={contentRef}
>
<div ref={topSentinelRef} aria-hidden className="h-px" />
{isFetchingOlder ? (
@@ -168,17 +171,18 @@ export const MessageTimeline = React.memo(function MessageTimeline({
</div>
{!isAtBottom ? (
<div className="pointer-events-none absolute inset-x-0 bottom-4 flex justify-center px-4">
<div className="pointer-events-none absolute inset-x-0 bottom-12 z-20 flex justify-center px-4">
<Button
className="pointer-events-auto rounded-full shadow-lg"
className="pointer-events-auto h-7 min-h-7 gap-1.5 rounded-full border-border/50 bg-background/85 px-2.5 text-[11px] font-medium text-muted-foreground shadow-sm backdrop-blur-sm hover:bg-muted/70 hover:text-foreground [&_svg]:size-3.5"
data-testid="message-scroll-to-latest"
onClick={() => {
scrollToBottom("smooth");
}}
size="sm"
type="button"
variant="outline"
>
<ArrowDown className="h-4 w-4" />
<ArrowDown aria-hidden />
{newMessageCount > 0
? `${newMessageCount} new message${newMessageCount === 1 ? "" : "s"}`
: "Jump to latest"}
@@ -73,18 +73,18 @@ export function TypingIndicatorRow({
return (
<div
aria-live="polite"
className="bg-background/95 px-4 py-2 sm:px-6"
className="shrink-0 bg-transparent px-4 py-2 sm:px-6"
data-testid="message-typing-indicator"
>
<div className="flex w-full items-center gap-2">
<div className="flex flex-shrink-0 items-center">
<div className="mx-auto flex w-full max-w-4xl items-center gap-2">
<div className="flex shrink-0 items-center">
{typingPubkeys.map((pubkey, index) => {
const profile = profiles?.[pubkey];
const profile = profiles?.[pubkey.toLowerCase()];
const label = labels[index] ?? pubkey.slice(0, 8);
return (
<div
key={pubkey}
className={`relative h-5 w-5 flex-shrink-0 rounded-full ring-1 ring-background${index > 0 ? " -ml-1.5" : ""}`}
className={`relative h-5 w-5 shrink-0 rounded-full ring-1 ring-background${index > 0 ? " -ml-1.5" : ""}`}
data-testid="message-typing-avatar"
>
<ProfileAvatar
@@ -35,7 +35,6 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarSeparator,
} from "@/shared/ui/sidebar";
// ---------------------------------------------------------------------------
@@ -429,8 +428,6 @@ export function AppSidebar({
</SidebarMenu>
</SidebarHeader>
<SidebarSeparator className="mx-0 w-full" />
<SidebarContent>
{isLoading ? (
<SidebarGroup>
@@ -513,8 +510,6 @@ export function AppSidebar({
) : null}
</SidebarContent>
<SidebarSeparator className="mx-0 w-full" />
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
+5 -5
View File
@@ -249,7 +249,7 @@ const Sidebar = React.forwardRef<
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
className,
)}
{...props}
@@ -308,10 +308,10 @@ const SidebarRail = React.forwardRef<
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
@@ -698,7 +698,7 @@ const SidebarMenuSub = React.forwardRef<
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"mx-3.5 flex min-w-0 flex-col gap-1 px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
@@ -730,7 +730,7 @@ const SidebarMenuSubButton = React.forwardRef<
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:text-sidebar-primary-foreground",
"flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:text-sidebar-primary-foreground",
"data-[active=true]:bg-sidebar-primary data-[active=true]:font-semibold data-[active=true]:text-sidebar-primary-foreground data-[active=true]:hover:bg-sidebar-primary data-[active=true]:hover:text-sidebar-primary-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",