Просмотр исходного кода

fix: preserve full Chinese glyphs in report headings

Remove Latin cap-height trimming from Chinese titles and controls, let category labels wrap, and keep report headers in sync with the selected category. Verify five viewport widths and refresh the v2 deployment package.
pengzhikang 1 день назад
Родитель
Сommit
20bfb2f66e

+ 39 - 3
deploy/five-views-files-20261103.json

@@ -1,12 +1,12 @@
 {
   "version": "ksf.five-views.publish.v1",
-  "release": "ksf.five-views.20261103.v1",
+  "release": "ksf.five-views.20261103.v2",
   "homepageSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
   "files": [
     {
       "path": "src/layout/kf-app.js",
-      "bytes": 31003,
-      "sha256": "c61882cb4f0763cb85dea64638ca2c41d8dd295deb7d85ec9eb60613d7bd201a",
+      "bytes": 31169,
+      "sha256": "068f8b5eea1c68af00b5006ea698f853811ced50b31b911ceab030265691330d",
       "allowMissing": false,
       "priorNormalizedSha256": [
         "ac936167c2820fc2cd8a122cd3686ad741bfde2b7628f59bd1e6de157df77272"
@@ -194,6 +194,42 @@
       "sha256": "1806c29f2832831f5621f206b188223e9c8fed9d21797ca06aa267d214770252",
       "allowMissing": true,
       "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/styles/tokens.js",
+      "bytes": 34664,
+      "sha256": "cafec263fd40b78f8fbe5f6d3da46ef1f089fa9e308467e3b811d9d88baba118",
+      "allowMissing": false,
+      "priorNormalizedSha256": [
+        "3f931625d7b9063a8174df5b623fd05f33dcea264812f87350d771a603e5fc2a"
+      ]
+    },
+    {
+      "path": "src/layout/kf-topbar.js",
+      "bytes": 6049,
+      "sha256": "d29b76bf0835f24d486569d410bb75531f476472aa4d0deb83a800430062140d",
+      "allowMissing": false,
+      "priorNormalizedSha256": [
+        "746ff49c7d66b24aa0ba18810cae7cfc795b830c840d38ab7da1decd1cced4b9"
+      ]
+    },
+    {
+      "path": "src/pages/view-reports.js",
+      "bytes": 12809,
+      "sha256": "42d56532ff79e930711c1c31dcea1f4e310e92aa06766d34653ff10027fcefa1",
+      "allowMissing": false,
+      "priorNormalizedSha256": [
+        "351a851459320077b17a7d8648dc736ff8f98ec4762f98dabb18bb12e4cdd55f"
+      ]
+    },
+    {
+      "path": "src/pages/view-report-type.js",
+      "bytes": 19915,
+      "sha256": "d6f43d2f425e54cf4898945e5cc2254341ef0216b35fecc5d9978334e87002b9",
+      "allowMissing": false,
+      "priorNormalizedSha256": [
+        "cbdcc7e9626a583ab26d1db8466d80bd06d2ee12ccb38892fd912bdc750cf61c"
+      ]
     }
   ],
   "preserved": [

+ 36 - 0
deploy/five-views-production-baseline-20261103.json

@@ -216,6 +216,42 @@
       "status": 200,
       "present": false,
       "htmlFallback": true
+    },
+    {
+      "path": "src/styles/tokens.js",
+      "bytes": 34749,
+      "rawSha256": "3f931625d7b9063a8174df5b623fd05f33dcea264812f87350d771a603e5fc2a",
+      "normalizedSha256": "3f931625d7b9063a8174df5b623fd05f33dcea264812f87350d771a603e5fc2a",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true,
+      "at": "2026-11-03T11:01:17.333Z"
+    },
+    {
+      "path": "src/layout/kf-topbar.js",
+      "bytes": 5975,
+      "rawSha256": "746ff49c7d66b24aa0ba18810cae7cfc795b830c840d38ab7da1decd1cced4b9",
+      "normalizedSha256": "746ff49c7d66b24aa0ba18810cae7cfc795b830c840d38ab7da1decd1cced4b9",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true,
+      "at": "2026-11-03T11:01:17.296Z"
+    },
+    {
+      "path": "src/pages/view-reports.js",
+      "bytes": 12660,
+      "rawSha256": "351a851459320077b17a7d8648dc736ff8f98ec4762f98dabb18bb12e4cdd55f",
+      "normalizedSha256": "351a851459320077b17a7d8648dc736ff8f98ec4762f98dabb18bb12e4cdd55f",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true,
+      "at": "2026-11-03T11:01:17.298Z"
+    },
+    {
+      "path": "src/pages/view-report-type.js",
+      "bytes": 19646,
+      "rawSha256": "cbdcc7e9626a583ab26d1db8466d80bd06d2ee12ccb38892fd912bdc750cf61c",
+      "normalizedSha256": "cbdcc7e9626a583ab26d1db8466d80bd06d2ee12ccb38892fd912bdc750cf61c",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true,
+      "at": "2026-11-03T11:01:17.299Z"
     }
   ]
 }

+ 6 - 6
docs/showcase/deploy.md

@@ -1,10 +1,10 @@
 # 运维001:五端演示与介绍页部署
 
-本包尚未部署。只更新26个静态文件,不执行业务函数、不改数据库、首页、nginx、Schema、ACL、Skill或wrapper,不需要OBS凭据、服务密钥或重启。
+本包为修复中文裁切后的v2版本,替代此前r1包,勿先后叠加发布。本包尚未部署。只更新30个静态文件,不执行业务函数、不改数据库、首页、nginx、Schema、ACL、Skill或wrapper,不需要OBS凭据、服务密钥或重启。
 
 ## 包与目录
 
-发布版本:`ksf.five-views.20261103.v1`。脚本需要Node.js 20或以上,无npm安装步骤。
+发布版本:`ksf.five-views.20261103.v2`。脚本需要Node.js 20或以上,无npm安装步骤。
 
 将zip解压到站点以外的新目录,例如`/opt/ksf-five-views-20261103/`。不要解压到站点内。进入解包根目录,执行`sha256sum -c FILES.sha256`,必须全部通过。
 
@@ -17,7 +17,7 @@ node scripts/publish-five-views.mjs --target-dir /opt/kangshifu-web
 node scripts/publish-five-views.mjs --target-dir /opt/kangshifu-web --apply
 ```
 
-第一条默认只读,核对`filesChecked=26`、changes清单与manifest一致。第二条会在站点外创建独立备份目录,先备份全部旧文件,再替换。留存两次完整stdout/stderr/exitcode。任何失败即停,不覆盖未知版本、不改manifest绕过检查。
+第一条默认只读,核对`filesChecked=30`、changes清单与manifest一致。第二条会在站点外创建独立备份目录,先备份全部旧文件,再替换。留存两次完整stdout/stderr/exitcode。任何失败即停,不覆盖未知版本、不改manifest绕过检查。
 
 脚本要求首页SHA256为`dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3`;保留既有公开机会卡、评分、报告与证据修复的五个文件指纹。既有共享文件接受已核验的LF归一化旧指纹;包内新文件及执行后文件使用原始字节SHA256精确核对。
 
@@ -25,13 +25,13 @@ node scripts/publish-five-views.mjs --target-dir /opt/kangshifu-web --apply
 
 ## 外部HTTPS与浏览器验收
 
-1. 按manifest对26个文件做HTTPS GET并计算原始SHA256;JSON应为JSON、JS应为JS,不能把SPA的200 HTML兜底当作文件成功。首页哈希保持不变。
+1. 按manifest对30个文件做HTTPS GET并计算原始SHA256;JSON应为JSON、JS应为JS,不能把SPA的200 HTML兜底当作文件成功。首页哈希保持不变。
 2. 打开并刷新`/showcase`与其五个介绍页:`/showcase/leadership`、`/showcase/research`、`/showcase/collaboration`、`/showcase/validation`、`/showcase/classroom`。五张图与五端入口均可用。
 3. 打开`/cockpit`、`/intel`、`/coop`、`/review`、`/class`。A端12项目/36人/24卡/44报告/3受阻;B端12,000声音/720商品/360引文;课堂18单作业。基线334条,扩展16,446条另计。
 4. 用模拟卡验证本机状态和评分、同名重评,打开来源、报告、四条价格,刷新保留,重置后只清本机演示修改。用浏览器网络日志确认新路由没有生产业务请求。
-5. 检查P01/P03/P10动线、小组/成员筛选、分页、手机布局和旧看板入口。旧报告错误链接应自动恢复;原公开卡详情应正常。对生产状态/评分只检查入口与合同,未经另外指示不为验收改真实记录。
+5. 检查报告中心标题与五类名称的上下笔画完整,点击分类后页标题正确,再检查P01/P03/P10动线、小组/成员筛选、分页、手机布局和旧看板入口。旧报告错误链接应自动恢复;原公开卡详情应正常。对生产状态/评分只检查入口与合同,未经另外指示不为验收改真实记录。
 
-本机已完成69项Node测试、13项静态检查及95项Edge验收;详见`docs/showcase/validation-result.json`。本机回归中的旧公开操作由浏览器拦截并本地回放,生产请求实际发送数为0。本机验证不等于生产验收:部署回执和生产浏览器验收后才能记录`browserVerified=true`。
+本机已完成69项Node测试、13项静态检查及115项Edge验收;详见`docs/showcase/validation-result.json`。本机回归中的旧公开操作由浏览器拦截并本地回放,生产请求实际发送数为0。本机验证不等于生产验收:部署回执和生产浏览器验收后才能记录`browserVerified=true`。
 
 ## 回退
 

+ 2 - 0
docs/showcase/design.md

@@ -7,3 +7,5 @@
 原有看板导航新增模拟入口,手机保留五个原主Tab,另加模拟介绍快捷入口。链接均保留部署前缀,支持根路径或子目录部署与刷新。
 
 介绍及演示外壳在生产数据判断之前渲染,生产服务不可用时仍可进入静态演示。署名不作为身份认证,所有新增资料明确标模拟。
+
+中文标题、分类名与控件保留完整行盒,不按拉丁大写字母高度裁剪。窄屏页面标题自然换行,分类名不叠加裁剪与省略。

+ 2 - 0
docs/showcase/test.md

@@ -7,3 +7,5 @@ Node:现有模型、报告与公开接口合同回归;基础334条计数、
 浏览器验收拦截所有外部请求。演示与介绍操作不产生外部业务请求;返回旧路由的公开读取使用本机回放,生产请求发送数为0。本机验证不等于生产浏览器验收。
 
 部署工具默认只读计划;以临时站点验证发布、重复发布、回退、未知版本拒绝、并发变更和失败后恢复。生产实际发布与外网验证留给运维001。
+
+中文裁切回归:先复现报告中心14.5px行盒小于19px字形、分类名10.2px行盒小于14—15px字形的问题;修复后测量2048/1280/1024/768/390五种宽度,比较Canvas实际字形高度与DOM可用行盒,确认完整显示并无页面横向溢出。分类当前标签使用正确aria-current值。

+ 12 - 4
docs/showcase/validation-result.json

@@ -1,5 +1,5 @@
 {
-  "version": "ksf.five-views.validation.20261103.v1",
+  "version": "ksf.five-views.validation.20261103.v2",
   "nodeTests": {
     "total": 69,
     "passed": 69,
@@ -11,12 +11,20 @@
   },
   "browser": {
     "engine": "Microsoft Edge headless",
-    "checks": 95,
-    "passed": 95,
+    "checks": 115,
+    "passed": 115,
     "errors": [],
     "productionRequestsSent": 0,
     "externalTrafficIntercepted": true,
-    "productionBrowserVerified": false
+    "productionBrowserVerified": false,
+    "typographyWidths": [
+      2048,
+      1280,
+      1024,
+      768,
+      390
+    ],
+    "chineseGlyphClipping": 0
   },
   "scope": {
     "baseline": 334,

+ 2 - 1
scripts/publish-five-views.mjs

@@ -3,7 +3,7 @@ import path from 'node:path';
 import crypto from 'node:crypto';
 import { fileURLToPath } from 'node:url';
 
-export const RELEASE = 'ksf.five-views.20261103.v1';
+export const RELEASE = 'ksf.five-views.20261103.v2';
 export const FILES = Object.freeze([
   'src/layout/kf-app.js', 'src/layout/kf-nav.js', 'src/layout/sample-shell.js',
   'src/utils/router.js', 'src/utils/http.js', 'src/services/workshop-data.js',
@@ -16,6 +16,7 @@ export const FILES = Object.freeze([
   'src/assets/showcase/leadership.png', 'src/assets/showcase/research.png',
   'src/assets/showcase/collaboration.png', 'src/assets/showcase/validation.png',
   'src/assets/showcase/classroom.png', 'src/assets/showcase/preview-seed.json', 'src/assets/showcase/demo-extension.json',
+  'src/styles/tokens.js', 'src/layout/kf-topbar.js', 'src/pages/view-reports.js', 'src/pages/view-report-type.js',
 ]);
 const PRESERVED = ['src/services/board-service.js', 'src/components/kf-opportunity-card.js',
   'src/pages/view-pool.js', 'src/core/report.js', 'src/core/evidence/chain.js'];

+ 22 - 1
scripts/verify-showcase-browser.mjs

@@ -67,7 +67,8 @@ try {
     return value.result.value;
   };
   const fixture = { result: { ok: true, data: { project: [], artifact: [], insight: [], source: [], session: [], opportunity: [], asOf: '2026-11-03T00:00:00.000Z', nextCursor: null } } };
-  fixture.result.data.artifact.push({ objectId: 'fixture-report', title: '原有报告回归', stageCode: 'S4', report: { title: '原有报告回归', chapters: [{ id: 'scope', title: '核对原文', sections: [{ kind: 'para', text: '公开报告阅读回归正文。' }] }], evidence: {} } });
+  fixture.result.data.artifact.push({ objectId: 'fixture-report', title: '原有报告回归', stageCode: 'S4', artifactType: 'competitor_report', report: { title: '原有报告回归', chapters: [{ id: 'scope', title: '核对原文', sections: [{ kind: 'para', text: '公开报告阅读回归正文。' }] }], evidence: {} } });
+  for (const type of ['category_report', 'voc_report', 'pain_list', 'concept']) fixture.result.data.artifact.push({ objectId: 'fixture-' + type, artifactType: type, title: '中文标题完整显示与多行排版验证', stageCode: 'S1', rawText: '本机回放排版测试资料。', createdAt: '2026-11-03T00:00:00.000Z' });
   fixture.result.data.opportunity.push({ objectId: 'fixture-opportunity', title: '原有机会卡回归', status: 'candidate', linkedArtifacts: ['fixture-report'], scores: [], updatedAt: '2026-11-03T00:00:00.000Z' });
   socket.addEventListener('message', ({ data }) => {
     const m = JSON.parse(data);
@@ -264,6 +265,26 @@ try {
   check('existing live route resumes loading', forbidden.some((r) => r.body.includes('ksfGetPublicBoard')));
   check('existing desktop and mobile navigation expose base-safe demo entry', await evaluate(`(()=>{const root=document.querySelector('kf-app').shadowRoot.querySelector('kf-nav').shadowRoot;return root.querySelector('a[data-path="/showcase"]').href.includes('/preview-test/showcase')&&root.querySelector('.showcase-entry').href.includes('/preview-test/showcase');})()`));
   const legacyNavigate = async (route) => { await evaluate(`import(${JSON.stringify(origin + mount + 'src/utils/router.js')}).then(m=>m.navigate(${JSON.stringify(route)}))`); await pause(450); };
+  const measureTitles = async () => evaluate(`(()=>{
+    const root=document.querySelector('kf-app').shadowRoot;
+    const headings=[root.querySelector('kf-topbar').shadowRoot.querySelector('h1'),...root.querySelector('main').firstElementChild.shadowRoot.querySelectorAll('[data-type-card] h3,.head h2')];
+    const canvas=document.createElement('canvas'),ctx=canvas.getContext('2d');
+    return headings.map(el=>{const s=getComputedStyle(el),r=el.getBoundingClientRect();ctx.font=s.fontWeight+' '+s.fontSize+' '+s.fontFamily;const glyph=ctx.measureText(el.textContent);return {text:el.textContent,height:r.height,glyphHeight:glyph.actualBoundingBoxAscent+glyph.actualBoundingBoxDescent,trim:s.textBoxTrim,clipped:r.height+0.5<glyph.actualBoundingBoxAscent+glyph.actualBoundingBoxDescent};});
+  })()`);
+  for (const width of [2048, 1280, 1024, 768, 390]) {
+    await cdp('Emulation.setDeviceMetricsOverride', { width, height: 1000, deviceScaleFactor: 1, mobile: width < 768 });
+    await legacyNavigate('/reports');
+    await evaluate('document.fonts.ready');
+    const headings = await measureTitles(); observations.push({ reportWidth: width, headings });
+    if (width === 2048 || width === 390) await shot('reports-typography-' + width);
+    check(`reports ${width}px: Chinese heading boxes retain the complete glyph height`, headings.length === 6 && headings.every(x => !x.clipped), headings);
+    check(`reports ${width}px: no page horizontal overflow`, await evaluate('document.documentElement.scrollWidth<=innerWidth+1'));
+    await evaluate(`document.querySelector('kf-app').shadowRoot.querySelector('view-reports').shadowRoot.querySelector('[data-type-card="voc"]').click()`); await pause(250);
+    const typeHeadings = await measureTitles();
+    check(`report category ${width}px: heading and classification retain full glyphs`, typeHeadings.length === 2 && typeHeadings.every(x => !x.clipped), typeHeadings);
+    check(`report category ${width}px: active tab is exposed correctly`, await evaluate(`document.querySelector('kf-app').shadowRoot.querySelector('view-report-type').shadowRoot.querySelector('.tab.cur').getAttribute('aria-current')==='page'`));
+  }
+  await cdp('Emulation.setDeviceMetricsOverride', { width: 1440, height: 1000, deviceScaleFactor: 1, mobile: false });
   await legacyNavigate('/pool/fixture-report');
   check('legacy wrong report link recovers to report route', await evaluate(`location.pathname==='/preview-test/report/fixture-report'&&Boolean(document.querySelector('kf-app').shadowRoot.querySelector('view-report'))`));
   await legacyNavigate('/pool/fixture-opportunity');

+ 3 - 2
src/layout/kf-app.js

@@ -14,7 +14,7 @@ import { t } from '../components/cjk.js';
 import { boardService } from '../services/board-service.js';
 import { currentPath, currentQuery, subscribe as subscribeRoute, navigate, href, onLinkClick, match } from '../utils/router.js';
 import { CONFIG } from '../app.config.js';
-import { traceArtifact, traceInsight, reportEvidence, conclusionToLedger, fmtDate, fmtAgo, STAGES, OPP_STATUS } from '../core/index.js';
+import { traceArtifact, traceInsight, reportEvidence, conclusionToLedger, fmtDate, fmtAgo, STAGES, OPP_STATUS, reportTypeOf } from '../core/index.js';
 import '../components/index.js';
 import './kf-nav.js';
 import './kf-topbar.js';
@@ -451,7 +451,8 @@ export class KfApp extends SignalWatcher(LitElement) {
     if (this._route.name === 'showcase') {
       return html`<view-showcase .system=${this._route.params.system || 'overview'} .routeQuery=${this._query}></view-showcase>`;
     }
-    const [title, sub] = TITLES[this._route.name] || TITLES.notfound;
+    const reportType = this._route.name === 'reports' ? reportTypeOf(this._route.params.type) : null;
+    const [title, sub] = reportType ? [reportType.name, '报告中心'] : TITLES[this._route.name] || TITLES.notfound;
     const db = this.#svc.db.get();
     const drawer = this.#drawerContent();
     const flash = this.#svc.flash.get();

+ 1 - 1
src/layout/kf-topbar.js

@@ -40,7 +40,7 @@ export class KfTopbar extends LitElement {
     /* 顶栏(v15):实底白 + 下缘 1px 发丝线。sticky 浮在内容之上,给与卡片一致的实底,不用玻璃。 */
     .bar { display: flex; align-items: center; gap: 14px; padding: 12px 28px; background: var(--kf-surface); border: 1px solid var(--kf-line-soft); border-top: 0; border-radius: 0 0 var(--r-md) var(--r-md); }
     .t { min-width: 0; flex: 1; }
-    h1 { margin: 0; font-size: 20px; font-weight: 600; color: var(--kf-ink); letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-box-trim: trim-both; text-box-edge: cap alphabetic; }
+    h1 { margin: 0; font-size: 20px; line-height: 1.5; font-weight: 600; color: var(--kf-ink); letter-spacing: -0.01em; overflow-wrap: anywhere; text-box-trim: none; text-box-edge: auto; }
     .s { font-size: 12px; color: var(--kf-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
     .sync { font-size: 12px; color: var(--kf-ink-3); display: flex; align-items: center; gap: 6px; }
     /* 视图可以把「页面级切换」放进顶栏(typescript 类型页的类型 tab):省掉一整行,首屏直接是内容 */

+ 2 - 2
src/pages/view-report-type.js

@@ -61,7 +61,7 @@ export class ViewReportType extends LitElement {
     .head .ico { width: 40px; height: 40px; flex: none; border-radius: var(--r-md); display: grid; place-items: center;
       background: var(--kf-accent-soft); color: var(--kf-accent-ink); }
     .head .ico svg { stroke: var(--kf-accent-ink); }
-    .head h2 { margin: 0; font-size: 20px; line-height: 1.3; font-weight: 600; color: var(--kf-ink); letter-spacing: -0.02em; text-box-trim: trim-both; text-box-edge: cap alphabetic; }
+    .head h2 { margin: 0; font-size: 20px; line-height: 1.5; font-weight: 600; color: var(--kf-ink); letter-spacing: -0.02em; text-box-trim: none; text-box-edge: auto; }
     .kpis { margin-left: auto; display: flex; gap: 8px; }
     /* 数据卡 = 内容卡同一层玻璃(v11 板块六:.72 + 1px 描边走阴影 + blur(8px)) */
     .kpi { min-width: 112px; padding: 8px 12px; border-radius: var(--kf-radius-sm); display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; gap: 6px 10px; align-items: end;
@@ -206,7 +206,7 @@ export class ViewReportType extends LitElement {
       <!-- 类型切换进顶栏(v11 3.4:页面顶 → 首个区块 16px,不再先占一行 tab 再占一行标题);
            slot 进 <kf-topbar> 的 actions,DOM 仍在视图里(链接与横向滑动逻辑不变) -->
       <nav slot="actions" class="tabs" aria-label="报告类型" ${ref((el) => { this._tabsEl = el; })}>
-        ${REPORT_TYPES.map((t) => html`<a class="tab ${t.key === type.key ? 'cur' : ''}" href=${href(`/reports/${t.key}`)} data-link ?aria-current=${t.key === type.key ? 'page' : 'false'}>${t.name}<span class="n">${counts[t.key]}</span></a>`)}
+        ${REPORT_TYPES.map((t) => html`<a class="tab ${t.key === type.key ? 'cur' : ''}" href=${href(`/reports/${t.key}`)} data-link aria-current=${t.key === type.key ? 'page' : 'false'}>${t.name}<span class="n">${counts[t.key]}</span></a>`)}
       </nav>
 
       <div class="head">

+ 3 - 3
src/pages/view-reports.js

@@ -70,9 +70,9 @@ export class ViewReports extends LitElement {
     .t .h { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; row-gap: 4px; }
     .t .ico { width: 32px; height: 32px; border-radius: var(--r-md); display: grid; place-items: center; color: var(--kf-accent-ink);
       background: var(--kf-accent-soft); flex: none; }
-    /* 长标签不截断(5.6):两行折行,不再 nowrap + 省略号 */
-    .t h3 { margin: 0; font-size: 14px; line-height: 1.35; font-weight: 600; color: var(--kf-ink); letter-spacing: var(--ls-cn-display);
-      word-break: keep-all; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-width: 0; }
+    /* 分类名较短,按中文完整行盒自然换行,避免裁掉上下笔画。 */
+    .t h3 { margin: 0; font-size: 14px; line-height: 1.5; font-weight: 600; color: var(--kf-ink); letter-spacing: var(--ls-cn-display);
+      word-break: keep-all; overflow-wrap: anywhere; min-width: 0; text-box-trim: none; text-box-edge: auto; }
     /* 份数在基线对齐的行里会「沉底」:给整片一个固定行为基准,数值行与阶段条行同一网格 */
     .t .n { margin-left: auto; display: flex; align-items: baseline; gap: 6px; }
     .t .n kf-spark { align-self: center; }

+ 9 - 13
src/styles/tokens.js

@@ -169,17 +169,14 @@ export const TOKENS_CSS = `:root, :host {
   --fw-body: 400; --fw-medium: 500; --fw-bold: 600;
 
   /* ===== 中文排版(v11 板块一)=====
-     1.1 text-box 行盒修剪:中文字体自带半行距,padding 16px 的实际视觉留白是 16 + 半行距(约 0.4em),
-         永远对不准 4pt 栅格。单行文本容器统一裁到 cap→alphabetic(40px 中文行盒 55 → 30px)。
-         禁用于多行正文(会破坏行距)。
+     1.1 中文保留完整行盒。拉丁 cap→alphabetic 的裁剪高度小于汉字字面,
+         与 overflow/line-clamp 叠加时会裁掉上下笔画;间距由 padding 和 line-height 控制。
      1.5 行高层级:正文 1.75(CLReq 行间隙取 50%–100% 中位);标题 1.3;大数字 1。
          字距:正文 0(中文是满格方块,加字距反而散);11–13px 全中文小标题 +0.02em;≥28px 大标题 -0.02em;
          中文负字距上限 -0.03em,超过笔画粘连(西文可用 -4.75%,中文不行)。 */
   --lh-body: 1.75; --lh-card: 1.6; --lh-title: 1.3;
   --ls-cn-small: 0.02em; --ls-cn-body: 0; --ls-cn-display: -0.02em;
-  /* v11b:trim 只写两个**独立长属性**。简写 text-box: trim-both cap alphabetic 在 Chrome 153 被整条丢弃
-     (computed 只剩 cap alphabetic,行盒 32px 不缩);长属性写法实测 .b 行盒 32 → 20.95px。 */
-  --tb-trim: trim-both; --tb-edge: cap alphabetic;
+  --tb-trim: none; --tb-edge: auto;
 
   /* 间距层级(v11b 第三/四节,四档,可按跨级关系校验):卡内 16 / 卡内元素 12 / 卡间 24 / 区块间 48 / 页顶→首块 16 */
   --kf-space-card: 16px; --kf-space-in: 12px; --kf-space-card-gap: 24px; --kf-space-section: 48px; --kf-space-top: 16px;
@@ -369,7 +366,7 @@ summary:focus:not(:focus-visible), [tabindex]:focus:not(:focus-visible) { outlin
   border: 1px solid var(--kf-line-soft);
   background: var(--kf-surface-btn);
   color: var(--kf-ink); font: 500 14px/1 var(--kf-font); cursor: pointer;
-  text-box-trim: trim-both; text-box-edge: cap alphabetic;
+  text-box-trim: none; text-box-edge: auto;
   transition: var(--kf-t);
 }
 .kf-btn:hover { background: #fff; border-color: rgba(0, 0, 0, 0.14); }
@@ -391,7 +388,7 @@ summary:focus:not(:focus-visible), [tabindex]:focus:not(:focus-visible) { outlin
   border-radius: var(--r-sm); border: 1px solid var(--kf-line-soft);
   background: var(--kf-surface);
   font: 500 14px/1 var(--kf-font); color: var(--kf-ink-2); cursor: pointer;
-  text-box-trim: trim-both; text-box-edge: cap alphabetic;
+  text-box-trim: none; text-box-edge: auto;
   transition: var(--kf-t);
 }
 .kf-chip:hover { background: var(--kf-surface-btn); border-color: rgba(0, 0, 0, 0.14); color: var(--kf-ink); }
@@ -404,7 +401,7 @@ summary:focus:not(:focus-visible), [tabindex]:focus:not(:focus-visible) { outlin
 .kf-muted { color: var(--kf-ink-3); }
 .kf-num, .kf-mono { font-variant-numeric: tabular-nums; }
 .kf-mono { font-family: var(--kf-mono); }
-.kf-kicker { font-size: var(--fs-xs); line-height: 1.3; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3); font-weight: var(--fw-medium); text-box-trim: trim-both; text-box-edge: cap alphabetic; }
+.kf-kicker { font-size: var(--fs-xs); line-height: 1.3; letter-spacing: var(--ls-cn-small); color: var(--kf-ink-3); font-weight: var(--fw-medium); }
 .kf-title { color: var(--kf-ink); font-weight: var(--fw-bold); letter-spacing: -0.01em; line-height: 1.3; }
 /* 主数值:32 / 600 / 1.1 / -0.02em / tabular-nums —— 决定「高管级」观感的那一档 */
 .kf-metric { font-size: var(--fs-xl); font-weight: var(--fw-bold); line-height: 1.1; letter-spacing: -0.02em;
@@ -415,11 +412,10 @@ summary:focus:not(:focus-visible), [tabindex]:focus:not(:focus-visible) { outlin
 /* ============================================================
  * 中文排版工具类(v11 板块一)
  * ============================================================ */
-/* 1.1 单行文本容器:行盒修剪(只用于单行;多行正文禁用,会破坏行距)。
-   用 text-box-trim / text-box-edge 两个长属性,不用 text-box 简写(v11b 实测简写被 Chrome 丢弃 trim-both)。 */
-.tb-cap, .kf-title, .kf-metric, .kf-metric-label, .kf-kicker, .kf-btn, .kf-chip,
+/* 中文标题与控件保留字面高度;纯数字的紧凑度由数值组件单独控制。 */
+.tb-cap, .kf-title, .kf-metric-label, .kf-kicker, .kf-btn, .kf-chip,
 h1, h2, h3 {
-  text-box-trim: trim-both; text-box-edge: cap alphabetic;
+  text-box-trim: none; text-box-edge: auto;
 }
 .kf-title { letter-spacing: var(--ls-cn-display); line-height: 1.25; }
 /* 1.2 单位分层:小一号、灰一档、留 0.12em —— 数字成为视觉主角 */