diff --git a/apps/web/src/components/contextChipParts.tsx b/apps/web/src/components/contextChipParts.tsx index 55d66063b13e..b3202675de16 100644 --- a/apps/web/src/components/contextChipParts.tsx +++ b/apps/web/src/components/contextChipParts.tsx @@ -1,6 +1,12 @@ import type { PullRequestContextMetadata } from "@t3tools/contracts"; import { CircleDashedIcon, FilmIcon, GitPullRequestIcon, ImageIcon } from "lucide-react"; -import type { ComponentProps, MouseEvent, ReactNode } from "react"; +import { + useState, + type ComponentProps, + type CSSProperties, + type MouseEvent, + type ReactNode, +} from "react"; import { cn } from "~/lib/utils"; import { PierreEntryIcon } from "./chat/PierreEntryIcon"; @@ -139,6 +145,34 @@ export function PullRequestChip(props: { ); } +/** Sample the loaded thumbnail once; transparent pixels should not darken its accent. */ +function averageImageColor(image: HTMLImageElement): string | undefined { + try { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = 16; + const context = canvas.getContext("2d"); + if (!context) return; + context.drawImage(image, 0, 0, 16, 16); + const { data } = context.getImageData(0, 0, 16, 16); + let red = 0; + let green = 0; + let blue = 0; + let alpha = 0; + for (let index = 0; index < data.length; index += 4) { + const weight = data[index + 3]!; + red += data[index]! * weight; + green += data[index + 1]! * weight; + blue += data[index + 2]! * weight; + alpha += weight; + } + if (alpha === 0) return; + return `rgb(${Math.round(red / alpha)} ${Math.round(green / alpha)} ${Math.round(blue / alpha)})`; + } catch { + // Cross-origin or unavailable pixels keep the default image tone and preview action. + return; + } +} + export function ImageChipButton({ name, previewUrl, @@ -146,6 +180,7 @@ export function ImageChipButton({ labelClassName, size, suffix, + style, ...props }: ComponentProps<"button"> & { name: string; @@ -155,6 +190,9 @@ export function ImageChipButton({ size: string; suffix?: string | null; }) { + const [sample, setSample] = useState<{ url: string; color: string | undefined }>(); + const [corsFailedUrl, setCorsFailedUrl] = useState(); + const accent = sample?.url === previewUrl ? sample?.color : undefined; return (