三个页面此前各是脱离外壳的独立全屏路由,各写了一套一样的 *-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
113 lines
8.5 KiB
Markdown
113 lines
8.5 KiB
Markdown
# PROGRESS · 次级页外壳 R4
|
||
|
||
任务书:`docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.md` 的 R4
|
||
执行方式:**直接执行模式**
|
||
分支:`codex/cend-shell-20260916`
|
||
基线:`origin/staging` = `b3ea8c33`(第一份任务书 R1–R3 合入后)
|
||
BUG 段:不占用(产品改造)
|
||
|
||
R5(星盘+星历)与 R6(报告)依赖本轮。
|
||
|
||
---
|
||
|
||
## T4.1 · 只读导航外壳
|
||
|
||
新增三个文件:
|
||
|
||
| 文件 | 职责 |
|
||
|---|---|
|
||
| `src/hooks/use-nav-rail.ts` | 两个只读 GET(`/api/sessions?limit=40`、`/api/account`),`Promise.allSettled`,**不含任何写操作** |
|
||
| `src/components/app-nav-rail.tsx` | 品牌 + 新建对话 + 星盘/星历/我的报告 + 一条「最近」平铺列表 + 身份页脚 |
|
||
| `src/components/secondary-shell.tsx` | `SidebarProvider` → `AppNavRail` → `SidebarInset.chat-panel.secondary-panel` + 46px 顶栏 |
|
||
|
||
会话行走 `sessionHref("", id)` → `/?c=<uuid>`,跳转用 `persistLoginSessionReturn()` + `window.location.assign()`,**与现有侧栏 `leaveChat()` 完全一致的行为**,没有另造一套。
|
||
|
||
侧栏原语(`SidebarProvider` / `Sidebar` / `SidebarTrigger` / `SidebarRail`)是通用的,抽屉与折叠态直接复用,没有重写。
|
||
|
||
### 验收对照
|
||
|
||
| 任务书验收标准 | 结果 |
|
||
|---|---|
|
||
| `AppNavRail` 不 import page.tsx 的 hook,不持有会话写状态 | ✅ 依赖只有 ui/sidebar、`chat-session-url`、`session-groups`、自己的 hook |
|
||
| 四个路由都挂上外壳,桌面常驻、移动端抽屉 | ⚠️ **三个**:`/chart`、`/ephemeris`、`/reports`。`/reports/[reportId]`(阅读页)留给 R6,见下 |
|
||
| 点「最近」行跳 `/?c=<uuid>` 并选中 | ✅ 复用 `sessionHref`;`parseSessionUrlQuery` 的既有测试未动 |
|
||
| 列表未到达时静态空态,无骨架无 spinner | ✅ 三档文案:未登录 / 已 settled 无数据 / 读取中,全是静态文字 |
|
||
| 未登录时降级为品牌 + 登录入口,不报错不空白 | ✅ 401 → `signedOut`,列表位显示「登录后可以看到你的对话」,页脚变「去登录」 |
|
||
| 四个路由渲染标记逐个入记录 | ✅ 见下 |
|
||
|
||
**`/reports/[reportId]` 本轮没挂**:阅读页是纸面文档表面,外壳会改变它的滚动所有权与打印行为,而 R6 正要重做它(加目录)。同一轮里既挂外壳又改阅读结构,出问题难以定位。留给 R6,已在任务书依赖里写明。
|
||
|
||
### 两处我自己的健壮性问题,被测试抓到
|
||
|
||
1. **`usePathname()` 会返回 `null`。** 类型标注是 `string`,但在 app-router 上下文之外(包括直接挂载组件的单测里)返回 null,我的 `pathname.startsWith(...)` 直接抛 `Cannot read properties of null`,打红了 `chart-page-view` 与 `ephemeris-page` 两个渲染测试共 5 条。已加 `?? ""`。既有的 `app-sidebar.tsx` 有同样的写法但没被覆盖到,本轮不动它(红线 11)。
|
||
2. **`fetch` 可能不存在**(jsdom / 纯 node 环境)。原写法会在 `load()` 里同步抛出,变成未捕获的 promise 拒绝。已加 `typeof fetch !== "function"` 短路 + `.catch()` 兜底。
|
||
|
||
## T4.2 · 删掉三套重复骨架
|
||
|
||
| 页面 | 删除 |
|
||
|---|---|
|
||
| `chart-page-view.tsx` | `.chart-page-shell` / `.chart-page-topbar` / `.chart-page-back`(返回对话)/ `.chart-page-hero` + `<h1>星盘</h1>` |
|
||
| `ephemeris-page.tsx` | `.ephemeris-shell` / `.ephemeris-topbar` / `.ephemeris-back` / hero 的 `<h1>`;底部「去问」提到顶栏 |
|
||
| `personal-report-center.tsx` | `.report-center-shell` / `.report-center-topbar` / `.report-center-back` / hero 的 `<h1>`;「生成完整报告」提到顶栏 |
|
||
|
||
CSS 侧删掉 22 条规则、裁剪 6 条选择器列表。**这次用了 R2 教训后的切分规则**(只在注释之后找选择器),改完复核:注释配平 73/73、含花括号的注释 0 条。
|
||
|
||
### 一处内容差点被我弄丢
|
||
|
||
`CHART_VIEW_COPY.eyebrow`(「主盘直接算 · 分盘按需 · 不消耗点数」)原本在 hero 里,随 hero 一起被删。这是 **BUG-717 专门定的文案**——它是成本与速度承诺,而且会印在失败页上。测试打红才发现,已放回 tab 行下方作为一行静默说明。
|
||
|
||
### 错误态与空态
|
||
|
||
三个页面的降级分支(未登录、读取失败、无数据、`natalMissing`、`panchangaUnavailable` 等)全部保留,只是从 `*-shell *-message` 的根节点改成外壳内的一个 `section`,`h1` 降为 `h2`(页名已在顶栏,一页不该有两个 h1)。
|
||
|
||
### 「返回对话」残留说明
|
||
|
||
`git grep 返回对话 src/` 还有 4 处命中,都在**根边界页**(`error.tsx`、`not-found.tsx`、`global-error.tsx`、`reports/[reportId]/not-found.tsx`)。按 `DESIGN.md`,这些页在根 layout 段里、**不得 import `globals.css`**,因此不能挂外壳,那个链接是它们唯一的回家路径。本轮不动。
|
||
|
||
---
|
||
|
||
## 收尾验收
|
||
|
||
| 红线 | 结果 |
|
||
|---|---|
|
||
| 1 · `tsc` 0 错 / `lint` 0 error | ✅ 118 warning,与基线同 |
|
||
| 2 · 测试总数 | ✅ tests 3350,pass 3303,fail 31,与基线 `diff` **逐条一致** |
|
||
| 3 · **四个路由渲染标记不变** | ✅ `/` `○` · `/chart` `○` · `/ephemeris` `○` · `/reports` `ƒ`,改前改后完全相同 |
|
||
| 4 · 次级页首屏体积 | ⚠️ **口径缺口**:CSS 侧 39,825 → **39,727 字节(−0.25%)**,三套骨架并成一套是净减。但当前 Turbopack 的路由表**不输出 per-route JS 体积**,导航栏与 sidebar 原语给三个次级页增加的 JS 我量不到,不能给数字。任务书预期的「次级页体积上升」无法证实也无法证伪 |
|
||
| 6 · 一律 `ChatComposer` / 一套滚动跟随 | ✅ 本轮未涉及 |
|
||
| 7 · 无 spinner / 骨架 | ✅ 列表在途是静态文案 |
|
||
| 8 · 不回退 BUG-698 的 `@supports (height: 1dvh)` | ✅ 未触碰 |
|
||
| 9 · 不回退 BUG-616/617 的报告盘面 grid | ✅ 未触碰 `report-chart-grid-rehype.ts` |
|
||
| 10 · 同提交更新 `DESIGN.md` | ✅ 新增「Secondary page shell」一节;「Personal report centre」的 Structure 与 Width 两条重写 |
|
||
| 11 · 不升依赖、不留新 warning | ✅ |
|
||
| 12 · 只读侧栏不发起写操作 | ✅ `use-nav-rail.ts` 只有两个 GET |
|
||
|
||
Python 快速门(BUG-736):本轮**未改** `page.tsx`;新增的 `use-nav-rail.ts` 不是 Home 级 hook(`Home()` 不引用它),按规则不需要登记进 `OPTIONAL_HOME_HOOKS`。仍跑了 `pytest tests/test_supabase_user_data_contract.py`,8 条全绿。
|
||
|
||
### 按红线 3 更新的既有断言(原值 / 新值 / 原因)
|
||
|
||
| 文件 | 原值 | 新值 | 原因 |
|
||
|---|---|---|---|
|
||
| `chart-page-view` · 移动端 | 三条以 `@media (max-width: 767px) { .chart-page-hero h1` 起锚;另断 `.chart-page-back` 44px | 直接断该断点块里的三个开关;删 `.chart-page-back` 一条 | hero 与返回按钮已删;**被断言的三个开关一条没动** |
|
||
| `chart-page-view` · 空态 | `assert.match(markup, /返回对话/)` | `assert.match(markup, /新建对话/)` + `doesNotMatch(/返回对话/)` | 返回路径从一次性链接变成常驻侧栏。这条**更强**:要求整条导航在场 |
|
||
| `personal-report-entry` | `.report-center-shell` 的 height/overflow | `.secondary-panel > :not(.chat-header)` 的 overflow + `.chat-panel` 的 grid 行 | 滚动所有权归外壳;「在高度锁内部滚动、不自己撑高」没变 |
|
||
| `personal-report-theme-contract` ×2 | `.report-center-back` 44px;标题三处含 `.report-center-hero h1` | 前者删除;后者收为封面 + 章节两处 | 返回按钮与页标题都移进了外壳顶栏,由 chat-header 规则统管 |
|
||
| `viewport-breakpoint-contract` ×2 | `.report-center-shell` / `.ephemeris-shell` 在 767 块内 | `.report-center-body` / `.ephemeris-body` | 页面根换了;「用全局 767 断点、不自立断点」由同测试的两条 `doesNotMatch` 继续守 |
|
||
|
||
一条实际收紧(空态从「有返回链接」变成「整条导航在场 + 不得再有返回链接」),其余是形态同步。
|
||
|
||
---
|
||
|
||
## 环境缺口(不得记为通过)
|
||
|
||
1. **次级页侧栏在移动端抽屉的手势与焦点行为**(本轮新挂,优先级最高)。
|
||
2. **未登录时三个次级页的降级表现**——无登录态,只能证明代码分支存在。
|
||
3. **从次级页点「最近」行回到对话并正确选中**——需要真实会话数据。
|
||
4. **per-route JS 体积**:构建不输出,需要另外的测量手段(见红线 4)。
|
||
|
||
---
|
||
|
||
## 下一轮
|
||
|
||
R5(星盘 + 星历)与 R6(报告中心 + 报告阅读,含把 `/reports/[reportId]` 挂进外壳)基线取本轮合入后的 `origin/staging`。R7、R8 与本轮文件不重叠,可并行。
|