8fb703c6db
- Contrast: --fg-muted reaches 4.5:1 on every surface in all nine themes (dark default was 2.55:1); status chips and accent text clear 4.5:1 on their own tints. New --accent-solid(/-hover) + tailwind `accent-solid` is the fill behind white text (.btn-primary, Switch on, Checkbox tick); --accent is now a text colour. Switch off / unchecked Checkbox get a >=3:1 edge. - <html lang> follows the chosen language; a global prefers-reduced-motion rule; index.html body uses a token instead of text-gray-200. - Layout: the sidebar starts collapsed below lg and becomes an off-canvas drawer below md (menu button, backdrop, Escape, useDialog). - ContextMenu: focus moves in, arrows/Home/End, Escape/Tab close, focus returns, a keyboard-opened menu anchors to its target. Select is a real listbox button (aria-expanded, activedescendant, type-ahead). PageTabs implements APG tabs with a tabpanel. New .btn-secondary/.btn-danger/ .btn-icon and a .select class for native selects, all with focus rings. - StatusBar announces connection and "N need you" politely; Sidebar's connection modal uses useDialog; EmptyState/Skeleton/AiToolIcon a11y. - api.ts request(): a non-JSON 200 throws a translated EBADRESPONSE instead of "Unexpected token '<'".
57 lines
1.7 KiB
TypeScript
57 lines
1.7 KiB
TypeScript
/**
|
|
* @file AiToolIcon.tsx
|
|
* @description An AI tool's icon: the mark shipped in `client/public/ai-tools/`
|
|
* when there is one, else
|
|
* the server's cached favicon (GET /api/ai-tools/:id/icon), else the label's
|
|
* first letter. Every tool icon in the app renders through here.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { api, type AiTool } from "../lib/api";
|
|
|
|
/** Shipped marks by tool id. */
|
|
const BUNDLED: Record<string, string> = {
|
|
claude: "/ai-tools/claude.svg",
|
|
codex: "/ai-tools/codex.svg",
|
|
gemini: "/ai-tools/gemini.svg",
|
|
openclaw: "/ai-tools/openclaw.svg",
|
|
hermes: "/ai-tools/hermes.svg",
|
|
};
|
|
|
|
/**
|
|
* @param props.tool - The catalog entry.
|
|
* @param props.className - Size classes (default `h-8 w-8`).
|
|
*/
|
|
export function AiToolIcon({
|
|
tool,
|
|
className = "h-8 w-8",
|
|
}: {
|
|
tool: Pick<AiTool, "id" | "label">;
|
|
className?: string;
|
|
}) {
|
|
// Keyed by id: one instance showing another tool must retry its icon.
|
|
const [failedId, setFailedId] = useState<string | null>(null);
|
|
const box = `${className} shrink-0 rounded-md bg-surface-4 object-contain p-1`;
|
|
const bundled = BUNDLED[tool.id];
|
|
if (bundled) return <img src={bundled} alt="" className={box} />;
|
|
if (failedId !== tool.id) {
|
|
return (
|
|
<img
|
|
src={api.aiTools.iconUrl(tool.id)}
|
|
alt=""
|
|
onError={() => setFailedId(tool.id)}
|
|
className={box}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<span
|
|
aria-hidden
|
|
className={`${className} flex shrink-0 items-center justify-center rounded-md bg-surface-4 text-xs font-semibold text-fg-secondary`}
|
|
>
|
|
{tool.label.charAt(0).toUpperCase()}
|
|
</span>
|
|
);
|
|
}
|