mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Style channel header navigation (#889)
This commit is contained in:
@@ -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={
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user