Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

113 lines
3.0 KiB
TypeScript

"use client";
import type { ReactElement } from "react";
import Image from "next/image";
import Link from "next/link";
import { DownloadIcon, SquareDashedIcon, TypeIcon } from "lucide-react";
import { toast } from "sonner";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { cn } from "@/lib/utils";
type BrandAssetsMenuProps = {
children: ReactElement;
};
type HeaderBrandLinkProps = {
className?: string;
labelClassName?: string;
};
export function HeaderBrandLink({
className,
labelClassName,
}: HeaderBrandLinkProps) {
return (
<BrandAssetsMenu>
<Link
href="/"
className={cn("flex shrink-0 items-center gap-2", className)}
>
<Image
src="/favicon/icon.svg"
alt="assistant-ui logo"
width={18}
height={18}
className="dark:hue-rotate-180 dark:invert"
/>
<span className={cn("font-medium tracking-tight", labelClassName)}>
assistant-ui
</span>
</Link>
</BrandAssetsMenu>
);
}
function BrandAssetsMenu({ children }: BrandAssetsMenuProps) {
return (
<ContextMenu modal={false}>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent className="w-56">
<ContextMenuItem
onSelect={() => void copySvg("Logomark as SVG", "/favicon/icon.svg")}
>
<Image
src="/favicon/icon.svg"
alt=""
width={16}
height={16}
className="size-4 dark:hue-rotate-180 dark:invert"
/>
Copy Logomark as SVG
</ContextMenuItem>
<ContextMenuItem
onSelect={() =>
void copySvg("Logotype as SVG", "/brand/logotype.svg")
}
>
<TypeIcon className="size-4" />
Copy Logotype as SVG
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem asChild>
<Link href="/brand">
<SquareDashedIcon className="size-4" />
Brand Guidelines
</Link>
</ContextMenuItem>
<ContextMenuItem asChild>
<a href="/assistant-ui-brand.zip" download>
<DownloadIcon className="size-4" />
Download Brand Assets
</a>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
// ClipboardItem wraps the pending fetch so the clipboard write starts inside
// the user activation window; Safari rejects a write that awaits the fetch.
const copySvg = async (label: string, path: string) => {
try {
const svg = fetch(path).then(async (response) => {
if (!response.ok) throw new Error("Failed to load SVG");
return new Blob([await response.text()], { type: "text/plain" });
});
await navigator.clipboard.write([new ClipboardItem({ "text/plain": svg })]);
toast.success(`${label} copied`);
} catch {
toast.error(`Failed to copy ${label.toLowerCase()}`);
}
};