Порівняти коміти

...
Автор SHA1 Повідомлення Дата
l0ng-ai 4189dc9b83 feat(ui): small raster data-URI images as icons
Plugin view items and toolbar buttons could only show IDE icon names, so a
review or CI plugin could not show avatars or pipeline status badges in its
tree or list.

`Icon` now draws a raster `data:` URI (PNG, JPEG, GIF or WebP, base64, at
most 64 KiB) as an image of the icon's size; anything else keeps the icon
name lookup and its file-icon fallback. The plugin fetches the image with its
own `net` grant and passes it inline, so the UI never loads a URL for an icon,
and hosts without this show the generic file icon. Documented in the views
guide.
2026-11-15 03:25:27 +08:00
5 змінених файлів з 71 додано та 2 видалено

No files matched your search

+5
Переглянути файл
@@ -136,6 +136,11 @@ buttons can act on what the user is editing.
Toolbar buttons: `View::new(body).button(Button::new("refresh", "Refresh").icon("refresh"))`
and `.status("12 items")`.
An item or button `icon` is an IDE icon name, a registered plugin icon, or a small raster image as a `data:` URI
(`data:image/png;base64,…`; PNG, JPEG, GIF or WebP, at most 64 KiB), drawn at the icon's size: an avatar or a pipeline
status badge. Fetch the image yourself (a `net` grant) and pass it inline; the IDE never loads URLs for an icon. Older
hosts show the generic file icon in its place.
### Events
`ViewEvent` carries `Button(id)`, `Activate(key)` (double click or Enter), `Select(key)`,
+2
Переглянути файл
@@ -126,6 +126,8 @@ register_plugin!(TodoFinder, views);
工具栏按钮:`View::new(body).button(Button::new("refresh", "Refresh").icon("refresh"))`,状态行:`.status("12 items")`。
条目和按钮的 `icon` 可以是 IDE 图标名、已注册的插件图标,或者以 `data:` URI 给出的小尺寸位图(`data:image/png;base64,…`,支持 PNG、JPEG、GIF、WebP,最大 64 KiB),按图标尺寸绘制,适合头像或流水线状态徽章。图片由插件自己获取(需要 `net` 授权)后内联传入,IDE 不会为图标加载 URL。较旧的宿主会在该位置显示通用文件图标。
### 事件
`ViewEvent` 包括 `Button(id)`、`Activate(key)`(双击或回车)、`Select(key)`、`Expand(key)`、`Collapse(key)`、
+5
Переглянути файл
@@ -3,6 +3,11 @@
display: block;
}
.image {
object-fit: cover;
border-radius: var(--radius-small);
}
.mono {
color: var(--icon-color, currentColor);
}
+27
Переглянути файл
@@ -284,3 +284,30 @@ describe("fileIconName", () => {
}
});
});
describe("image icons", () => {
const png =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
it("renders a raster data URI as an image of the icon's size", () => {
const { container } = render(<Icon name={png} size={14} />);
const img = container.querySelector("img")!;
expect(img.getAttribute("src")).toBe(png);
expect([img.width, img.height]).toEqual([14, 14]);
expect(container.querySelector("svg")).toBeNull();
});
it("falls back to the file icon for other URIs and oversized images", () => {
for (const name of [
"https://example.com/avatar.png",
"data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=",
`data:image/png;base64,${"A".repeat(70 * 1024)}`,
"data:image/png;base64,not base64!",
]) {
const { container, unmount } = render(<Icon name={name} />);
expect(container.querySelector("img")).toBeNull();
expect(container.querySelector("svg")).toBeTruthy();
unmount();
}
});
});
+32 -2
Переглянути файл
@@ -816,8 +816,19 @@ const withIds = (p: Prepared, prefix: string) => (p.hasIds ? p.html.replace(/__I
// ---------------------------------------------------------------------------
/** Largest `data:` image accepted as an icon (an avatar or a status badge a plugin fetched). */
const MAX_IMAGE_ICON = 64 * 1024;
/** A small raster image given as a `data:` URI (PNG, JPEG, GIF, WebP), shown in place of an icon. */
function isImageIcon(name: string): boolean {
return name.length <= MAX_IMAGE_ICON && /^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+=*$/.test(name);
}
export interface IconProps {
/** Icon name; unknown names render the generic file icon. `symbol-<kind>` renders a symbol node icon. */
/**
* Icon name; unknown names render the generic file icon. `symbol-<kind>` renders a symbol node icon,
* and a raster `data:` image URI (see `isImageIcon`) renders that image.
*/
name: IconName | (string & {});
size?: number;
className?: string;
@@ -826,7 +837,26 @@ export interface IconProps {
label?: string;
}
export function Icon({ name, size = 16, className, style, label }: IconProps) {
export function Icon(props: IconProps) {
return isImageIcon(props.name) ? <ImageIcon {...props} /> : <SvgIcon {...props} />;
}
function ImageIcon({ name, size = 16, className, style, label }: IconProps) {
return (
<img
className={clsx(styles.icon, styles.image, className)}
style={style}
src={name}
width={size}
height={size}
alt={label ?? ""}
aria-hidden={label ? undefined : true}
draggable={false}
/>
);
}
function SvgIcon({ name, size = 16, className, style, label }: IconProps) {
useFileIconRevision();
const uid = useId();
const path = iconPath(name);