On a 1x screen TDesign's glyphs smeared at 14 and 16px: their 24-unit
strokes land between pixels, so every ~1px line covered two at half ink.
All 37 icon/size pairs are now TDesign's shapes redrawn on their own grid
(viewBox 0 0 N N, so one unit is one CSS pixel):
- Straight lines are runs of whole pixels that end on pixel edges, with
no caps or joins. WebKit drops square caps on a path of several
subpaths, and Skia strokes 1px lines as hairlines that leave a joined
corner pixel three-quarters inked.
- Slants and curves are 1.25px so they read as heavy as the crisp runs.
Their ends, vertices and circle extremes sit on pixel centres.
- server24 keeps TDesign's 24px geometry; only its dots move onto whole
pixels.
Only closeCircle24 (TDesign's own 24px grid) and the spinner still come
from @ng-icons/tdesign-icons. This also fixes add14 from 5d23809, whose
square-capped ends rendered half-inked in WebKit. icons.spec.ts now also
checks that every grid name's glyph uses that grid's viewBox.
Checked at 1x and 2x in WebKit (the app's macOS engine) and in Chrome
(Skia, as in WebView2 and WebKitGTK); straight runs fill whole pixels in
both. In WebKit at 1x, a copy of the tab bar, toolbar, file rows, menu
and shortcut-row markup rendered every straight-line glyph with zero
blur, including icons laid out on fractional pixels.
Co-Authored-By: Claude <noreply@anthropic.com>
50 lines
2.4 KiB
TypeScript
50 lines
2.4 KiB
TypeScript
import { expect, test } from 'bun:test';
|
|
import { APP_ICONS } from './icons';
|
|
import { PIXEL_ICONS } from './pixel-icons';
|
|
|
|
// The px grid of each size token (styles.css); lg only carries the spinner.
|
|
const GRID: Record<string, string> = { sm: '14', md: '16', lg: '20', xl: '24' };
|
|
const SKINS = `${import.meta.dir}/../skins`;
|
|
|
|
// Templates can't type-check icon names, so read them: every <ng-icon> has a
|
|
// size token, and every literal name (both sides of a [name] ternary too) is
|
|
// registered and drawn for that token's grid. Names computed in TS are typed.
|
|
test('every template icon is registered and drawn for the size it renders at', async () => {
|
|
const problems: string[] = [];
|
|
for (const file of new Bun.Glob('**/*.{html,ts}').scanSync(SKINS)) {
|
|
const src = (await Bun.file(`${SKINS}/${file}`).text()).replace(/<!--[\s\S]*?-->/g, '');
|
|
for (const [tag] of src.matchAll(/<ng-icon\b[^>]*>/g)) {
|
|
const token = tag.match(/size="var\(--icon-size-(\w+)\)"/)?.[1];
|
|
if (!token || !GRID[token]) {
|
|
problems.push(`${file}: no size token in ${tag}`);
|
|
continue;
|
|
}
|
|
const bound = tag.match(/\[name\]="([^"]*)"/)?.[1] ?? '';
|
|
// a ternary's results, not its condition ('local' in `id === 'local' ? …`)
|
|
const names = [...tag.matchAll(/\bname="(\w+)"/g), ...bound.matchAll(/[?:]\s*'(\w+)'/g)].map((m) => m[1]);
|
|
for (const name of names) {
|
|
if (!Object.hasOwn(APP_ICONS, name)) problems.push(`${file}: ${name} is not registered`);
|
|
else if (name !== 'tdesignLoading' && !name.endsWith(GRID[token])) {
|
|
problems.push(`${file}: ${name} renders at ${token} (${GRID[token]}px)`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
expect(problems).toEqual([]);
|
|
});
|
|
|
|
// A grid name's glyph is drawn on that grid (a TDesign glyph registered as
|
|
// foo14 would be a 24-unit viewBox), and the redrawn ones use plain attributes
|
|
// only: TDesign's stroke-width var or any style would bypass the grid.
|
|
test('every glyph is drawn on the grid its name says', () => {
|
|
const problems: string[] = [];
|
|
for (const [name, svg] of Object.entries(APP_ICONS)) {
|
|
const grid = name.match(/(\d+)$/)?.[1];
|
|
if (grid && !svg.includes(`viewBox="0 0 ${grid} ${grid}"`)) problems.push(`${name}: not drawn on a ${grid}px grid`);
|
|
}
|
|
for (const [name, svg] of Object.entries(PIXEL_ICONS)) {
|
|
if (/var\(|style=/.test(svg)) problems.push(`${name}: uses a CSS var or a style`);
|
|
}
|
|
expect(problems).toEqual([]);
|
|
});
|