mirror of
https://github.com/block/buzz.git
synced 2026-11-02 23:41:08 +00:00
Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
import {
|
|
Activity,
|
|
Bot,
|
|
CircleDot,
|
|
Copy,
|
|
FileText,
|
|
FolderGit2,
|
|
Hash,
|
|
House,
|
|
Lock,
|
|
Zap,
|
|
} from "lucide-react";
|
|
import type * as React from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import type { ChannelType, ChannelVisibility } from "@/shared/api/types";
|
|
import { UpdateIndicator } from "@/features/settings/UpdateIndicator";
|
|
import { cn } from "@/shared/lib/cn";
|
|
import { channelChrome } from "@/shared/layout/chromeLayout";
|
|
import { Button } from "@/shared/ui/button";
|
|
import { writeTextToClipboard } from "@/shared/lib/clipboard";
|
|
|
|
type ChatHeaderProps = {
|
|
actions?: React.ReactNode;
|
|
belowSystemChrome?: boolean;
|
|
/** Ref to the outer chrome wrapper when `belowSystemChrome` is true. */
|
|
chromeWrapperRef?: React.Ref<HTMLDivElement>;
|
|
title: string;
|
|
description?: string;
|
|
channelType?: ChannelType;
|
|
visibility?: ChannelVisibility;
|
|
leadingContent?: React.ReactNode;
|
|
mode?: "home" | "channel" | "agents" | "workflows" | "pulse" | "projects";
|
|
overlaysContent?: boolean;
|
|
statusBadge?: React.ReactNode;
|
|
/** Render the chrome wrapper without an individual backdrop when a parent supplies shared blur. */
|
|
transparentChrome?: boolean;
|
|
};
|
|
|
|
const HEADER_ICON_CLASS = "h-4 w-4 text-muted-foreground";
|
|
const CHANNEL_HASH_ICON_CLASS = "h-4 w-4 translate-y-px";
|
|
|
|
function ChannelIcon({
|
|
channelType,
|
|
visibility,
|
|
mode = "channel",
|
|
}: {
|
|
channelType?: ChannelType;
|
|
visibility?: ChannelVisibility;
|
|
mode?: "home" | "channel" | "agents" | "workflows" | "pulse" | "projects";
|
|
}) {
|
|
if (mode === "home") {
|
|
return <House className={HEADER_ICON_CLASS} />;
|
|
}
|
|
|
|
if (mode === "agents") {
|
|
return <Bot className={HEADER_ICON_CLASS} />;
|
|
}
|
|
|
|
if (mode === "workflows") {
|
|
return <Zap className={HEADER_ICON_CLASS} />;
|
|
}
|
|
|
|
if (mode === "pulse") {
|
|
return <Activity className={HEADER_ICON_CLASS} />;
|
|
}
|
|
|
|
if (mode === "projects") {
|
|
return <FolderGit2 className={HEADER_ICON_CLASS} />;
|
|
}
|
|
|
|
if (channelType === "dm") {
|
|
return <CircleDot className={HEADER_ICON_CLASS} />;
|
|
}
|
|
|
|
if (visibility === "private") {
|
|
return <Lock className={HEADER_ICON_CLASS} />;
|
|
}
|
|
|
|
if (channelType === "forum") {
|
|
return <FileText className={HEADER_ICON_CLASS} />;
|
|
}
|
|
|
|
return <Hash className={CHANNEL_HASH_ICON_CLASS} color="gray" />;
|
|
}
|
|
|
|
export function ChatHeader({
|
|
actions,
|
|
belowSystemChrome = false,
|
|
chromeWrapperRef,
|
|
title,
|
|
description,
|
|
channelType,
|
|
visibility,
|
|
leadingContent,
|
|
mode = "channel",
|
|
overlaysContent = false,
|
|
statusBadge,
|
|
transparentChrome = false,
|
|
}: ChatHeaderProps) {
|
|
const trimmedDescription = description?.trim() ?? "";
|
|
|
|
async function handleCopyTitle() {
|
|
const value = title.trim();
|
|
if (!value) return;
|
|
|
|
try {
|
|
await writeTextToClipboard(value);
|
|
toast.success("Channel name copied");
|
|
} catch {
|
|
toast.error("Failed to copy channel name");
|
|
}
|
|
}
|
|
|
|
const header = (
|
|
<header
|
|
className={cn(
|
|
"pointer-events-auto relative z-30 min-w-0 shrink-0 cursor-default select-none bg-transparent px-5 py-2 transition-[margin,padding] duration-200 ease-linear",
|
|
overlaysContent && !belowSystemChrome && "-mb-14",
|
|
)}
|
|
data-testid="chat-header"
|
|
data-tauri-drag-region
|
|
>
|
|
<div className="flex h-9 min-w-0 items-center gap-2.5">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="group/title flex min-w-0 items-center gap-[4px] overflow-hidden">
|
|
<div className="flex shrink-0 items-center">
|
|
{leadingContent ?? (
|
|
<ChannelIcon
|
|
channelType={channelType}
|
|
mode={mode}
|
|
visibility={visibility}
|
|
/>
|
|
)}
|
|
</div>
|
|
<h1
|
|
className={cn(
|
|
"min-w-0 truncate text-base font-semibold leading-6 tracking-tight",
|
|
channelType !== "dm" && "translate-y-px",
|
|
)}
|
|
data-testid="chat-title"
|
|
title={trimmedDescription || undefined}
|
|
>
|
|
{title}
|
|
</h1>
|
|
<Button
|
|
aria-label={`Copy channel name: ${title}`}
|
|
className="h-6 w-6 shrink-0 opacity-0 text-muted-foreground transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/title:opacity-100"
|
|
onClick={() => void handleCopyTitle()}
|
|
size="icon-xs"
|
|
title="Copy channel name"
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
<Copy className="h-3.5 w-3.5" />
|
|
</Button>
|
|
{statusBadge ? (
|
|
<div className="flex shrink-0 flex-wrap items-center gap-1">
|
|
{statusBadge}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex shrink-0 items-center gap-1">
|
|
<UpdateIndicator />
|
|
{actions ? <div className="shrink-0">{actions}</div> : null}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
|
|
if (!belowSystemChrome) {
|
|
return header;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={chromeWrapperRef}
|
|
className={cn(
|
|
"pointer-events-none relative z-40 overflow-visible rounded-tl-xl",
|
|
transparentChrome
|
|
? "bg-transparent"
|
|
: "bg-background/80 backdrop-blur-md supports-backdrop-filter:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55",
|
|
channelChrome.negativeMargin,
|
|
)}
|
|
>
|
|
{header}
|
|
</div>
|
|
);
|
|
}
|