Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-icons-center.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'cherry-markdown': patch
---

fix: 工具栏下拉菜单使用 flex 居中图标与文字,并统一图标字体的跨平台基线和行框度量,避免 Windows Chrome 中字形仍高于文字。
8 changes: 8 additions & 0 deletions packages/cherry-markdown/build/run-iconfont.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,14 @@ async function main() {
fontObject.name.fontSubFamily = 'Regular';
fontObject.name.fullName = fontName;
fontObject.name.postScriptName = fontName;
// SVG glyphs use the hhea baseline. Keep OS/2 metrics consistent so browsers
// using that table do not center a different line box around the same glyph.
const metrics = fontObject['OS/2'];
metrics.sTypoAscender = fontObject.hhea.ascent;
metrics.sTypoDescender = fontObject.hhea.descent;
metrics.sTypoLineGap = fontObject.hhea.lineGap;
metrics.usWinAscent = fontObject.hhea.ascent;
metrics.usWinDescent = Math.abs(fontObject.hhea.descent);
font.set(fontObject);

await fs.promises.mkdir(fontsDir, { recursive: true });
Expand Down
18 changes: 11 additions & 7 deletions packages/cherry-markdown/src/sass/cherry.scss
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@
width: calc(100% - 8px);
padding: var(--dropdown-item-padding);
text-align: left;
display: block;
display: flex;
align-items: center;
min-height: var(--dropdown-item-height);
line-height: var(--line-height-relaxed);
font-size: var(--font-size-sm);
Expand All @@ -211,26 +212,29 @@
color: var(--dropdown-item-active-color);
}

// 二级菜单里图标与文字的垂直对齐:
// 1. <i.ch-icon> 使用固定盒(宽高 = 图标视觉尺寸),并按视觉中线对齐周围文字;
// 2. ::before 承载字形,用 vertical-align: middle 让字形按视觉中线而非字体基线对齐,
// 消除图标字体自身"偏上"的字形度量,从根本上解决"图标比文字高一些"的问题。
// 使用盒模型中心对齐图标与文字,避免 inline baseline 和系统字体 x-height
// 在不同浏览器、操作系统下产生相反方向的垂直偏移。
.ch-icon {
display: inline-block;
width: var(--toolbar-icon-size);
height: var(--toolbar-icon-size);
line-height: var(--toolbar-icon-size);
font-size: var(--toolbar-icon-size);
text-align: center;
vertical-align: middle;
flex-shrink: 0;
margin-right: 8px;
overflow: visible;

&::before {
vertical-align: middle;
line-height: var(--toolbar-icon-size);
}
}

i.ch-icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
}

&-separator {
Expand Down
25 changes: 25 additions & 0 deletions packages/cherry-markdown/test/build/iconfont-metrics.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// @vitest-environment node
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createFont, woff2 } from 'fonteditor-core';
import { describe, expect, it } from 'vite-plus/test';

const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');

describe('generated iconfont metrics', () => {
it('writes consistent hhea and OS/2 baseline and line metrics', async () => {
execFileSync(process.execPath, [resolve(projectRoot, 'build/run-iconfont.js')]);
await woff2.init();
for (const type of ['ttf', 'woff', 'woff2', 'eot']) {
const font = createFont(readFileSync(resolve(projectRoot, `dist/fonts/ch-icon.${type}`)), { type }).get();
const metrics = font['OS/2'];
expect(metrics.sTypoAscender, type).toBe(font.hhea.ascent);
expect(metrics.sTypoDescender, type).toBe(font.hhea.descent);
expect(metrics.sTypoLineGap, type).toBe(font.hhea.lineGap);
expect(metrics.usWinAscent, type).toBe(font.hhea.ascent);
expect(metrics.usWinDescent, type).toBe(Math.abs(font.hhea.descent));
}
}, 15000);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @vitest-environment node
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import postcss from 'postcss';
import { compile } from 'sass';
import { describe, expect, it } from 'vite-plus/test';

const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const stylesheet = postcss.parse(compile(resolve(projectRoot, 'src/sass/index.scss')).css);

const getDeclarations = (selector) => {
let declarations;

stylesheet.walkRules((rule) => {
if (rule.selectors.includes(selector)) {
declarations = Object.fromEntries(
rule.nodes.filter((node) => node.type === 'decl').map((node) => [node.prop, node.value]),
);
}
});

expect(declarations, selector).toBeDefined();
return declarations;
};

describe('toolbar alignment style contract', () => {
it('centers dropdown icons and text without inline baseline alignment', () => {
expect(getDeclarations('.cherry-dropdown-item')).toMatchObject({
display: 'flex',
'align-items': 'center',
});
expect(getDeclarations('.cherry-dropdown-item .ch-icon')).toMatchObject({
'flex-shrink': '0',
});
expect(getDeclarations('.cherry-dropdown-item i.ch-icon')).toMatchObject({
display: 'inline-flex',
'align-items': 'center',
'justify-content': 'center',
});
expect(getDeclarations('.cherry-dropdown-item .ch-icon')['vertical-align']).toBeUndefined();
expect(getDeclarations('.cherry-dropdown-item .ch-icon::before')['vertical-align']).toBeUndefined();
});
});
Loading