BUG-737:--font-display 以两个从未加载的 Anthropic 授权字体开头 (Tiempos Headline / StyreneB:无 @font-face、public/ 无字体、layout.tsx 只 vendor 了 Inter),于是每个中文标题都掉到 Songti SC / SimSun——20 条 规则宽,含助手回答正文的 h2/h3。display 与 body 合并为同一条无衬线栈, 层级改由字重承担(33 条 display 规则 400→500)。 拉丁衬线方案实测否决:Newsreader 拉丁子集 132 KB(Inter 的 2.7 倍), 只为给一个品牌字上衬线,且会把「D10 事业盘怎么读」劈成两种字形。 BUG-738:亮色 #85432f 与暗色 #d78064 不同源;--color-ring 在 @theme inline 里硬编码不跟随 :root。产品拍板换 Claude coral #cc785c, 但实测它作文字色只有 3.14:1,而 62 个调用点里 53 个是 color:。 按角色拆两阶:--color-action #a9583e(文字 4.85:1)、 --color-action-strong #cc785c(填充/导轨/焦点环,3:1 非文字阈值)。 --report-accent 刻意保持 #85432f(报告是纸面,不跟应用强调色)。 T1.3/T1.4:模型选择器进 ChatComposer 的 toolbar 插槽,删掉常驻 44px 的 .composer-footer;顶栏 68→46px,「分析对象:」改为标题旁静默 chip。 防复发:新增 font-stack-loadable-contract,断言字体栈里每个 family 要么 vendored 要么是系统字体,且 --font-display 不得触达任何 CJK 衬线。 测试 3346→3350,fail 仍 31 且清单与基线逐条一致;/ 仍 Static; CSS gzip 41,095→41,096(+0.002%);快速门 pytest 段 798 passed / 0 failed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
84 lines
4.0 KiB
TypeScript
84 lines
4.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import { extname, join } from "node:path";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const layout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
|
|
const siteStyles = readFileSync(new URL("../src/app/site-styles.ts", import.meta.url), "utf8");
|
|
const adminLayout = readFileSync(new URL("../src/app/admin/layout.tsx", import.meta.url), "utf8");
|
|
const adminCss = readFileSync(new URL("../src/app/admin/admin.css", import.meta.url), "utf8");
|
|
const homePage = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
|
const loginPage = readFileSync(new URL("../src/app/login/page.tsx", import.meta.url), "utf8");
|
|
|
|
test("Inter is loaded through next/font and applied as a CSS variable", () => {
|
|
assert.match(layout, /import localFont from "next\/font\/local"/);
|
|
assert.doesNotMatch(layout, /from ["']next\/font\/google["']/);
|
|
assert.match(layout, /src: "\.\/fonts\/InterVariable-latin\.woff2"/);
|
|
assert.match(layout, /display: "swap"/);
|
|
assert.match(layout, /variable: "--font-inter"/);
|
|
assert.match(layout, /className=\{inter\.variable\}/);
|
|
assert.match(readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"), /var\(--font-inter, Inter\)/);
|
|
assert.match(
|
|
readFileSync(new URL("../src/components/admin/admin-app.tsx", import.meta.url), "utf8"),
|
|
/var\(--font-inter, Inter\)/,
|
|
);
|
|
});
|
|
|
|
test("Inter is vendored locally so production builds do not fetch Google Fonts", () => {
|
|
const font = readFileSync(new URL("../src/app/fonts/InterVariable-latin.woff2", import.meta.url));
|
|
assert.equal(Buffer.from(font.subarray(0, 4)).toString(), "wOF2");
|
|
assert.ok(font.byteLength > 20_000);
|
|
const srcRoot = fileURLToPath(new URL("../src/", import.meta.url));
|
|
const stack = [srcRoot];
|
|
while (stack.length > 0) {
|
|
const dir = stack.pop()!;
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const fullPath = join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(fullPath);
|
|
continue;
|
|
}
|
|
if (![".ts", ".tsx", ".js", ".jsx", ".css"].includes(extname(entry.name))) continue;
|
|
const source = readFileSync(fullPath, "utf8");
|
|
assert.doesNotMatch(
|
|
source,
|
|
/from ["']next\/font\/google["']|fonts\.googleapis\.com/,
|
|
`${fullPath} must not fetch Google Fonts at build time`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("site chrome owns globals.css; admin does not import it", () => {
|
|
assert.match(siteStyles, /import "\.\/globals\.css"/);
|
|
assert.match(siteStyles, /import "\.\/birth-time-choice\.css"/);
|
|
assert.doesNotMatch(layout, /globals\.css|birth-time-choice\.css|site-styles/);
|
|
assert.match(homePage, /import "@\/app\/site-styles"/);
|
|
assert.match(loginPage, /import "\.\.\/site-styles"/);
|
|
assert.doesNotMatch(adminLayout, /globals\.css|site-styles/);
|
|
assert.doesNotMatch(
|
|
readFileSync(new URL("../src/app/error.tsx", import.meta.url), "utf8"),
|
|
/site-styles|globals\.css/,
|
|
);
|
|
assert.doesNotMatch(
|
|
readFileSync(new URL("../src/app/not-found.tsx", import.meta.url), "utf8"),
|
|
/site-styles|globals\.css/,
|
|
);
|
|
assert.match(adminCss, // 原值 `--font-body: StyreneB, var(--font-inter, Inter)` / 新值:去掉 StyreneB
|
|
// 原因:StyreneB 是 Anthropic 授权字体,本仓从未加载它(无 @font-face、public/ 无字体文件),
|
|
// 留在栈首只是死配置。见 BUG-737。
|
|
/--font-body: var\(--font-inter, Inter\)/);
|
|
});
|
|
|
|
test("root error and not-found pages do not depend on Tailwind utilities or Button", () => {
|
|
const errorPage = readFileSync(new URL("../src/app/error.tsx", import.meta.url), "utf8");
|
|
const notFoundPage = readFileSync(new URL("../src/app/not-found.tsx", import.meta.url), "utf8");
|
|
for (const source of [errorPage, notFoundPage]) {
|
|
assert.doesNotMatch(source, /className="flex/);
|
|
assert.doesNotMatch(source, /min-h-svh/);
|
|
assert.doesNotMatch(source, /text-muted-foreground/);
|
|
assert.doesNotMatch(source, /@\/components\/ui\/button/);
|
|
}
|
|
});
|