/**
* Shared UI primitives for AgentHub HTML pages.
*
* Constraints:
* - Dependency-free: only string/template helpers, no npm UI libs.
* - No emojis: inline SVG/CSS only.
* - Self-contained: pages import this and inline the returned CSS/JS.
*/
import { TaskStatus as TaskStatusSchema } from '../core/schema.js';
type TaskStatus = 'open' | 'in_progress' | 'review' | 'done' | 'cancelled';
/** CSS variables block matching the AgentHub dark design spec. */
export function designTokensCss(): string {
return `
:root {
--bg: #0F172A;
--surface: #161B22;
--raised: #1E293B;
--border: #30363D;
--text: #F8FAFC;
--muted: #94A3B8;
--accent: #58A6FF;
--green: #22C55E;
--status-open: #8B949E;
--status-in_progress: #58A6FF;
--status-review: #D29922;
--status-done: #22C55E;
--status-cancelled: #6E7681;
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
background: var(--bg);
color: var(--text);
font: 14px/1.5 var(--font-sans);
padding: 16px 20px 32px;
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
`;
}
const AGENT_PALETTE: Record = {
claude: { color: '#D97757', initial: 'C' },
codex: { color: '#10A37F', initial: 'Cx' },
kimi: { color: '#7C3AED', initial: 'K' },
'windows-claude': { color: '#2563EB', initial: 'W' },
backyard: { color: '#64748B', initial: 'B' },
};
function hashString(str: string): number {
let h = 0;
for (let i = 0; i < str.length; i++) {
h = (h << 5) - h + str.charCodeAt(i);
h |= 0;
}
return Math.abs(h);
}
function deterministicColor(name: string): string {
const colors = ['#DC2626', '#EA580C', '#D97706', '#65A30D', '#0891B2', '#2563EB', '#7C3AED', '#DB2777'];
return colors[hashString(name) % colors.length];
}
export interface AgentAvatarOptions {
/** Render an extra ring for the architect role. */
architectRing?: boolean;
size?: number;
}
/**
* Render a round agent avatar chip with per-agent accent color and initials.
* No external images, no emojis.
*/
export function agentAvatar(
name: string | undefined,
options: AgentAvatarOptions = {},
): string {
const resolvedName = name?.toLowerCase() ?? '';
const spec = AGENT_PALETTE[resolvedName] ?? {
color: deterministicColor(resolvedName || 'unknown'),
initial: (name ?? '?').slice(0, 1).toUpperCase(),
};
const { architectRing = false, size = 22 } = options;
const fontSize = Math.round(size * 0.45);
const ring = architectRing
? `box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px ${spec.color};`
: '';
return `${escapeHtml(spec.initial)}`;
}
/**
* Provider/company brand metadata + official Simple Icons SVG paths (verified,
* 24x24 viewBox). Used to group + badge agents by the model behind them.
*/
const PROVIDERS: Record = {
anthropic: {
name: 'Anthropic',
color: '#D97757',
path: 'M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z',
},
openai: {
name: 'OpenAI',
color: '#10A37F',
path: 'M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z',
},
moonshot: {
name: 'Moonshot',
color: '#7C3AED',
path: 'm1.053 16.91 9.538 2.55a21 20.981 0 0 0 .06 2.031l5.956 1.592a12 11.99 0 0 1-15.554-6.172m-1.02-5.79 11.352 3.035a21 20.981 0 0 0-.469 2.01l10.817 2.89a12 11.99 0 0 1-1.845 2.004L.658 15.918a12 11.99 0 0 1-.625-4.796m1.593-5.146L13.573 9.17a21 20.981 0 0 0-1.01 1.874l11.297 3.02a21 20.981 0 0 1-.67 2.362l-11.55-3.087L.125 10.26a12 11.99 0 0 1 1.499-4.285ZM6.067 1.58l11.285 3.016a21 20.981 0 0 0-1.688 1.719l7.824 2.091a21 20.981 0 0 1 .513 2.664L2.107 5.218a12 11.99 0 0 1 3.96-3.638M21.68 4.866 7.222 1.003A12 11.99 0 0 1 21.68 4.866',
},
};
/** Brand metadata for a provider kind, or null if unknown. */
export function providerMeta(kind?: string): { name: string; color: string } | null {
if (!kind) return null;
const p = PROVIDERS[kind];
return p ? { name: p.name, color: p.color } : null;
}
/** Inline brand logo SVG for a provider kind, tinted with its brand color. */
export function providerLogo(kind: string | undefined, size = 20): string {
const p = kind ? PROVIDERS[kind] : undefined;
if (!p) return '';
return ``;
}
/** Inline status pill for a task status. */
export function statusPill(status: TaskStatus): string {
const labels: Record = {
open: 'Open',
in_progress: 'In Progress',
review: 'Review',
done: 'Done',
cancelled: 'Cancelled',
};
const colorVar = `--status-${status}`;
return `${escapeHtml(labels[status])}`;
}
/**
* CSS snippet that injects RGB versions of status colors so statusPill can use
* rgba() backgrounds/borders. Include this once in the page