🐛 fix(about): 移除关于页品牌图标白底
- 恢复 10 套关于页专用透明品牌资源 - 保持选择器、Dock 与应用图标继续使用方形 WebP - 补充资源透明像素和默认回退逻辑回归测试
BIN
frontend/public/brand-icons/01-database-hug-about.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
frontend/public/brand-icons/02-database-search-about.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
frontend/public/brand-icons/03-bandana-badge-about.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
frontend/public/brand-icons/04-magnifier-wink-about.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
frontend/public/brand-icons/05-window-peek-about.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
frontend/public/brand-icons/06-hex-collar-about.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
frontend/public/brand-icons/07-graph-sit-about.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
frontend/public/brand-icons/08-cloud-banner-about.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
frontend/public/brand-icons/09-terminal-sit-about.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
frontend/public/brand-icons/10-compass-bandana-about.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
@@ -1,5 +1,6 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { unzlibSync } from 'fflate';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const appSource = readFileSync(
|
||||
@@ -17,6 +18,36 @@ const defaultTitlebarMark = fileURLToPath(
|
||||
new globalThis.URL('../public/brand-marks/02-database-search-transparent.png', import.meta.url),
|
||||
);
|
||||
|
||||
const readFirstPngPixelAlpha = (assetBase64: string): number => {
|
||||
const binary = globalThis.atob(assetBase64);
|
||||
const pngBytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
||||
const idatChunks: Uint8Array[] = [];
|
||||
|
||||
for (let offset = 8; offset + 12 <= pngBytes.length;) {
|
||||
const chunkLength = new DataView(
|
||||
pngBytes.buffer,
|
||||
pngBytes.byteOffset + offset,
|
||||
4,
|
||||
).getUint32(0);
|
||||
const chunkType = String.fromCharCode(...pngBytes.slice(offset + 4, offset + 8));
|
||||
if (chunkType === 'IDAT') {
|
||||
idatChunks.push(pngBytes.slice(offset + 8, offset + 8 + chunkLength));
|
||||
}
|
||||
offset += chunkLength + 12;
|
||||
}
|
||||
|
||||
const compressedLength = idatChunks.reduce((total, chunk) => total + chunk.length, 0);
|
||||
const compressedData = new Uint8Array(compressedLength);
|
||||
let compressedOffset = 0;
|
||||
for (const chunk of idatChunks) {
|
||||
compressedData.set(chunk, compressedOffset);
|
||||
compressedOffset += chunk.length;
|
||||
}
|
||||
|
||||
const scanlines = unzlibSync(compressedData);
|
||||
return scanlines[4] ?? -1;
|
||||
};
|
||||
|
||||
describe('about brand lockup', () => {
|
||||
it('uses a transparent lockup without a tile background on the about page', () => {
|
||||
expect(appSource).toContain('resolveBrandAboutSrc');
|
||||
@@ -37,18 +68,33 @@ describe('about brand lockup', () => {
|
||||
expect(readFileSync(defaultTitlebarMark, 'base64')).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('keeps one shared lossless WebP asset for every selectable mascot', () => {
|
||||
it('keeps a tile asset and a transparent about lockup for every selectable mascot', () => {
|
||||
const iconAssetPaths = brandIconsSource.match(/iconPath: '\/brand-icons\/\d{2}-.+\.webp'/g) || [];
|
||||
const aboutAssetPaths = brandIconsSource.match(/aboutPath: '\/brand-icons\/\d{2}-.+-about\.png'/g) || [];
|
||||
expect(iconAssetPaths).toHaveLength(10);
|
||||
expect(aboutAssetPaths).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('');
|
||||
expect(readFileSync(`${brandIconsDirectory}${assetPath?.replace('/brand-icons/', '')}`, 'base64')).not.toBe('');
|
||||
}
|
||||
|
||||
for (const declaration of aboutAssetPaths) {
|
||||
const assetPath = declaration.match(/'([^']+)'/)?.[1];
|
||||
expect(assetPath).toBeTruthy();
|
||||
const assetBase64 = readFileSync(
|
||||
`${brandIconsDirectory}${assetPath?.replace('/brand-icons/', '')}`,
|
||||
'base64',
|
||||
);
|
||||
expect(assetBase64).not.toBe('');
|
||||
expect(globalThis.atob(assetBase64).charCodeAt(25)).toBe(6);
|
||||
expect(readFirstPngPixelAlpha(assetBase64)).toBe(0);
|
||||
}
|
||||
|
||||
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([]);
|
||||
const aboutPngFiles = readdirSync(brandIconsDirectory).filter((file) => file.endsWith('-about.png'));
|
||||
expect(aboutPngFiles).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,13 +9,16 @@ import {
|
||||
} from './brandIcons';
|
||||
|
||||
describe('brand icon asset resolution', () => {
|
||||
it('uses one selected lossless asset for the favicon, picker, about page, and Dock', () => {
|
||||
it('keeps tile assets for app surfaces and uses a transparent lockup on the about page', () => {
|
||||
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);
|
||||
|
||||
const aboutAsset = resolveBrandAboutSrc(icon.id);
|
||||
expect(aboutAsset).toMatch(/^\/brand-icons\/\d{2}-.+-about\.png$/);
|
||||
expect(aboutAsset).not.toBe(selectedAsset);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,4 +29,10 @@ describe('brand icon asset resolution', () => {
|
||||
expect(resolveBrandTitlebarSrc('unknown')).toBe(defaultTitlebarAsset);
|
||||
expect(resolveBrandTitlebarSrc('01')).toBe(resolveBrandIconSrc('01'));
|
||||
});
|
||||
|
||||
it('falls back to the default transparent about lockup for invalid selections', () => {
|
||||
const defaultAboutAsset = resolveBrandAboutSrc('02');
|
||||
expect(resolveBrandAboutSrc()).toBe(defaultAboutAsset);
|
||||
expect(resolveBrandAboutSrc('unknown')).toBe(defaultAboutAsset);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ export type BrandIconDefinition = {
|
||||
titleZh: string;
|
||||
titleEn: string;
|
||||
iconPath: string;
|
||||
aboutPath: string;
|
||||
titlebarPath?: string;
|
||||
};
|
||||
|
||||
@@ -28,6 +29,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '抱库小狗',
|
||||
titleEn: 'Database hug',
|
||||
iconPath: '/brand-icons/01-database-hug.webp',
|
||||
aboutPath: '/brand-icons/01-database-hug-about.png',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
@@ -35,6 +37,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '搜库小狗',
|
||||
titleEn: 'Database search',
|
||||
iconPath: '/brand-icons/02-database-search.webp',
|
||||
aboutPath: '/brand-icons/02-database-search-about.png',
|
||||
titlebarPath: '/brand-marks/02-database-search-transparent.png',
|
||||
},
|
||||
{
|
||||
@@ -43,6 +46,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '头巾徽章',
|
||||
titleEn: 'Bandana badge',
|
||||
iconPath: '/brand-icons/03-bandana-badge.webp',
|
||||
aboutPath: '/brand-icons/03-bandana-badge-about.png',
|
||||
},
|
||||
{
|
||||
id: '04',
|
||||
@@ -50,6 +54,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '放大镜眨眼',
|
||||
titleEn: 'Magnifier wink',
|
||||
iconPath: '/brand-icons/04-magnifier-wink.webp',
|
||||
aboutPath: '/brand-icons/04-magnifier-wink-about.png',
|
||||
},
|
||||
{
|
||||
id: '05',
|
||||
@@ -57,6 +62,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '窗口探头',
|
||||
titleEn: 'Window peek',
|
||||
iconPath: '/brand-icons/05-window-peek.webp',
|
||||
aboutPath: '/brand-icons/05-window-peek-about.png',
|
||||
},
|
||||
{
|
||||
id: '06',
|
||||
@@ -64,6 +70,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '六边项圈',
|
||||
titleEn: 'Hex collar',
|
||||
iconPath: '/brand-icons/06-hex-collar.webp',
|
||||
aboutPath: '/brand-icons/06-hex-collar-about.png',
|
||||
},
|
||||
{
|
||||
id: '07',
|
||||
@@ -71,6 +78,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '关系图',
|
||||
titleEn: 'Graph sit',
|
||||
iconPath: '/brand-icons/07-graph-sit.webp',
|
||||
aboutPath: '/brand-icons/07-graph-sit-about.png',
|
||||
},
|
||||
{
|
||||
id: '08',
|
||||
@@ -78,6 +86,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '云朵横幅',
|
||||
titleEn: 'Cloud banner',
|
||||
iconPath: '/brand-icons/08-cloud-banner.webp',
|
||||
aboutPath: '/brand-icons/08-cloud-banner-about.png',
|
||||
},
|
||||
{
|
||||
id: '09',
|
||||
@@ -85,6 +94,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '终端旁坐',
|
||||
titleEn: 'Terminal sit',
|
||||
iconPath: '/brand-icons/09-terminal-sit.webp',
|
||||
aboutPath: '/brand-icons/09-terminal-sit-about.png',
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
@@ -92,6 +102,7 @@ export const BRAND_ICONS: BrandIconDefinition[] = [
|
||||
titleZh: '罗盘头巾',
|
||||
titleEn: 'Compass bandana',
|
||||
iconPath: '/brand-icons/10-compass-bandana.webp',
|
||||
aboutPath: '/brand-icons/10-compass-bandana-about.png',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -118,7 +129,7 @@ export function resolveBrandFullSrc(id?: unknown): string {
|
||||
}
|
||||
|
||||
export function resolveBrandAboutSrc(id?: unknown): string {
|
||||
return resolveBrandIconSrc(id);
|
||||
return resolveBrandIcon(id).aboutPath;
|
||||
}
|
||||
|
||||
export function resolveBrandTitlebarSrc(id?: unknown): string {
|
||||
|
||||