mirror of
https://github.com/block/buzz.git
synced 2026-11-05 00:41:28 +00: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 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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user