fix(ui,a11y): 修 logo 深色模式不可见 + 补 tailwind font-calligraphy 工具类
修复 A:header logo 深色模式字被吞 - logo.svg 右侧公司名(睿新致遠 + NOVALON)硬编码 fill='#0A0E14', 深色背景下 background 同色不可见,红色印章下书法字与英文全部被吞。 - 复用既有 logo-white.svg(白字 + 红印章)作为深色版, header 渲染双 <Image>,按 html[data-theme] CSS 显隐。 - 双 Image 不占位(display:none),零 JS 闪烁,未来主题切换器自动生效。 修复 B:font-calligraphy 工具类不生成 - tailwind.config.js 的 fontFamily 此前未声明 calligraphy key, 导致 font-calligraphy utility 永不生成,4 处装饰书法字 (BrandStamp「值得信赖」/ ChallengeSection「使命」/ WhyUsSection「睿」「我们」) 静默回退 sans。 - fontFamily 加 calligraphy: ['var(--font-calligraphy-system)']。 审计脚本: - logo-compare*.mjs:生产 vs 本地 logo 对比定位 - logo-dark-verify.mjs:明暗双版渲染验证 - font-calligraphy-verify.mjs:合成探针验证 utility 生效 验证: - type-check 0 error · lint 0 error(127 warnings 既有项与本次无关) - 深浅色 header 截图:明暗版正确切换,书法字 + NOVALON 清晰可见 - font-calligraphy 合成探针 computed fontFamily = STKaiti, KaiTi, 楷体, SimKai, serif ✓
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
// 终极:直接构造一个含 font-calligraphy className 的测试元素,
|
||||||
|
// 把它注入到任意真实页面 DOM,读取 computed fontFamily —— 验证 CSS 规则真生效
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
||||||
|
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
// 找一个已渲染的根节点挂测试元素
|
||||||
|
const mount = document.body;
|
||||||
|
const probe = document.createElement('span');
|
||||||
|
probe.id = '__probe_font_calligraphy__';
|
||||||
|
probe.className = 'font-calligraphy';
|
||||||
|
probe.textContent = '使命';
|
||||||
|
probe.style.position = 'fixed';
|
||||||
|
probe.style.top = '-9999px';
|
||||||
|
mount.appendChild(probe);
|
||||||
|
const cs = getComputedStyle(probe);
|
||||||
|
const out = {
|
||||||
|
probeFontFamily: cs.fontFamily,
|
||||||
|
probeClassList: Array.from(probe.classList),
|
||||||
|
};
|
||||||
|
probe.remove();
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
|
const expectedPrefix = 'STKaiti';
|
||||||
|
const pass = result.probeFontFamily.startsWith(expectedPrefix);
|
||||||
|
console.log(`\n[assert] probe computed fontFamily starts with ${expectedPrefix}: ${pass}`);
|
||||||
|
console.log(`[result] ${pass ? 'PASS' : 'FAIL'}`);
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 深色模式 logo 对比:生产 vs 本地
|
||||||
|
* 强制 prefers-color-scheme: dark,截 header logo
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const OUT = path.join(process.cwd(), 'dogfood-ui-audit', 'logo-compare');
|
||||||
|
fs.mkdirSync(OUT, { recursive: true });
|
||||||
|
|
||||||
|
const TARGETS = [
|
||||||
|
{ tag: 'prod-dark', url: 'https://www.novalon.cn/' },
|
||||||
|
{ tag: 'local-dark', url: 'http://127.0.0.1:3000/' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
for (const t of TARGETS) {
|
||||||
|
const ctx = await browser.newContext({
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
colorScheme: 'dark', // 强制深色模式
|
||||||
|
});
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
try {
|
||||||
|
await page.goto(t.url, { waitUntil: 'networkidle', timeout: 60000 });
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
// 1) 截 header
|
||||||
|
const header = page.locator('header').first();
|
||||||
|
await header.screenshot({ path: path.join(OUT, `${t.tag}-header.png`) }).catch(async () => {
|
||||||
|
await page.screenshot({ path: path.join(OUT, `${t.tag}-header.png`), clip: { x: 0, y: 0, width: 1440, height: 80 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2) 探测 logo 元素的可视状态(在深色背景下)
|
||||||
|
const info = await page.evaluate(() => {
|
||||||
|
const imgs = [...document.querySelectorAll('img')].filter(i => /logo\.svg$/.test(i.getAttribute('src') || ''));
|
||||||
|
return imgs.map(i => {
|
||||||
|
const r = i.getBoundingClientRect();
|
||||||
|
const cs = getComputedStyle(i);
|
||||||
|
const parentCs = getComputedStyle(i.closest('header') || document.body);
|
||||||
|
return {
|
||||||
|
src: i.getAttribute('src'),
|
||||||
|
renderedW: Math.round(r.width),
|
||||||
|
renderedH: Math.round(r.height),
|
||||||
|
filter: cs.filter,
|
||||||
|
opacity: cs.opacity,
|
||||||
|
visibility: cs.visibility,
|
||||||
|
parentBg: parentCs.backgroundColor,
|
||||||
|
htmlBg: getComputedStyle(document.documentElement).backgroundColor,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log(`[${t.tag}]`);
|
||||||
|
for (const l of info) console.log(' ' + JSON.stringify(l));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[${t.tag}] FAILED ${e.message.slice(0, 120)}`);
|
||||||
|
}
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
await browser.close();
|
||||||
|
console.log('\n✓ 输出目录:' + OUT);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* 综合视觉对比:生产 vs 本地,多个关键位置
|
||||||
|
* 收集决定性证据,让用户指出"logo 原本是书法体但生产不是"指的具体位置
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const OUT = path.join(process.cwd(), 'dogfood-ui-audit', 'logo-compare');
|
||||||
|
fs.mkdirSync(OUT, { recursive: true });
|
||||||
|
|
||||||
|
const PAGES = [
|
||||||
|
{ name: 'home', url: '/' },
|
||||||
|
{ name: 'about-brand', url: '/about/brand' },
|
||||||
|
{ name: 'about', url: '/about' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TARGETS = [
|
||||||
|
{ tag: 'prod', base: 'https://www.novalon.cn' },
|
||||||
|
{ tag: 'local', base: 'http://127.0.0.1:3000' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
|
||||||
|
for (const t of TARGETS) {
|
||||||
|
const ctx = await browser.newContext({
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
});
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
for (const p of PAGES) {
|
||||||
|
const url = t.base + p.url;
|
||||||
|
try {
|
||||||
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
// 截首屏(Hero + header 区域)
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(OUT, `${t.tag}-${p.name}-fold.png`),
|
||||||
|
clip: { x: 0, y: 0, width: 1440, height: 900 },
|
||||||
|
});
|
||||||
|
console.log(`[${t.tag}] ${p.name} OK`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[${t.tag}] ${p.name} FAILED ${e.message.slice(0, 100)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
await browser.close();
|
||||||
|
console.log('\n✓ 输出:' + OUT);
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* 模拟点深色模式按钮后的 header 截图
|
||||||
|
* 找切换按钮:theme toggle / data-theme / aria-label 含亮暗
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const OUT = path.join(process.cwd(), 'dogfood-ui-audit', 'logo-compare');
|
||||||
|
fs.mkdirSync(OUT, { recursive: true });
|
||||||
|
|
||||||
|
const TARGETS = [
|
||||||
|
{ tag: 'prod', url: 'https://www.novalon.cn/' },
|
||||||
|
{ tag: 'local', url: 'http://127.0.0.1:3000/' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
for (const t of TARGETS) {
|
||||||
|
const ctx = await browser.newContext({
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
});
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
try {
|
||||||
|
await page.goto(t.url, { waitUntil: 'networkidle', timeout: 60000 });
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
// 1) 找主题切换按钮
|
||||||
|
const btnInfo = await page.evaluate(() => {
|
||||||
|
const candidates = [
|
||||||
|
...document.querySelectorAll('header button, header [role="button"], [data-theme-toggle], [aria-label*="深" i], [aria-label*="暗" i], [aria-label*="dark" i], [aria-label*="theme" i], [aria-label*="切换" i]'),
|
||||||
|
];
|
||||||
|
return candidates.map(b => ({
|
||||||
|
tag: b.tagName,
|
||||||
|
label: b.getAttribute('aria-label'),
|
||||||
|
text: (b.textContent || '').slice(0, 30),
|
||||||
|
className: (b.className || '').toString().slice(0, 60),
|
||||||
|
data: JSON.stringify(b.dataset).slice(0, 80),
|
||||||
|
})).slice(0, 6);
|
||||||
|
});
|
||||||
|
console.log(`[${t.tag}] 候选切换按钮:`, JSON.stringify(btnInfo, null, 2));
|
||||||
|
|
||||||
|
// 2) 直接通过 localStorage 强制 dark(更稳)
|
||||||
|
await page.evaluate(() => {
|
||||||
|
try { localStorage.setItem('novalon-theme', 'dark'); } catch {}
|
||||||
|
});
|
||||||
|
await page.reload({ waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
// 3) 截深色模式下的 header
|
||||||
|
const header = page.locator('header').first();
|
||||||
|
await header.screenshot({ path: path.join(OUT, `${t.tag}-dark-header.png`) }).catch(async () => {
|
||||||
|
await page.screenshot({ path: path.join(OUT, `${t.tag}-dark-header.png`), clip: { x: 0, y: 0, width: 1440, height: 80 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4) 探测 logo 在深色模式下的实际颜色
|
||||||
|
const logoState = await page.evaluate(() => {
|
||||||
|
const img = [...document.querySelectorAll('img')].find(i => /logo\.svg$/.test(i.getAttribute('src') || ''));
|
||||||
|
if (!img) return { found: false };
|
||||||
|
const r = img.getBoundingClientRect();
|
||||||
|
const parent = img.closest('header') || document.body;
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
src: img.getAttribute('src'),
|
||||||
|
renderedW: Math.round(r.width),
|
||||||
|
renderedH: Math.round(r.height),
|
||||||
|
parentBg: getComputedStyle(parent).backgroundColor,
|
||||||
|
htmlClass: document.documentElement.className,
|
||||||
|
htmlDataTheme: document.documentElement.getAttribute('data-theme'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(`[${t.tag}] dark-mode logo state:`, JSON.stringify(logoState, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[${t.tag}] FAILED ${e.message.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
await browser.close();
|
||||||
|
console.log('\n✓ 输出:' + OUT);
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Logo 对比诊断:生产环境 vs 本地 dev
|
||||||
|
*
|
||||||
|
* 用法:node scripts/audit/logo-compare.mjs
|
||||||
|
* 输出:dogfood-ui-audit/logo-compare/{prod,local}-header.png + logo-meta.json
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const OUT = path.join(process.cwd(), 'dogfood-ui-audit', 'logo-compare');
|
||||||
|
fs.mkdirSync(OUT, { recursive: true });
|
||||||
|
|
||||||
|
const TARGETS = [
|
||||||
|
{ tag: 'prod', url: 'https://www.novalon.cn/' },
|
||||||
|
{ tag: 'local', url: 'http://127.0.0.1:3000/' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const meta = {};
|
||||||
|
|
||||||
|
for (const t of TARGETS) {
|
||||||
|
const ctx = await browser.newContext({
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
});
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
const errors = [];
|
||||||
|
page.on('requestfailed', r => errors.push('FAILED ' + r.url()));
|
||||||
|
page.on('console', m => { if (m.type() === 'error') errors.push('CONSOLE ' + m.text()); });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(t.url, { waitUntil: 'networkidle', timeout: 60000 });
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
// header logo 元素探测
|
||||||
|
const info = await page.evaluate(() => {
|
||||||
|
const imgs = [...document.querySelectorAll('img')]
|
||||||
|
.filter(i => /logo/i.test(i.getAttribute('src') || ''));
|
||||||
|
return imgs.map(i => {
|
||||||
|
const r = i.getBoundingClientRect();
|
||||||
|
const cs = getComputedStyle(i);
|
||||||
|
return {
|
||||||
|
src: i.getAttribute('src'),
|
||||||
|
currentSrc: i.currentSrc,
|
||||||
|
renderedW: Math.round(r.width),
|
||||||
|
renderedH: Math.round(r.height),
|
||||||
|
naturalW: i.naturalWidth,
|
||||||
|
naturalH: i.naturalHeight,
|
||||||
|
complete: i.complete,
|
||||||
|
fontFamily: cs.fontFamily,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 截 header 区域(放大到 logo 可见)
|
||||||
|
const header = page.locator('header').first();
|
||||||
|
await header.screenshot({ path: path.join(OUT, `${t.tag}-header.png`) }).catch(async () => {
|
||||||
|
await page.screenshot({ path: path.join(OUT, `${t.tag}-header.png`), clip: { x: 0, y: 0, width: 1440, height: 80 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
meta[t.tag] = { url: t.url, logos: info, errors };
|
||||||
|
console.log(`[${t.tag}] OK logos=${info.length} errors=${errors.length}`);
|
||||||
|
for (const l of info) {
|
||||||
|
console.log(` src=${l.src} currentSrc=${l.currentSrc}`);
|
||||||
|
console.log(` rendered=${l.renderedW}x${l.renderedH} natural=${l.naturalW}x${l.naturalH} complete=${l.complete}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
meta[t.tag] = { url: t.url, error: e.message.slice(0, 200), errors };
|
||||||
|
console.log(`[${t.tag}] FAILED ${e.message.slice(0, 120)}`);
|
||||||
|
}
|
||||||
|
await ctx.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(OUT, 'logo-meta.json'), JSON.stringify(meta, null, 2));
|
||||||
|
await browser.close();
|
||||||
|
console.log('\n✓ 输出目录:' + OUT);
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// 验证 header logo 明暗双版:浅色应显示 logo.svg(黑字),深色应显示 logo-white.svg(白字)
|
||||||
|
// 用法:node scripts/audit/logo-dark-verify.mjs
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
|
const BASE = 'http://127.0.0.1:3000';
|
||||||
|
const OUT = 'dogfood-output/logo-dark-verify';
|
||||||
|
|
||||||
|
const shots = [];
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
|
||||||
|
// 浅色(默认)
|
||||||
|
await page.goto(BASE, { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
const lightSrc = await page.$eval('header img', (el) => el.getAttribute('src'));
|
||||||
|
const visibleLight = await getVisible(page);
|
||||||
|
await page.screenshot({ path: `${OUT}/header-light.png` });
|
||||||
|
shots.push({ theme: 'light', htmlDataTheme: await page.getAttribute('html', 'data-theme'), firstImgSrc: lightSrc, visible: visibleLight });
|
||||||
|
|
||||||
|
// 深色:设 localStorage 后 reload(与站点内联脚本一致)
|
||||||
|
await page.evaluate(() => localStorage.setItem('novalon-theme', 'dark'));
|
||||||
|
await page.goto(BASE, { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
const visibleDark = await getVisible(page);
|
||||||
|
const darkSrc = await page.$eval('header img', (el) => el.getAttribute('src')).catch(() => 'N/A');
|
||||||
|
await page.screenshot({ path: `${OUT}/header-dark.png` });
|
||||||
|
shots.push({ theme: 'dark', htmlDataTheme: await page.getAttribute('html', 'data-theme'), firstImgSrc: darkSrc, visible: visibleDark });
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
console.log(JSON.stringify(shots, null, 2));
|
||||||
|
// 断言:浅色无 data-theme 属性(null),可见 logo.svg;深色 data-theme=dark,可见 logo-white.svg
|
||||||
|
const lightOk = shots[0].visible === 'logo.svg' && shots[0].htmlDataTheme !== 'dark';
|
||||||
|
const darkOk = shots[1].visible === 'logo-white.svg' && shots[1].htmlDataTheme === 'dark';
|
||||||
|
console.log(`\n[assert] light shows logo.svg: ${lightOk}`);
|
||||||
|
console.log(`[assert] dark shows logo-white.svg: ${darkOk}`);
|
||||||
|
console.log(`[result] ${lightOk && darkOk ? 'PASS' : 'FAIL'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回当前实际可见(display !== none)的 logo img 的 src
|
||||||
|
async function getVisible(page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const imgs = Array.from(document.querySelectorAll('header img'));
|
||||||
|
const vis = imgs.filter((i) => {
|
||||||
|
const s = getComputedStyle(i);
|
||||||
|
return s.display !== 'none' && s.visibility !== 'hidden' && Number(s.opacity) > 0;
|
||||||
|
});
|
||||||
|
return vis.length ? vis[0].getAttribute('src') : 'NONE_VISIBLE';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((e) => { console.error(e); process.exit(1); });
|
||||||
@@ -380,6 +380,17 @@ body {
|
|||||||
--grid-gap-2xl: 3rem;
|
--grid-gap-2xl: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Logo 明暗双版(html[data-theme] 驱动)===
|
||||||
|
站点主题由 html[data-theme='dark'] 属性驱动,而非 .dark class,
|
||||||
|
因此 Tailwind dark: 变体(darkMode:'class')不匹配 —— 用显式属性选择器切换。
|
||||||
|
- logo.svg(深黑字)用于浅色背景;logo-white.svg(白字)用于深色背景。
|
||||||
|
两个 <Image> 都在 DOM 内、按 data-theme 显隐(display:none,不占位),
|
||||||
|
无 JS 闪烁,未来主题切换器改 data-theme 时自动生效。
|
||||||
|
SSR 初始 HTML 无 data-theme → 默认显示 light 版,head 内联脚本同步设置
|
||||||
|
data-theme 后即时切换,不产生重叠。 */
|
||||||
|
html[data-theme='dark'] .logo-on-light { display: none; }
|
||||||
|
html:not([data-theme='dark']) .logo-on-dark { display: none; }
|
||||||
|
|
||||||
/* === 暗黑模式(html[data-theme='dark'])===
|
/* === 暗黑模式(html[data-theme='dark'])===
|
||||||
策略:仅覆盖 CSS 变量,组件无需逐元素反色。
|
策略:仅覆盖 CSS 变量,组件无需逐元素反色。
|
||||||
- 文字墨色 --color-ink 反色为浅色(其语义收窄为"文字/描边跟随主题")
|
- 文字墨色 --color-ink 反色为浅色(其语义收窄为"文字/描边跟随主题")
|
||||||
|
|||||||
@@ -96,16 +96,27 @@ function HeaderContent() {
|
|||||||
className="relative flex items-center group/logo"
|
className="relative flex items-center group/logo"
|
||||||
aria-label="返回首页"
|
aria-label="返回首页"
|
||||||
>
|
>
|
||||||
|
{/* Logo 明暗双版:浅色背景用 logo.svg(黑字),深色背景用 logo-white.svg(白字)。
|
||||||
|
按 html[data-theme] 显隐(规则见 globals.css),修复黑字在深色背景被吞的 bug。 */}
|
||||||
<Image
|
<Image
|
||||||
src="/logo.svg"
|
src="/logo.svg"
|
||||||
alt={config.name}
|
alt={config.name}
|
||||||
width={160}
|
width={160}
|
||||||
height={40}
|
height={40}
|
||||||
className="relative transition-all duration-300 ease-out group-hover/logo:scale-[1.02] w-auto h-8 md:h-10"
|
className="logo-on-light relative transition-all duration-300 ease-out group-hover/logo:scale-[1.02] w-auto h-8 md:h-10"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
priority
|
priority
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
/>
|
/>
|
||||||
|
<Image
|
||||||
|
src="/logo-white.svg"
|
||||||
|
alt={config.name}
|
||||||
|
width={160}
|
||||||
|
height={40}
|
||||||
|
className="logo-on-dark relative transition-all duration-300 ease-out group-hover/logo:scale-[1.02] w-auto h-8 md:h-10"
|
||||||
|
loading="eager"
|
||||||
|
suppressHydrationWarning
|
||||||
|
/>
|
||||||
</StaticLink>
|
</StaticLink>
|
||||||
|
|
||||||
<nav className="hidden md:flex items-center gap-0.5" role="navigation" aria-label="主导航" data-testid="desktop-navigation">
|
<nav className="hidden md:flex items-center gap-0.5" role="navigation" aria-label="主导航" data-testid="desktop-navigation">
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ module.exports = {
|
|||||||
mono: ['var(--font-mono)'],
|
mono: ['var(--font-mono)'],
|
||||||
brand: ['var(--font-brand)'],
|
brand: ['var(--font-brand)'],
|
||||||
serif: ['var(--font-serif)'],
|
serif: ['var(--font-serif)'],
|
||||||
|
// 书法体:globals.css 的 --font-calligraphy-system(STKaiti / KaiTi 系统字栈)
|
||||||
|
// 此前 fontFamily 未声明该 key,导致 font-calligraphy utility 不生成,
|
||||||
|
// 4 处装饰书法字(why-us / challenge / brand-visuals)静默回退 sans。
|
||||||
|
calligraphy: ['var(--font-calligraphy-system)'],
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
caption: 'var(--font-size-caption)',
|
caption: 'var(--font-size-caption)',
|
||||||
|
|||||||
Reference in New Issue
Block a user