diff --git a/packages/ui/src/bot-brand-icons.ts b/packages/ui/src/bot-brand-icons.ts new file mode 100644 index 0000000000..2cfdbbd726 --- /dev/null +++ b/packages/ui/src/bot-brand-icons.ts @@ -0,0 +1,50 @@ +/** + * Pre-bundled brand SVG bodies for the IM channels Maka uses for bot + * delivery (Telegram / WeChat / WeCom / Discord / QQ). + * + * Why local instead of `simple-icons:*` runtime CDN fetch: + * The bot logos were rendering through ``, which Iconify lazy-fetches from + * `https://api.iconify.design/...` on first render. On cold-offline + * Electron launches (or when network is firewalled) the bot picker + * would degrade to the `glyph` monogram fallback for the entire + * session. That is wrong end-result: a desktop app's brand logos + * should not depend on a third-party CDN at runtime + * (@kenji audit msg `e4cfbfb0` finding round-2 #2). + * + * Each `body` is a `` string copied verbatim from upstream + * Simple Icons (CC0 1.0 Universal), pinned at the upstream version + * where the icon was last published. The icons are then registered + * under the local `maka-bot:` prefix in `icons.tsx` via + * `addCollection`, so `` renders + * synchronously without any network roundtrip. + * + * Sources (CC0 1.0 Universal — Simple Icons project, simpleicons.org): + * - telegram, wechat, discord : extracted from + * @iconify-json/simple-icons@1.2.87 icons.json (upstream HEAD). + * - tencentqq : extracted from + * @iconify-json/simple-icons@1.2.10 icons.json (Simple Icons + * removed the bare `tencentqq` id and the QQ standalone icon + * afterwards; pinning to 1.2.10 keeps the brand correct). + * + * Still on CDN (no local SVG yet — see kenji audit follow-up note): + * - lark / feishu (飞书), dingtalk (钉钉). Both were never (lark / + * feishu) or no longer (dingtalk) carried by Simple Icons; we will + * need to source those from each brand's official kit before they + * can be fully offline-stable. Until then they fall through to + * the `simple-icons:*` CDN lazy fetch with the `glyph` offline + * fallback, the same way as before this PR. + */ + +export const MAKA_BOT_ICON_BODIES: Record = { + telegram: + '', + wechat: + '', + discord: + '', + qq: + '', +}; + +export const MAKA_BOT_ICON_PREFIX = 'maka-bot'; diff --git a/packages/ui/src/bot-brand.ts b/packages/ui/src/bot-brand.ts index 3bd3e88c11..c24f3703e9 100644 --- a/packages/ui/src/bot-brand.ts +++ b/packages/ui/src/bot-brand.ts @@ -5,7 +5,13 @@ export interface BotBrand { color: string; /** Single-character offline fallback while the remote icon loads. */ glyph: string; - /** Iconify Simple Icons id, lazy-fetched from the Iconify CDN. */ + /** + * Iconify icon id. When the prefix is `maka-bot:*` the icon is + * pre-registered locally (`bot-brand-icons.ts`) and renders + * synchronously with no CDN roundtrip; when the prefix is + * `simple-icons:*` the icon is lazy-fetched from the Iconify CDN at + * runtime — the `glyph` field is the offline fallback for that case. + */ iconifyId: string; /** Optional product-side help link for credential provisioning docs. */ configDocUrl?: string; @@ -15,12 +21,19 @@ export interface BotBrand { // Plan Reminder delivery picker need real brand logos here so the same // channel reads as the same channel everywhere in the product (kenji // audit 2026-06-25 msg `e4cfbfb0` finding #2). +// +// `maka-bot:*` ids are pre-bundled SVG bodies that render offline. +// `simple-icons:*` ids still rely on Iconify's CDN — Simple Icons +// dropped (or never carried) Feishu/Lark and DingTalk standalone +// icons, so until we source them from each brand's official kit they +// fall back to the colored-tile-with-glyph state when offline. Track +// the gap on @kenji audit follow-up. export const BOT_BRAND: Record = { - telegram: { color: '#229ED9', glyph: 'T', iconifyId: 'simple-icons:telegram', configDocUrl: 'https://core.telegram.org/bots/tutorial' }, + telegram: { color: '#229ED9', glyph: 'T', iconifyId: 'maka-bot:telegram', configDocUrl: 'https://core.telegram.org/bots/tutorial' }, feishu: { color: '#00C6B7', glyph: '飞', iconifyId: 'simple-icons:lark', configDocUrl: 'https://open.feishu.cn/document/server-docs/bot-v3' }, - wecom: { color: '#0089FF', glyph: '企', iconifyId: 'simple-icons:wechat', configDocUrl: 'https://developer.work.weixin.qq.com/document/' }, - wechat: { color: '#07C160', glyph: '微', iconifyId: 'simple-icons:wechat', configDocUrl: 'https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html' }, - discord: { color: '#5865F2', glyph: 'D', iconifyId: 'simple-icons:discord', configDocUrl: 'https://discord.com/developers/docs/intro' }, + wecom: { color: '#0089FF', glyph: '企', iconifyId: 'maka-bot:wechat', configDocUrl: 'https://developer.work.weixin.qq.com/document/' }, + wechat: { color: '#07C160', glyph: '微', iconifyId: 'maka-bot:wechat', configDocUrl: 'https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html' }, + discord: { color: '#5865F2', glyph: 'D', iconifyId: 'maka-bot:discord', configDocUrl: 'https://discord.com/developers/docs/intro' }, dingtalk: { color: '#1372FB', glyph: '钉', iconifyId: 'simple-icons:dingtalk', configDocUrl: 'https://open.dingtalk.com/document/' }, - qq: { color: '#EB1923', glyph: 'Q', iconifyId: 'simple-icons:tencentqq', configDocUrl: 'https://bot.q.qq.com/wiki/' }, + qq: { color: '#EB1923', glyph: 'Q', iconifyId: 'maka-bot:qq', configDocUrl: 'https://bot.q.qq.com/wiki/' }, }; diff --git a/packages/ui/src/icons.tsx b/packages/ui/src/icons.tsx index 6d543b34ba..14559c976d 100644 --- a/packages/ui/src/icons.tsx +++ b/packages/ui/src/icons.tsx @@ -33,12 +33,30 @@ import { Icon, addCollection } from '@iconify/react'; import { icons as phData } from '@iconify-json/ph'; +import { + MAKA_BOT_ICON_BODIES, + MAKA_BOT_ICON_PREFIX, +} from './bot-brand-icons.js'; import type { ComponentType } from 'react'; // Register the Phosphor icon collection once at module load so every // `` renders synchronously without a CDN fetch. addCollection(phData); +// Register the local bot brand SVG collection so +// `` etc. resolve synchronously, +// without hitting `api.iconify.design` at runtime. See +// `bot-brand-icons.ts` for the source provenance and why only 4 of the +// 6 bot brands are local today (kenji audit msg `e4cfbfb0` round-2 #2). +addCollection({ + prefix: MAKA_BOT_ICON_PREFIX, + width: 24, + height: 24, + icons: Object.fromEntries( + Object.entries(MAKA_BOT_ICON_BODIES).map(([name, body]) => [name, { body }]), + ), +}); + /** * Re-export of `@iconify/react`'s `` for cases where a caller * needs to render an arbitrary Iconify id (e.g. `simple-icons:wechat`