feat(ui): 星盘/星历/报告中心并入 app 外壳,侧栏不再消失
Independent Staging Quality Gate / validate (push) Failing after 9m57s
Independent Staging Quality Gate / publish (push) Skipped

三个页面此前各是脱离外壳的独立全屏路由,各写了一套一样的
*-shell / *-topbar(只有一个「返回对话」链接)/ *-hero 骨架,
侧栏在打开它们的瞬间整个消失。

新增 AppNavRail(只读:两个 GET,零写操作)与 SecondaryShell。
会话行走 sessionHref → /?c=<uuid>,跳转复用现有侧栏的
persistLoginSessionReturn + location.assign,行为完全一致。

刻意不带重命名/收藏/归档/删除与账户菜单:那套连着 Home() 的乐观更新与
回滚层,为四个路由把它整体上提远超需要,且会撞 useState 增长门禁。

四个路由渲染标记完全不变:/ ○、/chart ○、/ephemeris ○、/reports ƒ。
CSS gzip 39,825→39,727(−0.25%);per-route JS 体积构建不输出,
已作为口径缺口记录。

两处自身问题被测试抓到并修复:usePathname() 在 app-router 上下文外
返回 null(类型说是 string)、fetch 在 jsdom 里可能不存在。
另差点随 hero 一起丢掉 BUG-717 的 eyebrow 文案(成本与速度承诺,
会印在失败页上),已放回 tab 行下方。

/reports/[reportId] 留给 R6 与目录一起做;根边界页保留「返回对话」,
它们不得 import globals.css,挂不了外壳。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
Jesse_Chen
2026-09-16 06:03:30 +00:00
co-authored by Claude Opus 5
parent b3ea8c336e
commit 50ce02c837
15 changed files with 680 additions and 263 deletions
+18 -13
View File
@@ -127,21 +127,21 @@ test("sidebar adds 星盘 and 星历 after 新建对话 without renaming 我的
});
test("mobile width keeps two centre lines and moves the full parameter card below the chart", () => {
assert.match(
globalStyles,
/@media \(max-width: 767px\) \{\r?\n \.chart-page-hero h1[\s\S]*\.chart-page-center-compact \{ display: block; \}/,
);
assert.match(
globalStyles,
/@media \(max-width: 767px\) \{\r?\n \.chart-page-hero h1[\s\S]*\.chart-page-center-extra \{ display: none; \}/,
);
assert.match(
globalStyles,
/@media \(max-width: 767px\) \{\r?\n \.chart-page-hero h1[\s\S]*\.chart-page-params-below \{ display: block; \}/,
// 原值:三条断言都以 `@media (max-width: 767px) { .chart-page-hero h1` 起锚,
// 再加一条 `.chart-page-back` 的 44px 触摸目标。
// 新值:直接断那个断点块里的三个开关;`.chart-page-back` 一条删除。
// 原因:hero 与「返回对话」按钮随页面并入 app 外壳而删除(T4.2)。被断言的三个
// 开关本身一条没动,只是不再有 hero 那一行可以用来起锚。返回路径现在是
// 常驻侧栏,它的触摸目标由 sidebar-contract 管。
const mobile = globalStyles.slice(
globalStyles.indexOf("@media (max-width: 767px)", globalStyles.indexOf(".chart-page-center-compact")
- 2000),
);
assert.match(mobile, /\.chart-page-center-compact \{ display: block; \}/);
assert.match(mobile, /\.chart-page-center-extra \{ display: none; \}/);
assert.match(mobile, /\.chart-page-params-below \{ display: block; \}/);
assert.match(cssDeclarations(".chart-page-tab", globalStyles), /min-height:\s*44px/);
assert.match(cssDeclarations(".chart-page-chip", globalStyles), /min-height:\s*44px/);
assert.match(cssDeclarations(".chart-page-back", globalStyles), /min-height:\s*44px/);
assert.match(globalStyles, /\.chart-page-vedic-stage \.personal-report-chart-svg text \{ font-size: 16px; \}/);
});
@@ -157,7 +157,12 @@ test("an incomplete profile page has no loading animation", () => {
test("the chart page shell is visible before the natal chart arrives", () => {
const markup = renderToStaticMarkup(React.createElement(ChartPageView, { view: null }));
assert.match(markup, />星盘</);
assert.match(markup, /返回对话/);
// 原值:断言页面上有「返回对话」链接
// 新值:断言常驻侧栏在场(「新建对话」是它的第一项)
// 原因:星盘页并入 app 外壳后侧栏常驻,回对话不再依赖一个一次性链接;
// 这比原断言更强——它要求的是整条导航都在,而不只是一个返回按钮。
assert.match(markup, /新建对话/);
assert.doesNotMatch(markup, /返回对话/);
assert.match(markup, /这一张盘还没拿到/);
assert.doesNotMatch(markup, /正在加载|骨架|spinner|打开即有/i);
assert.doesNotMatch(markup, /chart-page-eyebrow/);
+7 -2
View File
@@ -196,11 +196,16 @@ test("global report copy uses the canonical profile, allows an unrectified minut
});
test("report centre and ready reader scroll inside the chat shell lock", () => {
const centre = cssDeclarations(".report-center-shell", globalStyles);
// 原值:`.report-center-shell`(报告中心自己的全屏滚动容器)
// 新值:`.secondary-panel > :not(.chat-header)`——报告中心并入 app 外壳后,
// 滚动所有权归外壳的内容区,三个次级页共用一条规则。
// 原因:T4.2。「页面在外壳的高度锁内部滚动、不自己撑高」这条要求没变。
const centre = cssDeclarations(".secondary-panel > :not(.chat-header)", globalStyles);
const reader = cssDeclarations(".personal-report-reader", globalStyles);
assert.match(globalStyles, /html, body \{ width: 100%; height: 100%; overflow: hidden; \}/);
assert.match(centre, /height:\s*100%/);
assert.match(centre, /overflow-y:\s*auto/);
// 高度由 .chat-panel 的 grid 行给定,内容区自己不再声明 height。
assert.match(cssDeclarations(".chat-panel", globalStyles), /grid-template-rows:\s*46px minmax\(0, 1fr\) auto/);
assert.match(centre, /-webkit-overflow-scrolling:\s*touch/);
assert.doesNotMatch(centre, /min-height:\s*100dvh/);
assert.match(reader, /height:\s*100%/);
@@ -77,10 +77,14 @@ test("print pins the palette back to light so paper is always paper", () => {
// 原因:BUG-737——那个"衬线栈"里的 Tiempos Headline 从未加载,中文标题实际落到宋体。
// --font-display 现在是无衬线,400 在大字号上发虚,display 层级改由字重承担。
test("report titles use the display stack at the display weight", () => {
const centreTitle = cssDeclarations(".report-center-hero h1");
// 原值:三处标题——报告中心页标题 `.report-center-hero h1`、封面、章节
// 新值:两处——中心页标题已随 hero 移进 app 外壳的 46px 顶栏(T4.2),
// 它现在走 `.chat-header strong`,由 chat-header 的规则统一管。
// 原因:报告中心不再是有自己 hero 的独立页。封面与章节标题是报告纸面自己的
// 排版,不受影响,仍然逐条断。
const coverTitle = cssDeclarations(".personal-report-cover h1");
const sectionTitle = cssDeclarations(".personal-report-section-heading h2");
for (const [name, body] of [["centre", centreTitle], ["cover", coverTitle], ["section", sectionTitle]] as const) {
for (const [name, body] of [["cover", coverTitle], ["section", sectionTitle]] as const) {
assert.match(body, /font-family:\s*var\(--font-display\)/, `${name} title must use --font-display`);
assert.match(body, /font-weight:\s*500/, `${name} title must stay weight 500`);
}
@@ -89,5 +93,8 @@ test("report titles use the display stack at the display weight", () => {
test("report chrome and appendix disclosure keep 44px targets", () => {
assert.match(cssDeclarations(".personal-report-back"), /min-height:\s*44px/);
assert.match(cssDeclarations(".personal-report-disclosure"), /min-height:\s*44px/);
assert.match(cssDeclarations(".report-center-back"), /min-height:\s*44px/);
// 原值:报告中心「返回对话」按钮的 44px 触摸目标
// 新值:删除该断言
// 原因:报告中心并入 app 外壳,返回路径是常驻侧栏(T4.2),
// 它的触摸目标由 sidebar-contract 与 touch-target-contract 管。
});
@@ -30,7 +30,10 @@ test("tablet CSS cuts at 1023 so it matches sidebarViewportForWidth", () => {
});
test("report centre and reader share the global mobile cut", () => {
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.report-center-shell/);
// 原值 `.report-center-shell` / 新值 `.report-center-body`
// 原因:报告中心并入 app 外壳(T4.2),页面根不再是自己的全屏滚动容器;
// 「用全局 767 断点、不自立断点」这条要求没变,下面两条 doesNotMatch 仍在守。
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.report-center-body/);
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.personal-report-reader-body/);
assert.doesNotMatch(css, /@media \(max-width: 720px\)/);
assert.doesNotMatch(css, /@media \(max-width: 760px\)/);
@@ -42,6 +45,7 @@ test("report TOC keeps a content-width exception at 860px", () => {
});
test("ephemeris page stacks at the global mobile cut", () => {
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.ephemeris-shell/);
// 原值 `.ephemeris-shell` / 新值 `.ephemeris-body` / 原因同上。
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.ephemeris-body/);
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.ephemeris-panchanga \{[\s\S]*grid-template-columns:\s*1fr/);
});