Style channel header navigation (#889)

This commit is contained in:
klopez4212
2026-06-08 15:03:06 +01:00
committed by GitHub
parent 2ebe551741
commit 29f6ccf9e9
13 changed files with 137 additions and 54 deletions

View File

@@ -55,6 +55,7 @@ export function AgentSessionThreadPanel({
const avatarUrl = profiles?.[agent.pubkey.toLowerCase()]?.avatarUrl ?? null; const avatarUrl = profiles?.[agent.pubkey.toLowerCase()]?.avatarUrl ?? null;
const isOverlay = useIsThreadPanelOverlay(); const isOverlay = useIsThreadPanelOverlay();
const isFloatingOverlay = isOverlay && !isSinglePanelView; const isFloatingOverlay = isOverlay && !isSinglePanelView;
const usesChannelSplitChrome = !isOverlay && !isSinglePanelView;
useEscapeKey(onClose, isOverlay || isSinglePanelView); useEscapeKey(onClose, isOverlay || isSinglePanelView);
const { ref: scrollRef, onScroll } = useStickToBottom<HTMLDivElement>(); const { ref: scrollRef, onScroll } = useStickToBottom<HTMLDivElement>();
@@ -111,7 +112,10 @@ export function AgentSessionThreadPanel({
{!isOverlay ? ( {!isOverlay ? (
<div <div
aria-hidden="true" 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} ) : 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` ? `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 : 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" ? "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 data-tauri-drag-region
> >
@@ -139,7 +143,14 @@ export function AgentSessionThreadPanel({
displayName={agent.name} displayName={agent.name}
size="xs" 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} {agent.name}
</h2> </h2>
</div> </div>
@@ -186,14 +197,22 @@ export function AgentSessionThreadPanel({
) : null} ) : null}
<Button <Button
aria-label="Close activity panel" 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" data-testid="agent-session-close"
onClick={onClose} onClick={onClose}
size="icon" size="icon"
type="button" type="button"
variant="ghost" variant="ghost"
> >
<X className="h-3.5 w-3.5" /> <X
className={cn(
usesChannelSplitChrome ? "size-5" : "h-3.5 w-3.5",
)}
/>
</Button> </Button>
</div> </div>
</div> </div>
@@ -204,10 +223,13 @@ export function AgentSessionThreadPanel({
onScroll={onScroll} onScroll={onScroll}
className={cn( className={cn(
"min-h-0 flex-1 overflow-y-auto px-3 pb-4", "min-h-0 flex-1 overflow-y-auto px-3 pb-4",
// Match MessageThreadPanel: single-panel mode has a 76px header // Single-panel mode keeps the 76px local header; split panes sit
// (min-h-[76px] with -mb-[76px]), so the body must clear it. Only // under the channel screen's 92px top chrome.
// the floating overlay (44px header) uses the smaller pt-4. usesChannelSplitChrome
isSinglePanelView ? "pt-[76px]" : isOverlay ? "pt-4" : "pt-[76px]", ? "pt-[92px]"
: isOverlay
? "pt-4"
: "pt-[76px]",
)} )}
> >
<ManagedAgentSessionPanel <ManagedAgentSessionPanel

View File

@@ -84,10 +84,10 @@ export function ChannelMembersBar({
return ( return (
<React.Fragment> <React.Fragment>
<div className="flex items-center gap-1"> <div className="flex items-center gap-[6px]">
<Button <Button
aria-label="Add agent" 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" data-testid="channel-add-bot-trigger"
disabled={!canAddAgents} disabled={!canAddAgents}
onClick={() => { onClick={() => {
@@ -95,27 +95,27 @@ export function ChannelMembersBar({
}} }}
size="icon" size="icon"
type="button" type="button"
variant="outline" variant="ghost"
> >
<Plus className="h-3 w-3" /> <Plus className="size-5" />
</Button> </Button>
<Button <Button
aria-label={`View channel members (${memberCount})`} 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" data-testid="channel-members-trigger"
onClick={onToggleMembers} onClick={onToggleMembers}
type="button" 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"> <span className="min-w-[1ch] text-[11px] font-medium tabular-nums">
{memberCount} {memberCount}
</span> </span>
</Button> </Button>
<HuddleIndicator <HuddleIndicator
className="h-7 w-7" className="h-8 w-8"
channelId={channel.id} channelId={channel.id}
onStart={async () => { onStart={async () => {
try { try {
@@ -132,14 +132,14 @@ export function ChannelMembersBar({
<Button <Button
aria-label="Manage channel" 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" data-testid="channel-management-trigger"
onClick={onManageChannel} onClick={onManageChannel}
size="icon" size="icon"
type="button" type="button"
variant="outline" variant="ghost"
> >
<Settings2 className="h-3 w-3" /> <Settings2 className="size-5" />
</Button> </Button>
</div> </div>

View File

@@ -44,8 +44,12 @@ import { useChannelFind } from "@/features/search/useChannelFind";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
import { AgentSessionProvider } from "@/shared/context/AgentSessionContext"; import { AgentSessionProvider } from "@/shared/context/AgentSessionContext";
import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext"; import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext";
import { useElementWidthBreakpoint } from "@/shared/hooks/use-mobile";
import { import {
useElementWidthBreakpoint,
useIsThreadPanelOverlay,
} from "@/shared/hooks/use-mobile";
import {
THREAD_PANEL_MIN_WIDTH_PX,
THREAD_PANEL_SINGLE_COLUMN_BREAKPOINT_PX, THREAD_PANEL_SINGLE_COLUMN_BREAKPOINT_PX,
useThreadPanelWidth, useThreadPanelWidth,
} from "@/shared/hooks/useThreadPanelWidth"; } from "@/shared/hooks/useThreadPanelWidth";
@@ -87,6 +91,7 @@ export function ChannelScreen({
widthPx: threadPanelWidthPx, widthPx: threadPanelWidthPx,
} = useThreadPanelWidth(); } = useThreadPanelWidth();
const [isMembersSidebarOpen, setIsMembersSidebarOpen] = React.useState(false); const [isMembersSidebarOpen, setIsMembersSidebarOpen] = React.useState(false);
const isThreadPanelOverlay = useIsThreadPanelOverlay();
const [channelContentRef, isNarrowPanelViewport] = const [channelContentRef, isNarrowPanelViewport] =
useElementWidthBreakpoint<HTMLDivElement>( useElementWidthBreakpoint<HTMLDivElement>(
THREAD_PANEL_SINGLE_COLUMN_BREAKPOINT_PX, THREAD_PANEL_SINGLE_COLUMN_BREAKPOINT_PX,
@@ -435,6 +440,15 @@ export function ChannelScreen({
Boolean( Boolean(
openThreadHeadMessage || openAgentSessionPubkey || profilePanelPubkey, 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 ( return (
<AgentSessionProvider onOpenAgentSession={handleOpenAgentSession}> <AgentSessionProvider onOpenAgentSession={handleOpenAgentSession}>
@@ -443,6 +457,7 @@ export function ChannelScreen({
activeChannel={activeChannel} activeChannel={activeChannel}
activeChannelEphemeralDisplay={activeChannelEphemeralDisplay} activeChannelEphemeralDisplay={activeChannelEphemeralDisplay}
activeChannelTitle={activeChannelTitle} activeChannelTitle={activeChannelTitle}
actionsRightInset={headerActionsRightInset}
activeDmPresenceStatus={activeDmPresenceStatus} activeDmPresenceStatus={activeDmPresenceStatus}
currentPubkey={currentPubkey} currentPubkey={currentPubkey}
isJoining={joinChannelMutation.isPending} isJoining={joinChannelMutation.isPending}

View File

@@ -14,6 +14,7 @@ type ChannelScreenHeaderProps = {
activeChannel: Channel | null; activeChannel: Channel | null;
activeChannelEphemeralDisplay: EphemeralChannelDisplay | null; activeChannelEphemeralDisplay: EphemeralChannelDisplay | null;
activeChannelTitle: string; activeChannelTitle: string;
actionsRightInset?: string;
activeDmPresenceStatus: PresenceStatus | null; activeDmPresenceStatus: PresenceStatus | null;
currentPubkey?: string; currentPubkey?: string;
isJoining?: boolean; isJoining?: boolean;
@@ -27,6 +28,7 @@ export function ChannelScreenHeader({
activeChannel, activeChannel,
activeChannelEphemeralDisplay, activeChannelEphemeralDisplay,
activeChannelTitle, activeChannelTitle,
actionsRightInset,
activeDmPresenceStatus, activeDmPresenceStatus,
currentPubkey, currentPubkey,
isJoining = false, isJoining = false,
@@ -79,10 +81,10 @@ export function ChannelScreenHeader({
return ( return (
<ChatHeader <ChatHeader
actionsPlacement="top-right"
belowSystemChrome belowSystemChrome
density="compact" density="compact"
actions={actions} actions={actions}
actionsRightInset={actionsRightInset}
channelType={activeChannel?.channelType} channelType={activeChannel?.channelType}
description={getChannelDescription(activeChannel)} description={getChannelDescription(activeChannel)}
statusBadge={ statusBadge={

View File

@@ -19,6 +19,7 @@ import { cn } from "@/shared/lib/cn";
type ChatHeaderProps = { type ChatHeaderProps = {
actions?: React.ReactNode; actions?: React.ReactNode;
actionsPlacement?: "inline" | "top-right"; actionsPlacement?: "inline" | "top-right";
actionsRightInset?: string;
belowSystemChrome?: boolean; belowSystemChrome?: boolean;
density?: "default" | "compact"; density?: "default" | "compact";
title: string; title: string;
@@ -80,6 +81,7 @@ function ChannelIcon({
export function ChatHeader({ export function ChatHeader({
actions, actions,
actionsPlacement = "inline", actionsPlacement = "inline",
actionsRightInset,
belowSystemChrome = false, belowSystemChrome = false,
density = "default", density = "default",
title, title,
@@ -101,10 +103,8 @@ export function ChatHeader({
const header = ( const header = (
<header <header
className={cn( 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]", "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" density === "compact" ? "h-[32px] py-0" : "min-h-[44px] py-[6px]",
? "min-h-[32px] py-[4px]"
: "min-h-[44px] py-[6px]",
overlaysContent && !belowSystemChrome && "-mb-[44px]", overlaysContent && !belowSystemChrome && "-mb-[44px]",
)} )}
data-testid="chat-header" data-testid="chat-header"
@@ -118,7 +118,7 @@ export function ChatHeader({
visibility={visibility} visibility={visibility}
/> />
<h1 <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" data-testid="chat-title"
title={trimmedDescription || undefined} title={trimmedDescription || undefined}
> >
@@ -137,7 +137,12 @@ export function ChatHeader({
createPortal(topRightActions, document.body) 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 /> <UpdateIndicator />
{actions ? <div className="shrink-0">{actions}</div> : null} {actions ? <div className="shrink-0">{actions}</div> : null}
</div> </div>
@@ -150,7 +155,7 @@ export function ChatHeader({
} }
return ( 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} {header}
</div> </div>
); );

View File

@@ -230,15 +230,18 @@ export function HuddleIndicator({
<> <>
<Button <Button
aria-label="Start huddle" 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" data-testid="channel-start-huddle-trigger"
disabled={startDisabled || isStarting} disabled={startDisabled || isStarting}
onClick={() => headphonesGate.gate(() => onStart())} onClick={() => headphonesGate.gate(() => onStart())}
size="icon" size="icon"
type="button" type="button"
variant="outline" variant="ghost"
> >
<Headphones className="h-3 w-3" /> <Headphones className="size-5" />
</Button> </Button>
{gateDialog} {gateDialog}
</> </>
@@ -270,15 +273,18 @@ export function HuddleIndicator({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
aria-label={`Join active huddle (${participantCount} participant${participantCount !== 1 ? "s" : ""})`} 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} disabled={isJoining || isStarting}
onClick={() => headphonesGate.gate(() => void doJoin())} onClick={() => headphonesGate.gate(() => void doJoin())}
size="icon" size="icon"
type="button" type="button"
variant="outline" variant="ghost"
> >
<Headphones className="h-3 w-3 text-muted-foreground" /> <Headphones className="size-5 text-muted-foreground" />
<span className="absolute inset-0 animate-pulse rounded-full ring-2 ring-border/70" /> <span className="absolute inset-0 animate-pulse rounded-lg ring-2 ring-border/70" />
{/* Participant count badge */} {/* Participant count badge */}
{participantCount > 0 && ( {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"> <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">

View File

@@ -2,11 +2,11 @@ export function DayDivider({ label }: { label: string }) {
return ( return (
<section <section
aria-label={label} 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-testid="message-timeline-day-divider"
data-day-label={label} 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} {label}
</p> </p>
</section> </section>

View File

@@ -12,7 +12,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
const REACTION_PILL_BASE_CLASSES = const REACTION_PILL_BASE_CLASSES =
"inline-flex h-8 items-center rounded-full border text-xs font-medium leading-none transition-colors"; "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 = 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"; "hover:bg-primary/10 hover:text-foreground focus-visible:bg-primary/10 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring";

View File

@@ -127,6 +127,7 @@ export function MessageThreadPanel({
const threadComposerWrapperRef = React.useRef<HTMLDivElement>(null); const threadComposerWrapperRef = React.useRef<HTMLDivElement>(null);
const isOverlay = useIsThreadPanelOverlay(); const isOverlay = useIsThreadPanelOverlay();
const isFloatingOverlay = isOverlay && !isSinglePanelView; const isFloatingOverlay = isOverlay && !isSinglePanelView;
const usesChannelSplitChrome = !isOverlay && !isSinglePanelView;
useEscapeKey(onClose, isOverlay || isSinglePanelView); useEscapeKey(onClose, isOverlay || isSinglePanelView);
useComposerHeightPadding( useComposerHeightPadding(
threadBodyRef, threadBodyRef,
@@ -207,7 +208,10 @@ export function MessageThreadPanel({
{!isOverlay ? ( {!isOverlay ? (
<div <div
aria-hidden="true" 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} ) : null}
@@ -215,10 +219,10 @@ export function MessageThreadPanel({
className={cn( className={cn(
"flex cursor-default select-none items-center", "flex cursor-default select-none items-center",
isSinglePanelView 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 : 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" ? "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 data-tauri-drag-region
> >
@@ -243,27 +247,27 @@ export function MessageThreadPanel({
</Button> </Button>
</div> </div>
) : null} ) : 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 Thread
</h2> </h2>
</div> </div>
<Button <Button
aria-label="Close thread" 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" data-testid="message-thread-close"
onClick={onClose} onClick={onClose}
size="icon" size="icon"
type="button" type="button"
variant="ghost" variant="ghost"
> >
<X className="h-2.5 w-2.5" /> <X className="size-5" />
</Button> </Button>
</div> </div>
<div <div
className={cn( className={cn(
"min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain pb-24 [overflow-anchor:none]", "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" data-testid="message-thread-body"
onScroll={syncScrollState} onScroll={syncScrollState}

View File

@@ -148,7 +148,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
ref={scrollContainerRef} ref={scrollContainerRef}
> >
<div <div
className="flex w-full flex-col gap-2 pt-[76px]" className="flex w-full flex-col gap-2 pt-[92px]"
ref={contentRef} ref={contentRef}
> >
<div ref={topSentinelRef} aria-hidden className="h-px" /> <div ref={topSentinelRef} aria-hidden className="h-px" />

View File

@@ -189,7 +189,10 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
} }
return dayGroups.map((group) => ( 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} /> <DayDivider label={group.label} />
{group.elements} {group.elements}
</section> </section>

View File

@@ -106,6 +106,8 @@ export function UserProfilePanel({
}: UserProfilePanelProps) { }: UserProfilePanelProps) {
const isOverlay = useIsThreadPanelOverlay(); const isOverlay = useIsThreadPanelOverlay();
const isFloatingOverlay = isOverlay && !isSinglePanelView; const isFloatingOverlay = isOverlay && !isSinglePanelView;
const usesChannelSplitChrome =
splitPaneClamp && !isOverlay && !isSinglePanelView;
useEscapeKey(onClose, isOverlay || isSinglePanelView); useEscapeKey(onClose, isOverlay || isSinglePanelView);
const profileQuery = useUserProfileQuery(pubkey); const profileQuery = useUserProfileQuery(pubkey);
@@ -239,7 +241,10 @@ export function UserProfilePanel({
{!isOverlay ? ( {!isOverlay ? (
<div <div
aria-hidden="true" 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} ) : 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]` ? `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 : 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" ? "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 data-tauri-drag-region
> >
<div className="flex min-w-0 items-center gap-1.5"> <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 Profile
</h2> </h2>
</div> </div>
<Button <Button
aria-label="Close profile" 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" data-testid="user-profile-panel-close"
onClick={onClose} onClick={onClose}
size="icon" size="icon"
type="button" type="button"
variant="ghost" variant="ghost"
> >
<X className="h-2.5 w-2.5" /> <X
className={cn(usesChannelSplitChrome ? "size-5" : "h-2.5 w-2.5")}
/>
</Button> </Button>
</div> </div>
<div <div
className={cn( className={cn(
"min-h-0 flex-1 overflow-y-auto px-4 pb-6", "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"> <div className="flex flex-col items-center gap-4 pt-4">

View File

@@ -328,6 +328,7 @@ export function SidebarSection({
{channel.channelType === "dm" && onHideDm ? ( {channel.channelType === "dm" && onHideDm ? (
<SidebarMenuAction <SidebarMenuAction
aria-label="Close direct message" 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}`} data-testid={`hide-dm-${channel.name}`}
onClick={() => onHideDm(channel.id)} onClick={() => onHideDm(channel.id)}
showOnHover showOnHover