🐛 fix(brand): 统一图标资源并适配 macOS Dock

- 将 40 个重复 PNG 变体收敛为 10 个 WebP 品牌资源
- 品牌选择器、关于页、favicon 与 Dock 统一复用所选图标
- 按 824px 安全区和 184px 标准圆角合成 macOS Dock 图标
- 补充资源映射、品牌展示和 Dock 合成回归测试
This commit is contained in:
Syngnat
2026-07-16 11:16:47 +08:00
parent 823fd54ef0
commit ac4b520295
58 changed files with 196 additions and 112 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -1,5 +1,5 @@
{
"version": 8,
"version": 9,
"defaultId": "02",
"icons": [
{
@@ -7,80 +7,80 @@
"slug": "database-hug",
"titleZh": "抱库小狗",
"titleEn": "Database hug",
"iconPath": "/brand-icons/01-database-hug.png",
"fullPath": "/brand-icons/01-database-hug-full.png"
"iconPath": "/brand-icons/01-database-hug.webp",
"fullPath": "/brand-icons/01-database-hug.webp"
},
{
"id": "02",
"slug": "database-search",
"titleZh": "搜库小狗",
"titleEn": "Database search",
"iconPath": "/brand-icons/02-database-search.png",
"fullPath": "/brand-icons/02-database-search-full.png"
"iconPath": "/brand-icons/02-database-search.webp",
"fullPath": "/brand-icons/02-database-search.webp"
},
{
"id": "03",
"slug": "bandana-badge",
"titleZh": "头巾徽章",
"titleEn": "Bandana badge",
"iconPath": "/brand-icons/03-bandana-badge.png",
"fullPath": "/brand-icons/03-bandana-badge-full.png"
"iconPath": "/brand-icons/03-bandana-badge.webp",
"fullPath": "/brand-icons/03-bandana-badge.webp"
},
{
"id": "04",
"slug": "magnifier-wink",
"titleZh": "放大镜眨眼",
"titleEn": "Magnifier wink",
"iconPath": "/brand-icons/04-magnifier-wink.png",
"fullPath": "/brand-icons/04-magnifier-wink-full.png"
"iconPath": "/brand-icons/04-magnifier-wink.webp",
"fullPath": "/brand-icons/04-magnifier-wink.webp"
},
{
"id": "05",
"slug": "window-peek",
"titleZh": "窗口探头",
"titleEn": "Window peek",
"iconPath": "/brand-icons/05-window-peek.png",
"fullPath": "/brand-icons/05-window-peek-full.png"
"iconPath": "/brand-icons/05-window-peek.webp",
"fullPath": "/brand-icons/05-window-peek.webp"
},
{
"id": "06",
"slug": "hex-collar",
"titleZh": "六边项圈",
"titleEn": "Hex collar",
"iconPath": "/brand-icons/06-hex-collar.png",
"fullPath": "/brand-icons/06-hex-collar-full.png"
"iconPath": "/brand-icons/06-hex-collar.webp",
"fullPath": "/brand-icons/06-hex-collar.webp"
},
{
"id": "07",
"slug": "graph-sit",
"titleZh": "关系图",
"titleEn": "Graph sit",
"iconPath": "/brand-icons/07-graph-sit.png",
"fullPath": "/brand-icons/07-graph-sit-full.png"
"iconPath": "/brand-icons/07-graph-sit.webp",
"fullPath": "/brand-icons/07-graph-sit.webp"
},
{
"id": "08",
"slug": "cloud-banner",
"titleZh": "云朵横幅",
"titleEn": "Cloud banner",
"iconPath": "/brand-icons/08-cloud-banner.png",
"fullPath": "/brand-icons/08-cloud-banner-full.png"
"iconPath": "/brand-icons/08-cloud-banner.webp",
"fullPath": "/brand-icons/08-cloud-banner.webp"
},
{
"id": "09",
"slug": "terminal-sit",
"titleZh": "终端旁坐",
"titleEn": "Terminal sit",
"iconPath": "/brand-icons/09-terminal-sit.png",
"fullPath": "/brand-icons/09-terminal-sit-full.png"
"iconPath": "/brand-icons/09-terminal-sit.webp",
"fullPath": "/brand-icons/09-terminal-sit.webp"
},
{
"id": "10",
"slug": "compass-bandana",
"titleZh": "罗盘头巾",
"titleEn": "Compass bandana",
"iconPath": "/brand-icons/10-compass-bandana.png",
"fullPath": "/brand-icons/10-compass-bandana-full.png"
"iconPath": "/brand-icons/10-compass-bandana.webp",
"fullPath": "/brand-icons/10-compass-bandana.webp"
}
]
}

View File

@@ -1,4 +1,4 @@
import { readFileSync } from 'node:fs';
import { readdirSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
@@ -25,13 +25,18 @@ describe('about brand lockup', () => {
expect(aboutLogoSnippet).toContain("boxShadow: 'none'");
});
it('defines a transparent about asset for every selectable mascot', () => {
const aboutAssetPaths = brandIconsSource.match(/aboutPath: '\/brand-icons\/\d{2}-.+-about\.png'/g) || [];
expect(aboutAssetPaths).toHaveLength(10);
for (const declaration of aboutAssetPaths) {
it('keeps one shared lossless WebP asset for every selectable mascot', () => {
const iconAssetPaths = brandIconsSource.match(/iconPath: '\/brand-icons\/\d{2}-.+\.webp'/g) || [];
expect(iconAssetPaths).toHaveLength(10);
for (const declaration of iconAssetPaths) {
const assetPath = declaration.match(/'([^']+)'/)?.[1];
expect(assetPath).toBeTruthy();
expect(readFileSync(`${brandIconsDirectory}${assetPath?.replace('/brand-icons/', '')}`, 'utf8')).not.toBe('');
}
const webpFiles = readdirSync(brandIconsDirectory).filter((file) => file.endsWith('.webp'));
expect(webpFiles).toHaveLength(10);
const legacyPngFiles = readdirSync(brandIconsDirectory).filter((file) => file.endsWith('.png'));
expect(legacyPngFiles).toEqual([]);
});
});

View File

@@ -814,7 +814,7 @@ function App() {
link.setAttribute('data-brand-icon', 'true');
document.head.appendChild(link);
}
link.type = 'image/png';
link.type = 'image/webp';
link.href = href;
let cancelled = false;

View File

@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import {
BRAND_ICONS,
resolveBrandAboutSrc,
resolveBrandDockSrc,
resolveBrandFullSrc,
resolveBrandIconSrc,
} from './brandIcons';
describe('brand icon asset resolution', () => {
it('uses one selected lossless asset for the favicon, picker, about page, and Dock', () => {
for (const icon of BRAND_ICONS) {
const selectedAsset = resolveBrandIconSrc(icon.id);
expect(selectedAsset).toMatch(/^\/brand-icons\/\d{2}-.+\.webp$/);
expect(resolveBrandFullSrc(icon.id)).toBe(selectedAsset);
expect(resolveBrandAboutSrc(icon.id)).toBe(selectedAsset);
expect(resolveBrandDockSrc(icon.id)).toBe(selectedAsset);
}
});
});

View File

@@ -16,11 +16,6 @@ export type BrandIconDefinition = {
titleZh: string;
titleEn: string;
iconPath: string;
fullPath: string;
/** Transparent complete lockup used by the About surface. */
aboutPath: string;
/** Opaque source tile composed into a rounded macOS Dock image at runtime. */
dockPath: string;
};
export const DEFAULT_BRAND_ICON_ID: BrandIconId = '02';
@@ -31,100 +26,70 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
slug: 'database-hug',
titleZh: '抱库小狗',
titleEn: 'Database hug',
iconPath: '/brand-icons/01-database-hug.png',
fullPath: '/brand-icons/01-database-hug-full.png',
aboutPath: '/brand-icons/01-database-hug-about.png',
dockPath: '/brand-icons/01-database-hug-dock.png',
iconPath: '/brand-icons/01-database-hug.webp',
},
{
id: '02',
slug: 'database-search',
titleZh: '搜库小狗',
titleEn: 'Database search',
iconPath: '/brand-icons/02-database-search.png',
fullPath: '/brand-icons/02-database-search-full.png',
aboutPath: '/brand-icons/02-database-search-about.png',
dockPath: '/brand-icons/02-database-search-dock.png',
iconPath: '/brand-icons/02-database-search.webp',
},
{
id: '03',
slug: 'bandana-badge',
titleZh: '头巾徽章',
titleEn: 'Bandana badge',
iconPath: '/brand-icons/03-bandana-badge.png',
fullPath: '/brand-icons/03-bandana-badge-full.png',
aboutPath: '/brand-icons/03-bandana-badge-about.png',
dockPath: '/brand-icons/03-bandana-badge-dock.png',
iconPath: '/brand-icons/03-bandana-badge.webp',
},
{
id: '04',
slug: 'magnifier-wink',
titleZh: '放大镜眨眼',
titleEn: 'Magnifier wink',
iconPath: '/brand-icons/04-magnifier-wink.png',
fullPath: '/brand-icons/04-magnifier-wink-full.png',
aboutPath: '/brand-icons/04-magnifier-wink-about.png',
dockPath: '/brand-icons/04-magnifier-wink-dock.png',
iconPath: '/brand-icons/04-magnifier-wink.webp',
},
{
id: '05',
slug: 'window-peek',
titleZh: '窗口探头',
titleEn: 'Window peek',
iconPath: '/brand-icons/05-window-peek.png',
fullPath: '/brand-icons/05-window-peek-full.png',
aboutPath: '/brand-icons/05-window-peek-about.png',
dockPath: '/brand-icons/05-window-peek-dock.png',
iconPath: '/brand-icons/05-window-peek.webp',
},
{
id: '06',
slug: 'hex-collar',
titleZh: '六边项圈',
titleEn: 'Hex collar',
iconPath: '/brand-icons/06-hex-collar.png',
fullPath: '/brand-icons/06-hex-collar-full.png',
aboutPath: '/brand-icons/06-hex-collar-about.png',
dockPath: '/brand-icons/06-hex-collar-dock.png',
iconPath: '/brand-icons/06-hex-collar.webp',
},
{
id: '07',
slug: 'graph-sit',
titleZh: '关系图',
titleEn: 'Graph sit',
iconPath: '/brand-icons/07-graph-sit.png',
fullPath: '/brand-icons/07-graph-sit-full.png',
aboutPath: '/brand-icons/07-graph-sit-about.png',
dockPath: '/brand-icons/07-graph-sit-dock.png',
iconPath: '/brand-icons/07-graph-sit.webp',
},
{
id: '08',
slug: 'cloud-banner',
titleZh: '云朵横幅',
titleEn: 'Cloud banner',
iconPath: '/brand-icons/08-cloud-banner.png',
fullPath: '/brand-icons/08-cloud-banner-full.png',
aboutPath: '/brand-icons/08-cloud-banner-about.png',
dockPath: '/brand-icons/08-cloud-banner-dock.png',
iconPath: '/brand-icons/08-cloud-banner.webp',
},
{
id: '09',
slug: 'terminal-sit',
titleZh: '终端旁坐',
titleEn: 'Terminal sit',
iconPath: '/brand-icons/09-terminal-sit.png',
fullPath: '/brand-icons/09-terminal-sit-full.png',
aboutPath: '/brand-icons/09-terminal-sit-about.png',
dockPath: '/brand-icons/09-terminal-sit-dock.png',
iconPath: '/brand-icons/09-terminal-sit.webp',
},
{
id: '10',
slug: 'compass-bandana',
titleZh: '罗盘头巾',
titleEn: 'Compass bandana',
iconPath: '/brand-icons/10-compass-bandana.png',
fullPath: '/brand-icons/10-compass-bandana-full.png',
aboutPath: '/brand-icons/10-compass-bandana-about.png',
dockPath: '/brand-icons/10-compass-bandana-dock.png',
iconPath: '/brand-icons/10-compass-bandana.webp',
},
];
@@ -147,14 +112,14 @@ export function resolveBrandIconSrc(id?: unknown): string {
}
export function resolveBrandFullSrc(id?: unknown): string {
return resolveBrandIcon(id).fullPath;
return resolveBrandIconSrc(id);
}
export function resolveBrandAboutSrc(id?: unknown): string {
return resolveBrandIcon(id).aboutPath;
return resolveBrandIconSrc(id);
}
/** Opaque source tile for the rounded macOS Dock composition. */
/** Dock uses the exact lossless WebP lockup rendered by BrandIconPicker. */
export function resolveBrandDockSrc(id?: unknown): string {
return resolveBrandIcon(id).dockPath;
return resolveBrandIconSrc(id);
}

View File

@@ -1,7 +1,16 @@
import { describe, expect, it } from 'vitest';
import { shouldSyncMacOSDockIcon } from './macDockIcon';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
calculateMacOSDockCornerRadius,
calculateMacOSDockImageRect,
composeMacOSDockIconBase64,
shouldSyncMacOSDockIcon,
} from './macDockIcon';
describe('shouldSyncMacOSDockIcon', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('only allows the native macOS runtime', () => {
expect(shouldSyncMacOSDockIcon({ platform: 'darwin', buildType: 'production' })).toBe(true);
expect(shouldSyncMacOSDockIcon({ platform: 'DARWIN', buildType: 'debug' })).toBe(true);
@@ -13,4 +22,68 @@ describe('shouldSyncMacOSDockIcon', () => {
expect(shouldSyncMacOSDockIcon({ platform: 'linux', buildType: 'production' })).toBe(false);
expect(shouldSyncMacOSDockIcon()).toBe(false);
});
it('fits square brand icons into the standard macOS Dock safe area', () => {
const rect = calculateMacOSDockImageRect(512, 512);
expect(rect).toEqual({
x: 100,
y: 100,
width: 824,
height: 824,
});
expect(calculateMacOSDockCornerRadius(rect)).toBe(184);
});
it('centres portrait brand lockups without stretching them into a square', () => {
expect(calculateMacOSDockImageRect(272, 449)).toEqual({
x: 263,
y: 100,
width: 499,
height: 824,
});
});
it('clips the complete brand image to the standard macOS rounded tile before drawing', async () => {
const calls: string[] = [];
const arcTo = vi.fn((...args: number[]) => calls.push(`arcTo:${args[4]}`));
const context = {
beginPath: vi.fn(() => calls.push('beginPath')),
moveTo: vi.fn(() => calls.push('moveTo')),
lineTo: vi.fn(() => calls.push('lineTo')),
arcTo,
closePath: vi.fn(() => calls.push('closePath')),
clip: vi.fn(() => calls.push('clip')),
drawImage: vi.fn(() => calls.push('drawImage')),
imageSmoothingEnabled: false,
imageSmoothingQuality: 'low',
} as unknown as CanvasRenderingContext2D;
const canvas = {
width: 0,
height: 0,
getContext: vi.fn(() => context),
toDataURL: vi.fn(() => 'data:image/png;base64,encoded'),
} as unknown as HTMLCanvasElement;
class FakeImage {
onload: (() => void) | null = null;
onerror: (() => void) | null = null;
naturalWidth = 512;
naturalHeight = 512;
width = 512;
height = 512;
set src(_value: string) {
queueMicrotask(() => this.onload?.());
}
}
vi.stubGlobal('Image', FakeImage);
vi.stubGlobal('document', { createElement: vi.fn(() => canvas) });
await expect(composeMacOSDockIconBase64('/brand-icons/09-terminal-sit.webp')).resolves.toBe('encoded');
expect(arcTo.mock.calls.map((args) => args[4])).toEqual([184, 184, 184, 184]);
expect(calls.indexOf('clip')).toBeGreaterThan(calls.indexOf('beginPath'));
expect(calls.indexOf('drawImage')).toBeGreaterThan(calls.indexOf('clip'));
});
});

View File

@@ -1,12 +1,22 @@
const DOCK_ICON_SIZE = 1024;
const DOCK_ICON_INSET = 24;
const DOCK_ICON_CORNER_RADIUS = 216;
// Chrome and VS Code both keep their high-alpha artwork inside an 824px
// square on a 1024px macOS icon canvas.
const DOCK_ICON_INSET = 100;
// Chrome's 824px tile uses a 184px outer corner radius.
const DOCK_ICON_CORNER_RADIUS_RATIO = 184 / 824;
export type DockIconRuntimeEnvironment = {
platform?: unknown;
buildType?: unknown;
};
export type MacOSDockImageRect = {
x: number;
y: number;
width: number;
height: number;
};
/**
* Only the native macOS runtime can update the Dock image. The generated
* Wails bridge also exists in the browser build, so checking method presence
@@ -32,37 +42,53 @@ function canvasToBase64Png(canvas: HTMLCanvasElement): string {
return comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
}
function addRoundedRectPath(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number,
): void {
const clampedRadius = Math.min(radius, width / 2, height / 2);
export function calculateMacOSDockImageRect(imageWidth: number, imageHeight: number): MacOSDockImageRect {
const tileSize = DOCK_ICON_SIZE - (DOCK_ICON_INSET * 2);
const sourceWidth = Math.max(1, Number(imageWidth) || tileSize);
const sourceHeight = Math.max(1, Number(imageHeight) || tileSize);
const scale = Math.min(tileSize / sourceWidth, tileSize / sourceHeight);
const width = Math.round(sourceWidth * scale);
const height = Math.round(sourceHeight * scale);
return {
x: Math.round((DOCK_ICON_SIZE - width) / 2),
y: Math.round((DOCK_ICON_SIZE - height) / 2),
width,
height,
};
}
export function calculateMacOSDockCornerRadius(rect: MacOSDockImageRect): number {
return Math.round(Math.min(rect.width, rect.height) * DOCK_ICON_CORNER_RADIUS_RATIO);
}
function clipMacOSDockImage(ctx: CanvasRenderingContext2D, rect: MacOSDockImageRect): void {
const radius = calculateMacOSDockCornerRadius(rect);
const right = rect.x + rect.width;
const bottom = rect.y + rect.height;
ctx.beginPath();
ctx.moveTo(x + clampedRadius, y);
ctx.lineTo(x + width - clampedRadius, y);
ctx.arcTo(x + width, y, x + width, y + clampedRadius, clampedRadius);
ctx.lineTo(x + width, y + height - clampedRadius);
ctx.arcTo(x + width, y + height, x + width - clampedRadius, y + height, clampedRadius);
ctx.lineTo(x + clampedRadius, y + height);
ctx.arcTo(x, y + height, x, y + height - clampedRadius, clampedRadius);
ctx.lineTo(x, y + clampedRadius);
ctx.arcTo(x, y, x + clampedRadius, y, clampedRadius);
ctx.moveTo(rect.x + radius, rect.y);
ctx.lineTo(right - radius, rect.y);
ctx.arcTo(right, rect.y, right, rect.y + radius, radius);
ctx.lineTo(right, bottom - radius);
ctx.arcTo(right, bottom, right - radius, bottom, radius);
ctx.lineTo(rect.x + radius, bottom);
ctx.arcTo(rect.x, bottom, rect.x, bottom - radius, radius);
ctx.lineTo(rect.x, rect.y + radius);
ctx.arcTo(rect.x, rect.y, rect.x + radius, rect.y, radius);
ctx.closePath();
ctx.clip();
}
/**
* Compose the selected opaque brand tile into a rounded, transparent PNG.
* The transparent outer pixels let macOS apply its native Dock treatment
* instead of displaying the source image as a full square.
* Place the selected complete brand icon on a transparent macOS canvas.
* Keep the source artwork intact while normalising its outer tile to the
* standard macOS corner geometry.
*/
export async function composeMacOSDockIconBase64(src: string): Promise<string> {
const img = await loadImage(src);
const size = DOCK_ICON_SIZE;
const tileSize = size - (DOCK_ICON_INSET * 2);
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
@@ -70,15 +96,10 @@ export async function composeMacOSDockIconBase64(src: string): Promise<string> {
if (!ctx) {
throw new Error('2d context unavailable');
}
ctx.save();
addRoundedRectPath(ctx, DOCK_ICON_INSET, DOCK_ICON_INSET, tileSize, tileSize, DOCK_ICON_CORNER_RADIUS);
ctx.clip();
// Opaque soft-blue fallback under draw (matches the pre-baked dock tiles).
ctx.fillStyle = '#E8F4FF';
ctx.fillRect(DOCK_ICON_INSET, DOCK_ICON_INSET, tileSize, tileSize);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, DOCK_ICON_INSET, DOCK_ICON_INSET, tileSize, tileSize);
ctx.restore();
const rect = calculateMacOSDockImageRect(img.naturalWidth || img.width, img.naturalHeight || img.height);
clipMacOSDockImage(ctx, rect);
ctx.drawImage(img, rect.x, rect.y, rect.width, rect.height);
return canvasToBase64Png(canvas);
}

View File

@@ -47,7 +47,7 @@ export default function BrandIconPicker({ value, onChange, darkMode = false }: B
overflow: 'visible',
}}
>
{/* Use full lockup so the complete logo is visible in the picker */}
{/* Each picker card uses the same lossless source as every app surface. */}
<div
style={{
width: '100%',
@@ -61,7 +61,7 @@ export default function BrandIconPicker({ value, onChange, darkMode = false }: B
}}
>
<img
src={item.fullPath}
src={item.iconPath}
alt={item.titleZh}
style={{
maxWidth: '100%',