Style channel header navigation (#889)

This commit is contained in:
klopez4212
2026-06-08 07:03:06 -07:00
committed by GitHub
parent 2ebe551741
commit 29f6ccf9e9
13 changed files with 137 additions and 54 deletions
@@ -55,6 +55,7 @@ export function AgentSessionThreadPanel({
const avatarUrl = profiles?.[agent.pubkey.toLowerCase()]?.avatarUrl ?? null;
const isOverlay = useIsThreadPanelOverlay();
const isFloatingOverlay = isOverlay && !isSinglePanelView;
const usesChannelSplitChrome = !isOverlay && !isSinglePanelView;
useEscapeKey(onClose, isOverlay || isSinglePanelView);
const { ref: scrollRef, onScroll } = useStickToBottom<HTMLDivElement>();
@@ -111,7 +112,10 @@ export function AgentSessionThreadPanel({
{!isOverlay ? (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 z-40 h-[76px] bg-background/75 backdrop-blur-md before:absolute before:left-0 before:right-0 before:top-10 before:h-px before:bg-border/35 after:absolute after:bottom-0 after:-left-px after:top-10 after:w-px after:bg-border/80 supports-[backdrop-filter]:bg-background/65 dark:bg-background/45 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/35"
className={cn(
"pointer-events-none absolute inset-x-0 top-0 z-40 bg-background/75 backdrop-blur-md before:absolute before:left-0 before:right-0 before:top-10 before:h-px before:bg-border/35 after:absolute after:bottom-0 after:-left-px after:top-10 after:w-px after:bg-border/80 supports-[backdrop-filter]:bg-background/65 dark:bg-background/45 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/35",
usesChannelSplitChrome ? "h-[92px]" : "h-[76px]",
)}
/>
) : null}
@@ -122,7 +126,7 @@ export function AgentSessionThreadPanel({
? `relative ${PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS} -mb-[76px] min-h-[76px] shrink-0 gap-[10px] bg-background/80 pb-[4px] pl-[16px] pr-[8px] pt-[42px] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 sm:pl-[24px] sm:pr-[12px] dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55`
: isOverlay
? "relative z-50 min-h-[44px] shrink-0 gap-3 bg-background/80 px-3 py-[6px] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55"
: "absolute inset-x-0 top-[42px] z-50 min-h-[32px] gap-3 px-3 py-[4px]",
: "absolute inset-x-0 top-[48px] z-50 h-[32px] gap-[10px] py-0 pl-[16px] pr-[8px] sm:pr-[12px]",
)}
data-tauri-drag-region
>
@@ -139,7 +143,14 @@ export function AgentSessionThreadPanel({
displayName={agent.name}
size="xs"
/>
<h2 className="min-w-0 flex-1 translate-y-px truncate text-sm font-semibold leading-5 tracking-tight">
<h2
className={cn(
"min-w-0 flex-1 translate-y-px truncate font-semibold tracking-tight",
usesChannelSplitChrome
? "text-base leading-6"
: "text-sm leading-5",
)}
>
{agent.name}
</h2>
</div>
@@ -186,14 +197,22 @@ export function AgentSessionThreadPanel({
) : null}
<Button
aria-label="Close activity panel"
className="h-6 w-6 text-foreground hover:bg-muted/60 hover:text-foreground"
className={cn(
usesChannelSplitChrome
? "h-8 w-8 rounded-lg border border-border/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground [&_svg]:size-5"
: "h-6 w-6 text-foreground hover:bg-muted/60 hover:text-foreground",
)}
data-testid="agent-session-close"
onClick={onClose}
size="icon"
type="button"
variant="ghost"
>
<X className="h-3.5 w-3.5" />
<X
className={cn(
usesChannelSplitChrome ? "size-5" : "h-3.5 w-3.5",
)}
/>
</Button>
</div>
</div>
@@ -204,10 +223,13 @@ export function AgentSessionThreadPanel({
onScroll={onScroll}
className={cn(
"min-h-0 flex-1 overflow-y-auto px-3 pb-4",
// Match MessageThreadPanel: single-panel mode has a 76px header
// (min-h-[76px] with -mb-[76px]), so the body must clear it. Only
// the floating overlay (44px header) uses the smaller pt-4.
isSinglePanelView ? "pt-[76px]" : isOverlay ? "pt-4" : "pt-[76px]",
// Single-panel mode keeps the 76px local header; split panes sit
// under the channel screen's 92px top chrome.
usesChannelSplitChrome
? "pt-[92px]"
: isOverlay
? "pt-4"
: "pt-[76px]",
)}
>
<ManagedAgentSessionPanel
@@ -84,10 +84,10 @@ export function ChannelMembersBar({
return (
<React.Fragment>
<div className="flex items-center gap-1">
<div className="flex items-center gap-[6px]">
<Button
aria-label="Add agent"
className="h-7 w-7 rounded-full"
className="h-8 w-8 rounded-lg border border-border/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground [&_svg]:size-5"
data-testid="channel-add-bot-trigger"
disabled={!canAddAgents}
onClick={() => {
@@ -95,27 +95,27 @@ export function ChannelMembersBar({
}}
size="icon"
type="button"
variant="outline"
variant="ghost"
>
<Plus className="h-3 w-3" />
<Plus className="size-5" />
</Button>
<Button
aria-label={`View channel members (${memberCount})`}
className="h-7 gap-1 rounded-full px-2"
className="h-8 gap-1 rounded-lg border border-border/40 px-2.5 text-muted-foreground hover:bg-muted/70 hover:text-foreground [&_svg]:size-5"
data-testid="channel-members-trigger"
onClick={onToggleMembers}
type="button"
variant="outline"
variant="ghost"
>
<Users className="h-3 w-3" />
<Users className="size-5" />
<span className="min-w-[1ch] text-[11px] font-medium tabular-nums">
{memberCount}
</span>
</Button>
<HuddleIndicator
className="h-7 w-7"
className="h-8 w-8"
channelId={channel.id}
onStart={async () => {
try {
@@ -132,14 +132,14 @@ export function ChannelMembersBar({
<Button
aria-label="Manage channel"
className="h-7 w-7 rounded-full"
className="h-8 w-8 rounded-lg border border-border/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground [&_svg]:size-5"
data-testid="channel-management-trigger"
onClick={onManageChannel}
size="icon"
type="button"
variant="outline"
variant="ghost"
>
<Settings2 className="h-3 w-3" />
<Settings2 className="size-5" />
</Button>
</div>
@@ -44,8 +44,12 @@ import { useChannelFind } from "@/features/search/useChannelFind";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
import { AgentSessionProvider } from "@/shared/context/AgentSessionContext";
import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext";
import { useElementWidthBreakpoint } from "@/shared/hooks/use-mobile";
import {
useElementWidthBreakpoint,
useIsThreadPanelOverlay,
} from "@/shared/hooks/use-mobile";
import {
THREAD_PANEL_MIN_WIDTH_PX,
THREAD_PANEL_SINGLE_COLUMN_BREAKPOINT_PX,
useThreadPanelWidth,
} from "@/shared/hooks/useThreadPanelWidth";
@@ -87,6 +91,7 @@ export function ChannelScreen({
widthPx: threadPanelWidthPx,
} = useThreadPanelWidth();
const [isMembersSidebarOpen, setIsMembersSidebarOpen] = React.useState(false);
const isThreadPanelOverlay = useIsThreadPanelOverlay();
const [channelContentRef, isNarrowPanelViewport] =
useElementWidthBreakpoint<HTMLDivElement>(
THREAD_PANEL_SINGLE_COLUMN_BREAKPOINT_PX,
@@ -435,6 +440,15 @@ export function ChannelScreen({
Boolean(
openThreadHeadMessage || openAgentSessionPubkey || profilePanelPubkey,
);
const hasSplitRightPanel =
!isSinglePanelView &&
!isThreadPanelOverlay &&
Boolean(
openThreadHeadMessage || openAgentSessionPubkey || profilePanelPubkey,
);
const headerActionsRightInset = hasSplitRightPanel
? `min(${threadPanelWidthPx}px, calc(100% - ${THREAD_PANEL_MIN_WIDTH_PX}px))`
: undefined;
return (
<AgentSessionProvider onOpenAgentSession={handleOpenAgentSession}>
@@ -443,6 +457,7 @@ export function ChannelScreen({
activeChannel={activeChannel}
activeChannelEphemeralDisplay={activeChannelEphemeralDisplay}
activeChannelTitle={activeChannelTitle}
actionsRightInset={headerActionsRightInset}
activeDmPresenceStatus={activeDmPresenceStatus}
currentPubkey={currentPubkey}
isJoining={joinChannelMutation.isPending}
@@ -14,6 +14,7 @@ type ChannelScreenHeaderProps = {
activeChannel: Channel | null;
activeChannelEphemeralDisplay: EphemeralChannelDisplay | null;
activeChannelTitle: string;
actionsRightInset?: string;
activeDmPresenceStatus: PresenceStatus | null;
currentPubkey?: string;
isJoining?: boolean;
@@ -27,6 +28,7 @@ export function ChannelScreenHeader({
activeChannel,
activeChannelEphemeralDisplay,
activeChannelTitle,
actionsRightInset,
activeDmPresenceStatus,
currentPubkey,
isJoining = false,
@@ -79,10 +81,10 @@ export function ChannelScreenHeader({
return (
<ChatHeader
actionsPlacement="top-right"
belowSystemChrome
density="compact"
actions={actions}
actionsRightInset={actionsRightInset}
channelType={activeChannel?.channelType}
description={getChannelDescription(activeChannel)}
statusBadge={
+12 -7
View File
@@ -19,6 +19,7 @@ import { cn } from "@/shared/lib/cn";
type ChatHeaderProps = {
actions?: React.ReactNode;
actionsPlacement?: "inline" | "top-right";
actionsRightInset?: string;
belowSystemChrome?: boolean;
density?: "default" | "compact";
title: string;
@@ -80,6 +81,7 @@ function ChannelIcon({
export function ChatHeader({
actions,
actionsPlacement = "inline",
actionsRightInset,
belowSystemChrome = false,
density = "default",
title,
@@ -101,10 +103,8 @@ export function ChatHeader({
const header = (
<header
className={cn(
"relative z-30 flex min-w-0 shrink-0 cursor-default select-none items-center gap-[10px] bg-transparent pl-[16px] pr-[8px] transition-[margin,padding] duration-200 ease-linear sm:pl-[24px] sm:pr-[12px]",
density === "compact"
? "min-h-[32px] py-[4px]"
: "min-h-[44px] py-[6px]",
"pointer-events-auto relative z-30 flex min-w-0 shrink-0 cursor-default select-none items-center gap-[10px] bg-transparent pl-[16px] pr-[8px] transition-[margin,padding] duration-200 ease-linear sm:pl-[24px] sm:pr-[12px]",
density === "compact" ? "h-[32px] py-0" : "min-h-[44px] py-[6px]",
overlaysContent && !belowSystemChrome && "-mb-[44px]",
)}
data-testid="chat-header"
@@ -118,7 +118,7 @@ export function ChatHeader({
visibility={visibility}
/>
<h1
className="min-w-0 translate-y-px truncate text-sm font-semibold leading-5 tracking-tight"
className="min-w-0 translate-y-px truncate text-base font-semibold leading-6 tracking-tight"
data-testid="chat-title"
title={trimmedDescription || undefined}
>
@@ -137,7 +137,12 @@ export function ChatHeader({
createPortal(topRightActions, document.body)
)
) : (
<div className="flex shrink-0 items-center gap-1">
<div
className="flex shrink-0 items-center gap-1"
style={
actionsRightInset ? { marginRight: actionsRightInset } : undefined
}
>
<UpdateIndicator />
{actions ? <div className="shrink-0">{actions}</div> : null}
</div>
@@ -150,7 +155,7 @@ export function ChatHeader({
}
return (
<div className="relative z-30 h-[76px] -mb-[76px] bg-background/80 pt-[42px] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55">
<div className="pointer-events-none relative z-30 h-[92px] -mb-[92px] bg-background/80 pb-[9px] pt-[48px] backdrop-blur-md after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-border/35 after:content-[''] supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55">
{header}
</div>
);
@@ -230,15 +230,18 @@ export function HuddleIndicator({
<>
<Button
aria-label="Start huddle"
className={cn("h-7 w-7 rounded-full", className)}
className={cn(
"h-8 w-8 rounded-lg border border-border/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground [&_svg]:size-5",
className,
)}
data-testid="channel-start-huddle-trigger"
disabled={startDisabled || isStarting}
onClick={() => headphonesGate.gate(() => onStart())}
size="icon"
type="button"
variant="outline"
variant="ghost"
>
<Headphones className="h-3 w-3" />
<Headphones className="size-5" />
</Button>
{gateDialog}
</>
@@ -270,15 +273,18 @@ export function HuddleIndicator({
<TooltipTrigger asChild>
<Button
aria-label={`Join active huddle (${participantCount} participant${participantCount !== 1 ? "s" : ""})`}
className={cn("relative h-7 w-7 rounded-full", className)}
className={cn(
"relative h-8 w-8 rounded-lg border border-border/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground [&_svg]:size-5",
className,
)}
disabled={isJoining || isStarting}
onClick={() => headphonesGate.gate(() => void doJoin())}
size="icon"
type="button"
variant="outline"
variant="ghost"
>
<Headphones className="h-3 w-3 text-muted-foreground" />
<span className="absolute inset-0 animate-pulse rounded-full ring-2 ring-border/70" />
<Headphones className="size-5 text-muted-foreground" />
<span className="absolute inset-0 animate-pulse rounded-lg ring-2 ring-border/70" />
{/* Participant count badge */}
{participantCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full border border-border bg-background px-0.5 text-[9px] font-bold text-muted-foreground">
@@ -2,11 +2,11 @@ export function DayDivider({ label }: { label: string }) {
return (
<section
aria-label={label}
className="sticky top-[76px] z-[5] flex justify-center py-1"
className="sticky top-[92px] z-[5] flex justify-center py-1"
data-testid="message-timeline-day-divider"
data-day-label={label}
>
<p className="shrink-0 rounded-lg border border-border/70 bg-background/95 px-2 py-1 text-[10px] font-medium tracking-[0.02em] text-muted-foreground/70 shadow-xs backdrop-blur-sm">
<p className="relative z-10 shrink-0 rounded-lg border border-border/70 bg-background/95 px-2 py-1 text-[10px] font-medium tracking-[0.02em] text-muted-foreground/70 shadow-xs backdrop-blur-sm">
{label}
</p>
</section>
@@ -12,7 +12,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
const REACTION_PILL_BASE_CLASSES =
"inline-flex h-8 items-center rounded-full border text-xs font-medium leading-none transition-colors";
const REACTION_GLYPH_CLASSES = "-translate-y-px h-3.5 w-3.5 text-sm";
const REACTION_GLYPH_CLASSES = "h-3.5 w-3.5 translate-y-px text-sm";
const REACTION_PILL_HOVER_CLASSES =
"hover:bg-primary/10 hover:text-foreground focus-visible:bg-primary/10 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring";
@@ -127,6 +127,7 @@ export function MessageThreadPanel({
const threadComposerWrapperRef = React.useRef<HTMLDivElement>(null);
const isOverlay = useIsThreadPanelOverlay();
const isFloatingOverlay = isOverlay && !isSinglePanelView;
const usesChannelSplitChrome = !isOverlay && !isSinglePanelView;
useEscapeKey(onClose, isOverlay || isSinglePanelView);
useComposerHeightPadding(
threadBodyRef,
@@ -207,7 +208,10 @@ export function MessageThreadPanel({
{!isOverlay ? (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 z-40 h-[76px] bg-transparent after:absolute after:bottom-0 after:-left-px after:top-10 after:w-px after:bg-border/45 after:transition-colors peer-hover/thread-resize:after:bg-border/80 peer-focus-visible/thread-resize:after:bg-border/80"
className={cn(
"pointer-events-none absolute inset-x-0 top-0 z-40 bg-transparent after:absolute after:bottom-0 after:-left-px after:top-10 after:w-px after:bg-border/45 after:transition-colors peer-hover/thread-resize:after:bg-border/80 peer-focus-visible/thread-resize:after:bg-border/80",
usesChannelSplitChrome ? "h-[92px]" : "h-[76px]",
)}
/>
) : null}
@@ -215,10 +219,10 @@ export function MessageThreadPanel({
className={cn(
"flex cursor-default select-none items-center",
isSinglePanelView
? `relative ${PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS} -mb-[76px] min-h-[76px] shrink-0 gap-[10px] bg-background/80 pb-[3px] pl-[16px] pr-[8px] pt-[43px] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 sm:pl-[24px] sm:pr-[12px] dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55`
? `relative ${PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS} -mb-[76px] min-h-[76px] shrink-0 gap-[10px] bg-background/80 pb-[3px] pl-[16px] pr-[8px] pt-[43px] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 sm:pr-[12px] dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55`
: isOverlay
? "relative z-50 min-h-[44px] shrink-0 gap-3 bg-background/80 px-3 py-[6px] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55"
: "absolute inset-x-0 top-[42px] z-50 min-h-[32px] gap-3 px-3 py-[4px]",
: "absolute inset-x-0 top-[48px] z-50 h-[32px] gap-[10px] py-0 pl-[16px] pr-[8px] sm:pr-[12px]",
)}
data-tauri-drag-region
>
@@ -243,27 +247,27 @@ export function MessageThreadPanel({
</Button>
</div>
) : null}
<h2 className="translate-y-px text-sm font-semibold leading-5 tracking-tight">
<h2 className="translate-y-px text-base font-semibold leading-6 tracking-tight">
Thread
</h2>
</div>
<Button
aria-label="Close thread"
className="ml-auto h-4 w-4 rounded-full text-foreground hover:bg-muted/60 hover:text-foreground"
className="ml-auto h-8 w-8 rounded-lg border border-border/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground [&_svg]:size-5"
data-testid="message-thread-close"
onClick={onClose}
size="icon"
type="button"
variant="ghost"
>
<X className="h-2.5 w-2.5" />
<X className="size-5" />
</Button>
</div>
<div
className={cn(
"min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain pb-24 [overflow-anchor:none]",
isSinglePanelView ? "pt-[76px]" : isOverlay ? "" : "pt-[76px]",
usesChannelSplitChrome ? "pt-[92px]" : isOverlay ? "" : "pt-[76px]",
)}
data-testid="message-thread-body"
onScroll={syncScrollState}
@@ -148,7 +148,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
ref={scrollContainerRef}
>
<div
className="flex w-full flex-col gap-2 pt-[76px]"
className="flex w-full flex-col gap-2 pt-[92px]"
ref={contentRef}
>
<div ref={topSentinelRef} aria-hidden className="h-px" />
@@ -189,7 +189,10 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
}
return dayGroups.map((group) => (
<section className="flex flex-col gap-2.5" key={group.key}>
<section
className="relative flex flex-col gap-2.5 before:absolute before:inset-x-0 before:top-[15px] before:h-px before:bg-border/35 before:content-['']"
key={group.key}
>
<DayDivider label={group.label} />
{group.elements}
</section>
@@ -106,6 +106,8 @@ export function UserProfilePanel({
}: UserProfilePanelProps) {
const isOverlay = useIsThreadPanelOverlay();
const isFloatingOverlay = isOverlay && !isSinglePanelView;
const usesChannelSplitChrome =
splitPaneClamp && !isOverlay && !isSinglePanelView;
useEscapeKey(onClose, isOverlay || isSinglePanelView);
const profileQuery = useUserProfileQuery(pubkey);
@@ -239,7 +241,10 @@ export function UserProfilePanel({
{!isOverlay ? (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 z-40 h-[76px] bg-background/80 backdrop-blur-md after:absolute after:left-0 after:right-0 after:top-10 after:h-px after:bg-border/35 supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55"
className={cn(
"pointer-events-none absolute inset-x-0 top-0 z-40 bg-background/80 backdrop-blur-md after:absolute after:left-0 after:right-0 after:top-10 after:h-px after:bg-border/35 supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55",
usesChannelSplitChrome ? "h-[92px]" : "h-[76px]",
)}
/>
) : null}
@@ -250,32 +255,52 @@ export function UserProfilePanel({
? `relative ${PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS} -mb-[76px] min-h-[76px] shrink-0 gap-[10px] bg-transparent pb-[4px] pl-[16px] pr-[8px] pt-[42px] sm:pl-[24px] sm:pr-[12px]`
: isOverlay
? "relative z-50 min-h-[44px] shrink-0 gap-3 bg-background/80 px-3 py-[6px] backdrop-blur-md supports-[backdrop-filter]:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-[backdrop-filter]:bg-background/55"
: "absolute inset-x-0 top-[42px] z-50 min-h-[32px] gap-3 bg-transparent px-3 py-[4px] after:absolute after:bottom-0 after:-left-px after:top-0 after:w-px after:bg-border/45 after:transition-colors peer-hover/profile-resize:after:bg-border/80 peer-focus-visible/profile-resize:after:bg-border/80",
: cn(
"absolute inset-x-0 z-50 bg-transparent after:absolute after:bottom-0 after:-left-px after:top-0 after:w-px after:bg-border/45 after:transition-colors peer-hover/profile-resize:after:bg-border/80 peer-focus-visible/profile-resize:after:bg-border/80",
usesChannelSplitChrome
? "top-[48px] h-[32px] gap-[10px] py-0 pl-[16px] pr-[8px] sm:pr-[12px]"
: "top-[42px] min-h-[32px] gap-3 px-3 py-[4px]",
),
)}
data-tauri-drag-region
>
<div className="flex min-w-0 items-center gap-1.5">
<h2 className="translate-y-px text-sm font-semibold leading-5 tracking-tight">
<h2
className={cn(
"translate-y-px font-semibold tracking-tight",
usesChannelSplitChrome
? "text-base leading-6"
: "text-sm leading-5",
)}
>
Profile
</h2>
</div>
<Button
aria-label="Close profile"
className="ml-auto h-4 w-4 rounded-full text-foreground hover:bg-muted/60 hover:text-foreground"
className={cn(
"ml-auto",
usesChannelSplitChrome
? "h-8 w-8 rounded-lg border border-border/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground [&_svg]:size-5"
: "h-4 w-4 rounded-full text-foreground hover:bg-muted/60 hover:text-foreground",
)}
data-testid="user-profile-panel-close"
onClick={onClose}
size="icon"
type="button"
variant="ghost"
>
<X className="h-2.5 w-2.5" />
<X
className={cn(usesChannelSplitChrome ? "size-5" : "h-2.5 w-2.5")}
/>
</Button>
</div>
<div
className={cn(
"min-h-0 flex-1 overflow-y-auto px-4 pb-6",
!isFloatingOverlay && "pt-[76px]",
!isFloatingOverlay &&
(usesChannelSplitChrome ? "pt-[92px]" : "pt-[76px]"),
)}
>
<div className="flex flex-col items-center gap-4 pt-4">
@@ -328,6 +328,7 @@ export function SidebarSection({
{channel.channelType === "dm" && onHideDm ? (
<SidebarMenuAction
aria-label="Close direct message"
className="right-0 top-1/2 h-8 w-8 -translate-y-1/2 rounded-lg border border-border/40 [&>svg]:size-5 peer-data-[size=default]/menu-button:top-1/2 peer-data-[size=lg]/menu-button:top-1/2 peer-data-[size=sm]/menu-button:top-1/2"
data-testid={`hide-dm-${channel.name}`}
onClick={() => onHideDm(channel.id)}
showOnHover