open-design/apps/web/src/components/AgentIcon.tsx
whincwu d4a430f004 feat(web): add CodeBuddy Code agent logo
Add the CodeBuddy brand icon (purple rounded square with white face
mark) as a color SVG asset and register it in the AgentIcon component.
2026-05-30 17:11:20 +08:00

108 lines
2.9 KiB
TypeScript

import type { CSSProperties } from 'react';
interface Props {
id: string;
size?: number;
className?: string;
}
// Agents that ship a bundled brand asset under `apps/web/public/agent-icons/`.
// SVG is preferred (resolution-independent, single file ≤ a few KB); PNG is
// the fallback for vendors that don't publish an SVG mark anywhere (Devin
// only ships a rasterised icon on devin.ai). New brand: drop the optimised
// file in that folder and add the id here.
const ICON_EXT: Record<string, 'svg' | 'png'> = {
amr: 'svg',
claude: 'svg',
codebuddy: 'svg',
codex: 'svg',
gemini: 'svg',
opencode: 'svg',
'cursor-agent': 'svg',
copilot: 'svg',
qwen: 'svg',
qoder: 'svg',
deepseek: 'svg',
reasonix: 'svg',
mimo: 'svg',
hermes: 'svg',
'grok-build': 'svg',
kimi: 'svg',
pi: 'svg',
kiro: 'svg',
kilo: 'svg',
vibe: 'svg',
antigravity: 'svg',
aider: 'png',
'trae-cli': 'png',
devin: 'png',
};
// SVG marks that are single-color silhouettes (no baked brand colors).
// Rendered as a CSS-masked `<span>` so `background-color: currentColor`
// can paint them in whatever text color the surrounding theme resolves
// to — light text under dark theme, dark text under light theme. The
// SVG file itself uses an explicit dark fill (`#1c1b1a`, baked) instead
// of `currentColor`, so if anything outside this component ever loads
// the asset through `<img>` it still renders as a legible dark mark
// rather than collapsing to the SVG document's default black-on-…-black.
const MONO_ICONS = new Set([
'cursor-agent',
'opencode',
'hermes',
'mimo',
'kilo',
'grok-build',
]);
export function AgentIcon({ id, size = 36, className }: Props) {
const cls = 'agent-icon' + (className ? ' ' + className : '');
const ext = ICON_EXT[id];
if (ext) {
if (ext === 'svg' && MONO_ICONS.has(id)) {
const src = `/agent-icons/${id}.svg`;
const style: CSSProperties = {
width: size,
height: size,
WebkitMaskImage: `url("${src}")`,
maskImage: `url("${src}")`,
};
return (
<span
className={cls + ' agent-icon-mono'}
style={style}
aria-hidden="true"
/>
);
}
return (
<img
src={`/agent-icons/${id}.${ext}`}
alt=""
width={size}
height={size}
className={cls}
aria-hidden="true"
draggable={false}
/>
);
}
// Fallback for brands we don't ship artwork for. A neutral rounded
// square with the initial letter — reads as "no official mark yet"
// without inventing brand artwork we can't license.
const initial = (id.match(/[a-z]/i)?.[0] ?? '?').toUpperCase();
return (
<span
className={cls + ' agent-icon-fallback'}
style={{
width: size,
height: size,
fontSize: Math.round(size * 0.42),
lineHeight: 1,
}}
aria-hidden="true"
>
{initial}
</span>
);
}