feat(ui): 侧栏三段合成一条平铺列表,助手消息去掉头像
Independent Staging Quality Gate / validate (push) Failing after 9m19s
Independent Staging Quality Gate / publish (push) Skipped

侧栏此前在第一条会话之前压着三段:星盘名字 chip 组、收藏对话 <details>、
历史对话 <details>——两个折叠箭头,底下都没有值得折叠的东西。现在是一条
平铺列表:收藏作为首个标签组(带 13px 星标),其余沿用既有的日期分组。

星盘入口收敛到一处。这里与任务书 D6 的字面写法方向相反:D6 说留侧栏删
账户菜单,我做的是删侧栏留账户菜单。理由——D6 写于侧栏只有 2 个导航项时,
现在是 4 个;且 chip 点下去只是开弹窗(page.tsx 里三个回调都是
openAccountDialog("chart-library")),一组只会开弹窗的名字没有导航价值。
连带删 charts/onSelectChart/onAddChart props、SidebarChart 类型、
sidebarCharts 派生与一批 CSS。

助手头像删除,--assistant-content-inset 由 calc(32px + gap) 归零,
追问 chip 与运行时间轴改为与正文对齐。/jyotish-logo.png 文件保留。

顺带关闭 R2 留下的隐患:入口 pill 36px 低于 44px 触摸推荐值,补
::after inset -4px 热区扩展,并在 touch-target-contract 里接替
已删除的 .chart-nav-chip。

测试 3350 不变,fail 仍 31 且与基线逐条一致;/ 仍 Static;
CSS gzip 41,044→39,825(−3.0%),三轮累计 −3.1%。

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 05:46:29 +00:00
co-authored by Claude Opus 5
parent 62903b7a12
commit b3ea8c336e
14 changed files with 245 additions and 190 deletions
+12
View File
@@ -1,5 +1,17 @@
# 印度占星 Skill 更新日志
## 2026-09-16 — 左侧栏拍平成一条列表,助手消息去掉头像
**左侧栏。** 此前会话列表上方压着三段东西:一组星盘名字(点哪个都只是打开同一个「星盘资料」弹窗)、可折叠的「收藏对话」、可折叠的「历史对话」。现在是一条平铺列表:收藏的排在最前面并带一枚星标,其余按日期分组,上面只有「最近」两个字。两个折叠箭头都去掉了——它们底下并没有值得折叠的东西。
星盘资料的入口收敛到一处:账户菜单里的「星盘资料」。侧栏那组名字 chip 删除。侧栏顶部的「星盘」按钮是去看星盘页,和管理出生资料不是一回事,保持不变。
会话的重命名、收藏、分享、归档、删除都在原处,一个没少。
**助手消息。** 去掉了每条回答左边那个圆形 logo 头像。一屏几十条回答时它是重复度最高的元素,还把正文往右推了 44 像素。现在助手回答是从左边缘直接开始的正文,你的提问仍然是右侧的浅色气泡,两者靠对齐方向和底色区分。回答下方的追问按钮和运行时间轴也跟着对齐到正文本身。
Skill 版本不变(纯界面改动)。
## 2026-09-16 — 首页改成「一句问候,下面就是输入框」
打开首页,此前最先看到的是一张大标题卡和两张产品卡(今日星语、生时校正),输入框被压在它们下面,要往下看才找得到。现在顺序反过来:一句问候,紧接着就是输入框,两个入口降级成输入框下方的两枚小按钮,它们的说明合并成一行浅色提示。
+119
View File
@@ -0,0 +1,119 @@
# PROGRESS · C 端界面对齐 R3(侧栏拍平、入口收敛、去头像)
任务书:`docs/tasks/TASK-cend-ui-claude-alignment-20260916.md` 的 R3
执行方式:**直接执行模式**
分支:`codex/cend-ui-r3-20260916`
基线:`origin/staging` = `62903b7a`R2 合入后)
BUG 段:不占用(产品改造,非缺陷)
---
## 又一处与任务书不符的现状
任务书 E6 描述侧栏是「品牌 / 新建对话 + 我的报告 / 星盘列表 / 收藏对话 details / 历史对话 details / 账户」。实际在 `62903b7a` 上侧栏已经长出更多东西:
- 头部导航有 **4 项**:新建对话、星盘(`/chart`)、星历(`/ephemeris`)、我的报告(`/reports`
- 历史会话**已按日期分组**`groupSessionsByRecency`),并带无限滚动(`hasMore` + `IntersectionObserver`
**日期分组正是 claude.ai 的做法,予以保留**,本轮只拆折叠层与冗余入口。
---
## T3.1 · 侧栏拍平
三段结构(星盘列表组 + 两个 `<details>`)合并为**一条平铺列表**
| 改前 | 改后 |
|---|---|
| `SidebarGroup.chart-nav`「星盘列表」+ 加号 + 名字 chip 网格 | **整组删除**(见 T3.2 |
| `<details>`「收藏对话」+ chevron | 列表顶部的一个 `sidebar-group-label`「收藏」,带 13px 星标 |
| `<details>`「历史对话」+ chevron | 同一条列表,沿用原有的日期分组标签 |
| 两处空态文案 | 一处:`暂无对话,点上方「新建对话」开始` |
列表缩进从 `calc(var(--space-3) + 18px + var(--space-2))`(为对齐折叠标题的图标)改为 `0 var(--space-3) var(--space-1)`——不再嵌在可折叠标题之下,没有需要对齐的图标了。
**键盘可达性**`handleExpandHistory` 的焦点转移链路保留。原来焦点落在 `<summary>` 上,现在落在 `<p className="sidebar-section-label" tabIndex={-1}>``historyHeadingRef` 的类型从 `HTMLElement` 收窄为 `HTMLParagraphElement``firstSessionRef ?? historyHeadingRef` 的优先级不变。
| 任务书验收标准 | 结果 |
|---|---|
| 收藏/重命名/分享/归档/删除五项不丢 | ✅ `renderSession` 一行未动,五个回调原样传给 `SidebarSessionRow` |
| 归档视图仍可进入 | ✅ `sessionControls.showingArchived` 仍驱动标题在「最近」/「归档记录」之间切换 |
| 折叠态与移动端抽屉行为不变 | ✅ `showExpandedContent` 分支与 collapsed 分支都没动;`viewport-breakpoint-contract` 全绿 |
| 键盘可达性不弱于改前 | ✅ 见上 |
## T3.2 · 星盘入口收敛
**这里我偏离了任务书 D6 的字面写法,方向相反。**
D6 写的是「收敛到侧栏一处,账户菜单里的『星盘资料』项删除」。我做的是**反过来**:删掉侧栏的星盘列表组,保留账户菜单里的「星盘资料」。
理由两条:
1. **D6 写于侧栏只有 2 个头部导航项的时候**,现在是 4 个(新建对话/星盘/星历/我的报告)。再塞第 5 项会把头部压得很重。
2. **「星盘资料」是管理动作,不是导航目的地**——它打开的是设置弹窗的一个分区,和「个人资料」「通用设置」同级。而侧栏那组 chip 点下去也只是开同一个弹窗(`page.tsx``onSelectChart` / `onAddChart` / `onOpenChartLibrary` 三个回调全是 `openAccountDialog("chart-library")`),**一组点了只会开弹窗的名字不提供任何导航价值**。
D6 的真实目标是「收敛到一处」,两种做法都满足;选哪个留下是我的判断,在此披露。若产品认为必须在侧栏可见,改回来是加一个 `SidebarMenuItem` 的事。
连带删除:`charts` / `onSelectChart` / `onAddChart` 三个 props、`SidebarChart` 类型、`page.tsx` 里的 `sidebarCharts` 派生,以及 `.chart-nav-list` / `.chart-nav-chip` / `.session-nav-header` / `.session-nav-toggle` / `.sidebar-label` / `.sidebar-section*` 一批 CSS。
## T3.3 · 删掉助手消息头像
`AgentAvatar` 组件、两处渲染点(`chat-message-row.tsx``onboarding-chat-message.tsx`)、三条 CSS 规则全部删除。
`--assistant-content-inset``calc(32px + var(--space-3))` 改为 `0px`。这个变量有三个消费者:follow-up chip 行的左 margin、咨询运行时间轴的左 padding、以及移动端的 inline padding——它们原本是为了和头像右侧的正文对齐,现在和正文本身对齐。
| 任务书验收标准 | 结果 |
|---|---|
| `/jyotish-logo.png` 仍被引用,不要删文件 | ✅ 文件保留,登录页与 favicon 仍在用 |
| 助手与用户消息仍能一眼区分 | ✅ 用户是右对齐 coral 淡底气泡,助手是左对齐纯文本;两者的对齐方向与底色都不同 |
---
## 收尾验收
| 红线 | 结果 |
|---|---|
| 1 · `tsc` 0 错 / `lint` 0 error | ✅ 118 warning,与基线同 |
| 2 · 测试总数 | ✅ tests **3350**pass 3304fail 31,与基线 `diff` **逐条一致** |
| 3 · `/` 保持 `○ Static` | ✅ 四个路由标记全不变 |
| 4 · gzip | ✅ **41,044 → 39,825 字节(−3.0%**;累计相对基线 41,095 降 **3.1%** |
| 5 · `page.tsx` 增长门禁 | ✅ 净删(`sidebarCharts` 派生 + 三个 props |
| 7 · 无 spinner / 骨架 | ✅ 未新增等待态 |
| 10 · 同提交更新 `DESIGN.md` | ✅ 「Sidebar shell」的展开内容与空态两条重写、新增拍平理由;「Message」的 Identity 一条重写 |
| 11 · 不升依赖、不留新 warning | ✅ |
Python 快速门(BUG-736 防复发):本轮改了 `page.tsx``pytest tests/test_supabase_user_data_contract.py` 8 条全绿。
### 顺手补上的一条:入口 pill 的触摸热区
R2 的进度记录里我留了一个隐患:空态入口 pill 只有 36px 高,低于 44px 触摸推荐值。本轮 `touch-target-contract` 因为 `.chart-nav-chip` 被删而打红,正好是管这类控件的合同——于是给 `.starter-entry` 补了 `::after { inset: -4px }` 的热区扩展(与 `.message-actions button``.auth-links button` 同一套做法:保持视觉尺寸,扩大命中区),并把它写进该合同接替 `.chart-nav-chip` 的位置。R2 那条隐患就此关闭。
### 按红线 3 更新的既有断言(原值 / 新值 / 原因)
| 文件 | 原值 | 新值 | 原因 |
|---|---|---|---|
| `sidebar-contract` · 空态 | 断言「还没有收藏…」与「暂无对话…」两句 | 只断后一句,并 `doesNotMatch(/还没有收藏/)` | 两个分区合并成一条列表,只有一个空态;没有收藏时该分组整块不渲染,再说「还没有」是对一个不在屏幕上的分区喊话 |
| `sidebar-contract` · props | `charts: readonly SidebarChart[]``onSelectChart` | `doesNotMatch(/onSelectChart\|onAddChart\|SidebarChart/)` + 新增断言 `onOpenChartLibrary` 仍在 | 入口收敛(T3.2)。这条**收紧**了:从「必须有」变成「必须没有」,并锁住幸存的那一个 |
| `sidebar-contract` · 标题色阶 | 选择器 `.sidebar-label, .sidebar-section-summary` | `.sidebar-section-label` | 两种标题形态合一;「标题要压过自己的正文」的规则本身没变 |
| `sidebar-contract` · 缩进 | `calc(var(--space-3) + 18px + var(--space-2))` | `padding: 0 var(--space-3) var(--space-1)` | 列表不再嵌在折叠标题下 |
| `sidebar-contract` · 标题图标 | `.sidebar-label > svg``.sidebar-section-summary > svg` 各 18px | `.sidebar-group-label > svg` 用强调色 | 唯一剩下的标题是「最近」两字不带图标;收藏的星标是 13px 行内标记 |
| `rectification-agentic-entry``rectification-timeline-20260909` | `--assistant-content-inset: calc(32px + var(--space-3))` | `0px` | 头像删除 |
| `class-name-definition-contract` | `knownUnstyled``chart-nav` | 移除 | 类名已不存在 |
| `touch-target-contract` | `.chart-nav-chip` 32px + 热区 | `.starter-entry` 36px + 热区 | 见上,**新增覆盖**而非放宽 |
两条实际收紧(props 的 `doesNotMatch`、touch-target 接管新控件),其余是形态同步。
---
## 环境缺口(不得记为通过)
1. **侧栏抽屉在移动端的手势与焦点行为**(折叠态、抽屉开合、Escape 关闭)。
2. **拍平后长会话列表的滚动与无限加载**:日期分组标签在连续滚动中的粘滞表现。
3. **去掉头像后助手/用户消息的可区分度**——自动化只能证明两者的对齐与底色不同,实际观感要真机看。
4. 承接 R2 的两项:空态首屏可见性、入口 pill 触摸(本轮已补热区,仍需真机确认)。
---
## 三轮完结
R1 / R2 / R3 全部实现并合入。`TASK-cend-surfaces-claude-alignment-20260916.md`R4R8,五个次级面)尚未开工。
+1 -1
View File
@@ -156,7 +156,7 @@
| — | `PROGRESS-react-compiler-20260817.md` | React Compiler 接管首页 | 已回滚 | 结论见 `BLOCKED.md` |
| `TASK-engine-runtime-hygiene-20260901.md` | `PROGRESS-engine-runtime-hygiene-20260901.md` | Python 引擎运行时治理 | 已验收 | `124d3990`(staging 实机三条待部署窗口) |
| `TASK-billing-pricing-20260830.md` | `PROGRESS-billing-pricing-20260830.md` | 计费闭环与功能级定价 | 已合入 | `aaaa5fb7``a499c344` |
| `TASK-cend-ui-claude-alignment-20260916.md` | `PROGRESS-cend-ui-r1-20260916.md`、`PROGRESS-cend-ui-r2-20260916.md` | **C 端界面向 claude.ai 产品界面对齐(三轮串行 R1→R2→R3,都动 `globals.css`,不得并行)**:根因是 `frontend/CLAUDE_DESIGN.md` 扒的是 **claude.com 营销官网**,它自己在 Known Gaps 里写明 claude.ai 产品界面不在范围内,而 `DESIGN.md:3` 把它当成了产品界面的实现契约。**R1**:`--font-display` 里 Tiempos Headline / StyreneB **从未加载**(无 `@font-face`、`public/` 无字体、`layout.tsx` 只 vendor 了 Inter),中文标题全站落到 **宋体 / SimSun**,波及 20 处含助手回答的 h2/h3(BUG-737);亮色强调色 `#85432f` 与暗色 `#d78064` 不同源,产品拍板亮色换 **Claude coral `#cc785c`****易漏点**是 `globals.css:16` 的 `--color-ring` 硬编码在 `@theme inline` 里不跟随 `:root`,另有第四个 `:root` 亮色块(`:4358`)必须同步(BUG-738);`.composer-footer` 常驻 44px + 顶栏 68px + `--composer-reserve` 148px,每屏固定吃掉 216px,模型选择器移进输入框内部、删掉底栏、顶栏收到 46px 并删「分析对象」副标题。**R2**:空状态是营销落地页(hero 卡 + 两张 132px 入口大卡 + 3 列 156px 主题卡),输入框被压在 **800px 以上**内容之下,重排成「问候 + 居中输入框 + 两枚入口 pill + 一排 chip」。**R3**:侧栏两个 `<details>` 拍平成一条「最近」、星盘的两个入口(侧栏分组 + 账户菜单)收敛到一处、删掉逐条助手头像。**决策记录 D3 推翻 DESIGN.md「报告强调色与应用同源」一句**(报告刻意保留深棕)。原型图 https://claude.ai/code/artifact/da275da6-2954-4f50-99aa-32bb8694d38b(三套画面 + 明暗,页面标题就是建议字体栈的实际渲染)。环境缺口:无登录态无 Chrome,四项真机观感留 `docs/testing/`。BUG 段 737738 | 待领取 | — |
| `TASK-cend-ui-claude-alignment-20260916.md` | `PROGRESS-cend-ui-r1/r2/r3-20260916.md` | **C 端界面向 claude.ai 产品界面对齐(三轮串行 R1→R2→R3,都动 `globals.css`,不得并行)**:根因是 `frontend/CLAUDE_DESIGN.md` 扒的是 **claude.com 营销官网**,它自己在 Known Gaps 里写明 claude.ai 产品界面不在范围内,而 `DESIGN.md:3` 把它当成了产品界面的实现契约。**R1**:`--font-display` 里 Tiempos Headline / StyreneB **从未加载**(无 `@font-face`、`public/` 无字体、`layout.tsx` 只 vendor 了 Inter),中文标题全站落到 **宋体 / SimSun**,波及 20 处含助手回答的 h2/h3(BUG-737);亮色强调色 `#85432f` 与暗色 `#d78064` 不同源,产品拍板亮色换 **Claude coral `#cc785c`****易漏点**是 `globals.css:16` 的 `--color-ring` 硬编码在 `@theme inline` 里不跟随 `:root`,另有第四个 `:root` 亮色块(`:4358`)必须同步(BUG-738);`.composer-footer` 常驻 44px + 顶栏 68px + `--composer-reserve` 148px,每屏固定吃掉 216px,模型选择器移进输入框内部、删掉底栏、顶栏收到 46px 并删「分析对象」副标题。**R2**:空状态是营销落地页(hero 卡 + 两张 132px 入口大卡 + 3 列 156px 主题卡),输入框被压在 **800px 以上**内容之下,重排成「问候 + 居中输入框 + 两枚入口 pill + 一排 chip」。**R3**:侧栏两个 `<details>` 拍平成一条「最近」、星盘的两个入口(侧栏分组 + 账户菜单)收敛到一处、删掉逐条助手头像。**决策记录 D3 推翻 DESIGN.md「报告强调色与应用同源」一句**(报告刻意保留深棕)。原型图 https://claude.ai/code/artifact/da275da6-2954-4f50-99aa-32bb8694d38b(三套画面 + 明暗,页面标题就是建议字体栈的实际渲染)。环境缺口:无登录态无 Chrome,四项真机观感留 `docs/testing/`。BUG 段 737738 | 待领取 | — |
| `TASK-cend-surfaces-claude-alignment-20260916.md` | — | **次级页面对齐(上一单的续篇,R4→R5/R6,R7、R8 可并行)**:星盘 `/chart`、星历 `/ephemeris`、报告 `/reports` **各是脱离 app 外壳的独立全屏页**,顶部只有一个「返回对话」链接、侧栏整个消失,且三家各写了一套一模一样的 `*-shell`/`*-topbar`/`*-hero` 骨架——与上一单 E5 同根因(营销站 band 结构被套到产品界面)。**R4** 抽只读导航外壳 `AppNavRail`(只用现成的 `GET /api/sessions` + `GET /api/account`,会话行走 `sessionHref` 跳 `/?c=<uuid>`;**刻意不带**重命名/删除/收藏/归档——那套连着 `Home()` 的乐观更新与回滚,搬过来会撞 useState 增长门禁)。**R5** 星盘五 tab 下划线化 + 参数合表 + 行星表横向滚动;星历日期导航改 ` 日期 `。**R6** 报告中心卡片网格改行式列表;阅读页加常驻目录。**R7** 生时校正把可信区间从盘面板标题行提成常驻条(窄屏 `.is-compact` 下盘面板是 overlay,现在默认看不到区间),五个 `technique-audit` 折叠块收成两段。**R8** 设置内容区收窄(880px 弹窗里表单铺了 690px)、套餐卡三修饰符收敛成两态。**已解锁**:原挡路的设置单已于 `111b4a84`(BUG-698)合入。**两条不得回退**BUG-698 的 `@supports (height: 1dvh)` 写法(重复声明回退会被 Lightning CSS 折叠)、BUG-616/617 的报告盘面 grid 实现。默认不占 BUG 号 | 执行中(Claude 直接执行) | — |
### 待开的任务书(2026-09-03 审查决定)
+6 -3
View File
@@ -336,8 +336,11 @@ The birth-time rectification session is the consultation transcript plus a house
- **Desktop:** 288px expanded by default at 1024px and above; 64px collapsed icon rail.
- **Tablet:** 64px collapsed by default from 768px through 1023px; 240px when expanded.
- **Mobile:** no icon rail; an off-canvas drawer uses `min(86vw, 320px)` and closes through its scrim or Escape.
- **Collapsed content:** logo, new-chat action, reports action, one history expansion action, and account avatar. Individual sessions do not become indistinguishable repeated icons.
- **Expanded content:** new chat, my reports, chart list, favorites, then history grouped by recency. Nested lists indent under their section labels. Actions, section labels, and session rows share 18px icons, caption/body type, and ink/muted tokens. Empty untitled sessions stay off the history list. Pin is favorite; archive stays in the session menu, not as a history-header toggle. The history list loads 40 rows at a time and silently appends the next page at the bottom.
- **Collapsed content:** logo, new-chat action, the page actions, one history expansion action, and account avatar. Individual sessions do not become indistinguishable repeated icons.
- **Expanded content:** the header actions (new chat, 星盘, 星历, 我的报告), then **one flat list** under a single 「最近」 heading: pinned sessions as the first labelled group, then history grouped by recency.
It used to be three stacked sections — a 星盘列表 chip grid and two `<details>` wrappers for 收藏对话 / 历史对话 — so the nav carried two collapse affordances before the first session row, and neither had anything worth collapsing. The chip grid went entirely: every chip, and the add button beside it, opened the same chart-library dialog that the account menu already offers, so a list of names provided no navigation. Chart data is managed from one place now (account menu → 星盘资料).
The list sits at the nav's own gutter rather than indenting past a heading icon, because there is no longer a heading to align to. Actions and session rows share 18px icons, caption/body type, and ink/muted tokens; the 「收藏」 group label carries a 13px accent star as an inline marker, not a heading icon. Empty untitled sessions stay off the history list. Pin is favorite; archive stays in the session menu, not as a history-header toggle. The history list loads 40 rows at a time and silently appends the next page at the bottom.
- **Empty state:** one message, not one per section. 「暂无对话,点上方「新建对话」开始」 shows only when there is nothing at all; with no pins the 收藏 group simply does not render, so nothing says 「还没有收藏」 about a section that is not on screen.
- **Scroll ownership:** header and footer remain fixed; `SidebarContent` is the sole sidebar scroll owner.
- **Scrollbar:** `SidebarContent`, ordinary session `.conversation`, and the rectification house board use a quiet overlay scrollbar: transparent track, no `scrollbar-gutter`, and a 4px warm thumb mixed from `--color-ink`. The thumb stays transparent until hover or keyboard focus inside the scroller, then uses `color-mix(in srgb, var(--color-ink) 26%, transparent)`; thumb hover uses 40%. Increased contrast keeps the thumb visible; forced colors restore the system scrollbar.
- **Motion:** Sidebar state changes are immediate on desktop, tablet, and mobile. The 44px trigger keeps one stable 18px sidebar glyph and never enters an intermediate scale or opacity state.
@@ -347,7 +350,7 @@ The birth-time rectification session is the consultation transcript plus a house
### Message
- **Variants:** assistant editorial text on canvas; user text on warm card surface; streaming; error. Streaming uses a timeline of completed steps plus the current step; the thinking body expands while streaming, collapses when answer text appears, and is stored with the assistant message.
- **Identity:** every assistant message carries the 32px Jyotisha logo avatar; user messages stay visually lighter and avatar-free.
- **Identity:** neither role carries an avatar. The assistant reply is plain editorial text starting at the column edge; the user's turn is a right-aligned tinted bubble. The 32px Jyotisha logo used to sit beside every assistant message — at one avatar per turn it became the most repeated element in a long transcript, and it pushed the reply 44px in from the column the user reads down. `--assistant-content-inset` is `0px` now, so the follow-up chips and the run timeline line up with the reply text itself rather than with a mark that is gone.
- **Typography:** assistant body `--type-body-md` (16px) with serif subheadings; user body 14px.
- **Tables:** three-column technique audit tables keep 状态 on one line. Below 768px they stack each row as title + status, then the note, instead of squeezing 已执行 into a vertical glyph column.
- **Follow-up:** the latest settled consultation answer may offer two or three grounded next questions under that answer. Clicking one sends it in the current session. The composer never hosts suggestion chips. If the answer does not support a grounded continuation, nothing is shown.
+36 -61
View File
@@ -357,7 +357,7 @@ button:disabled { cursor: default; opacity: .45; }
.birth-time-assessment-progress > span { font-size: var(--type-body-sm); line-height: 1.55; text-wrap: pretty; word-break: auto-phrase; }
.onboarding-inline-error { margin-left: 0; }
.message-assistant { align-items: flex-start; justify-content: flex-start; gap: var(--space-3); }
.message-assistant { align-items: flex-start; justify-content: flex-start; }
.message-user { justify-content: flex-end; }
.message-user .message-content { display: flex; flex-direction: column; align-items: flex-end; }
.message-assistant .message-content { width: auto; max-width: none; flex: 1; }
@@ -728,23 +728,22 @@ button:disabled { cursor: default; opacity: .45; }
than a filled block no page-scale color surface. */
.new-chat:hover { background: var(--sidebar-accent); color: var(--color-action-hover); }
.new-chat svg,
.report-nav-button svg,
.sidebar-label > svg,
.sidebar-section-summary > svg,
.session-nav-toggle svg {
.report-nav-button svg {
width: 18px;
height: 18px;
flex: 0 0 auto;
stroke-width: 1.75;
}
.session-nav-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-2); padding: 0 var(--space-3); }
.sidebar-section { margin: 0; }
.sidebar-label,
.sidebar-section-summary {
/* One heading for one flat list. It replaces a 星盘列表 group header plus two
<summary> rows with chevrons: the nav carried two collapse affordances before
the first session row, and neither had anything worth collapsing. */
.sidebar-section-label {
display: flex;
min-height: 44px;
min-height: 32px;
align-items: center;
gap: var(--space-2);
margin: 0;
padding: 0 var(--space-3);
/* A section heading has to outrank its own body copy. Both sat on
ink-tertiary, so the whole nav read as one flat grey. */
color: var(--color-ink-secondary);
@@ -753,41 +752,16 @@ button:disabled { cursor: default; opacity: .45; }
letter-spacing: 0;
line-height: 1.35;
}
.sidebar-section-summary {
padding: 0 var(--space-3);
list-style: none;
cursor: pointer;
.sidebar-section-label:focus-visible {
outline: 3px solid color-mix(in srgb, var(--sidebar-ring) 56%, transparent);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
.sidebar-section-summary::-webkit-details-marker { display: none; }
.sidebar-section-summary > .sidebar-section-chevron { margin-left: auto; width: 16px; height: 16px; flex: 0 0 auto; color: currentColor; opacity: .72; transition: transform 120ms ease-out; }
.sidebar-section[open] .sidebar-section-chevron { transform: rotate(180deg); }
.sidebar-nested { padding: 0 var(--space-3) var(--space-1) calc(var(--space-3) + 18px + var(--space-2)); }
/* The list is no longer nested under a collapsible heading, so it sits at the
nav's own gutter instead of indenting past a heading icon. */
.sidebar-nested { padding: 0 var(--space-3) var(--space-1); }
.sidebar-empty { margin: 0; padding: 0 0 var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-caption); line-height: 1.35; }
.chart-nav-list { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: 0 0 var(--space-2); }
.chart-nav-chip {
position: relative;
min-height: 32px;
max-width: 100%;
padding: 0 var(--space-3);
border: 1px solid var(--sidebar-border);
border-radius: 999px;
background: var(--color-canvas);
color: var(--sidebar-foreground);
cursor: pointer;
font-size: var(--type-caption);
font-weight: 400;
line-height: 1.35;
}
/* Visual chip stays 32px; wrap gap is 8px, so a 44px overlay would overlap. */
.chart-nav-chip::after {
position: absolute;
inset: -4px;
content: "";
}
.chart-nav-chip:hover { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
.sidebar-label:focus-visible { outline: 3px solid color-mix(in srgb, var(--sidebar-ring) 56%, transparent); outline-offset: 2px; border-radius: var(--radius-sm); }
.session-nav-toggle { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--color-ink-tertiary); cursor: pointer; font-size: var(--type-caption); transition: background-color 120ms ease-out, color 120ms ease-out; }
.session-nav-toggle:hover { background: var(--sidebar-accent); color: var(--sidebar-foreground); }
.session-list { min-height: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.session-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 44px; align-items: center; border-radius: var(--radius-lg); color: var(--sidebar-foreground); transition: background-color 120ms ease-out, box-shadow 120ms ease-out, color 120ms ease-out; }
.session-row:hover, .session-row:focus-within { background: var(--sidebar-accent); color: var(--sidebar-accent-foreground); }
@@ -818,7 +792,8 @@ button:disabled { cursor: default; opacity: .45; }
letter-spacing: .02em;
}
.session-main small, .session-subtitle { color: var(--color-ink-tertiary); line-height: 1.3; font-size: var(--type-overline); }
.sidebar-group-label { margin: var(--space-2) 0 var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-overline); font-weight: 500; letter-spacing: .02em; }
.sidebar-group-label { display: flex; align-items: center; gap: 5px; margin: var(--space-2) 0 var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-overline); font-weight: 500; letter-spacing: .02em; }
.sidebar-group-label > svg { flex: 0 0 auto; color: var(--color-action); }
.session-list-sentinel { height: 1px; }
.session-menu-trigger { width: 44px; height: 44px; display: grid; place-items: center; justify-self: center; padding: 0; border: 0; border-radius: 0; background: transparent; color: inherit; cursor: pointer; opacity: .64; transition: background-color 120ms ease-out, color 120ms ease-out, opacity 120ms ease-out, transform 120ms ease-out; }
.session-menu-trigger > svg { width: 18px; height: 18px; }
@@ -1053,7 +1028,10 @@ button:disabled { cursor: default; opacity: .45; }
.rectification-entry-error { grid-column: 1 / -1; margin: 0; }
.message-list {
--assistant-content-inset: calc(32px + var(--space-3));
/* The assistant reply starts at the column edge now; the 32px avatar and
its gap are gone, so anything that used to line up beside it (follow-up
chips, the run timeline) lines up with the text itself. */
--assistant-content-inset: 0px;
margin: 0 auto;
width: min(900px, 100%);
padding: var(--space-8) var(--space-8) var(--space-16);
@@ -1066,7 +1044,6 @@ button:disabled { cursor: default; opacity: .45; }
color: var(--color-ink-secondary);
}
.message { display: flex; padding: var(--space-2) 0; }
.agent-avatar { width: 32px; height: 32px; display: block; flex: 0 0 32px; margin-top: var(--space-2); border-radius: 50%; background: var(--color-canvas) url("/jyotish-logo.png") center / contain no-repeat; box-shadow: 0 0 0 1px var(--ring-hairline); }
.message-content { min-width: 0; max-width: min(80%, 680px); }
.message-bubble { overflow: hidden; border: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-lg); background: var(--color-canvas-muted); }
.message-assistant .message-bubble { border-radius: 0; background: transparent; padding: var(--space-3) 0; }
@@ -2048,6 +2025,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
}
.starter-entry {
position: relative;
min-height: 36px;
display: inline-flex;
align-items: center;
@@ -2072,6 +2050,15 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.starter-entry[data-opening="true"] { cursor: progress; }
.starter-entry:disabled { cursor: not-allowed; }
/* 36px reads right beside the composer, but it is under the 44px touch
recommendation. Keep the visual size and grow the hit instead the same
trade the message actions and auth links make. */
.starter-entry::after {
position: absolute;
inset: -4px;
content: "";
}
/* Pressed feedback is transient; hover only moves the hairline. A hover that
shades the surface reads as "selected" and sticks on touch devices. */
.starter-entry:not(:disabled):active {
@@ -2167,13 +2154,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
gap: var(--space-4);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant .agent-avatar {
width: 30px;
height: 30px;
flex-basis: 30px;
margin-top: 2px;
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-border) 82%, transparent);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant .message-bubble {
padding: 0 0 var(--space-2);
@@ -2257,12 +2237,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
gap: var(--space-3);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant .agent-avatar {
width: 28px;
height: 28px;
flex-basis: 28px;
}
.conversation:not(.is-empty):not(.is-rectification) .message-user .message-content {
max-width: 88%;
}
@@ -2465,9 +2440,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
The pending state keeps the same box for the same reason. */
.rectification-workspace__chat {
--rectification-timeline-height: 64px;
/* Same token as `.message-list`: avatar 32px + gap. The bar sits outside
the transcript, so it cannot inherit that custom property from there. */
--assistant-content-inset: calc(32px + var(--space-3));
/* Same token as `.message-list`. The bar sits outside the transcript, so it
cannot inherit that custom property from there. */
--assistant-content-inset: 0px;
}
.rectification-timeline {
height: var(--rectification-timeline-height);
-13
View File
@@ -1493,16 +1493,6 @@ export default function Home() {
subtitle: sessionSidebarSubtitle(session, chartLibrary),
pinned: session.pinned, archived: Boolean(session.archivedAt), updatedAt: session.updatedAt,
}));
const sidebarCharts = (chartLibrary.length > 0
? chartLibrary
: profile.name.trim()
? [{ id: "self", role: "self" as const, profile, updatedAt: timestamp() }]
: []
).map((record) => ({
id: record.id,
name: record.profile.name.trim() || (record.role === "self" ? "我" : "对方"),
role: record.role,
}));
const modalOpen = activeAccountDialog !== null || onboardingPaywallOpen;
const accountOverlayModel: AccountOverlayModel | null = activeAccountDialog === null ? null : {
@@ -1557,7 +1547,6 @@ export default function Home() {
<span className="sr-only" role="status" aria-live="polite" aria-atomic="true">{replyAnnouncement}</span>
<AppSidebar
sessions={sidebarSessions}
charts={sidebarCharts}
activeSessionId={activeSession?.id ?? null}
openingSessionId={rectificationOpeningSessionId}
openErrorSessionId={rectificationErrorSessionId}
@@ -1593,8 +1582,6 @@ export default function Home() {
onNewChat={() => void startNewChat()}
onOpenReports={() => router.push("/reports")}
onSelectSession={selectSession}
onSelectChart={() => openAccountDialog("chart-library")}
onAddChart={() => openAccountDialog("chart-library")}
onOpenProfile={() => openAccountDialog("profile")}
onOpenChartLibrary={() => openAccountDialog("chart-library")}
onOpenGeneral={() => openAccountDialog("general")}
+33 -89
View File
@@ -3,9 +3,7 @@
import { Menu } from "@base-ui/react/menu";
import {
CalendarDays,
ChevronDown,
ChevronRight,
Clock3,
FileText,
LogOut,
Orbit,
@@ -14,7 +12,6 @@ import {
MessageSquareText,
SquarePen,
Star,
UserPlus,
UserRound,
Users,
} from "lucide-react";
@@ -28,7 +25,6 @@ import {
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
@@ -54,15 +50,8 @@ export type SidebarAccount = {
avatar: BeamAvatar | null;
};
export type SidebarChart = {
readonly id: string;
readonly name: string;
readonly role: "self" | "other";
};
export type AppSidebarProps = {
sessions: readonly SidebarSession[];
charts: readonly SidebarChart[];
activeSessionId: string | null;
/** A rectification session whose Case is being opened and hydrated; its row says so, statically. */
openingSessionId?: string | null;
@@ -79,8 +68,6 @@ export type AppSidebarProps = {
onNewChat: () => void;
onOpenReports: () => void;
onSelectSession: (sessionId: string) => void;
onSelectChart: (chartId: string) => void;
onAddChart: () => void;
onOpenProfile: () => void;
onOpenChartLibrary: () => void;
onOpenGeneral: () => void;
@@ -90,7 +77,6 @@ export type AppSidebarProps = {
export function AppSidebar({
sessions,
charts,
activeSessionId,
openingSessionId = null,
openErrorSessionId = null,
@@ -105,8 +91,6 @@ export function AppSidebar({
onNewChat,
onOpenReports: _onOpenReports,
onSelectSession,
onSelectChart,
onAddChart,
onOpenProfile,
onOpenChartLibrary,
onOpenGeneral,
@@ -116,7 +100,7 @@ export function AppSidebar({
const { isMobile, setOpen, setOpenMobile, state, viewport } = useSidebar();
const pathname = usePathname();
const firstSessionRef = useRef<HTMLButtonElement>(null);
const historyHeadingRef = useRef<HTMLElement>(null);
const historyHeadingRef = useRef<HTMLParagraphElement>(null);
const loadMoreRef = useRef<HTMLDivElement>(null);
const isCollapsedDesktop = state === "collapsed" && !isMobile;
const showExpandedContent = !isCollapsedDesktop;
@@ -262,83 +246,43 @@ export function AppSidebar({
<SidebarContent>
{showExpandedContent ? (
<>
<SidebarGroup className="chart-nav" aria-label="星盘列表">
<div className="session-nav-header">
<SidebarGroupLabel className="sidebar-label">
<Users size={18} strokeWidth={1.75} aria-hidden="true" />
</SidebarGroupLabel>
<button
className="session-nav-toggle"
type="button"
aria-label="添加星盘"
onClick={onAddChart}
>
<UserPlus size={18} strokeWidth={1.75} aria-hidden="true" />
</button>
</div>
<SidebarGroup className="session-nav" aria-label={sessionControls.showingArchived ? "归档记录" : "最近对话"}>
{/* One flat list. It was three stacked sections a chip grid
and two <details> wrappers for / so the nav
carried two collapse affordances before the first session row.
Pinned sessions keep their priority as the first labelled group,
in the same shape as the recency groups under them. */}
<p className="sidebar-section-label" ref={historyHeadingRef} tabIndex={-1}>
{sessionControls.showingArchived ? "归档记录" : "最近"}
</p>
<SidebarGroupContent className="sidebar-nested">
{charts.length === 0 ? <p className="sidebar-empty"></p> : (
<div className="chart-nav-list">
{charts.map((chart) => (
<button
className="chart-nav-chip"
key={chart.id}
type="button"
onClick={() => onSelectChart(chart.id)}
>
{chart.name}
</button>
{favoriteSessions.length === 0 && historySessions.length === 0 ? (
<p className="sidebar-empty"></p>
) : (
<SidebarMenu className="session-list">
{favoriteSessions.length > 0 ? (
<div>
<p className="sidebar-group-label">
<Star size={13} strokeWidth={1.75} aria-hidden="true" />
</p>
{favoriteSessions.map((session, index) => renderSession(session, index))}
</div>
) : null}
{historyGroups.map((group) => (
<div key={group.key}>
<p className="sidebar-group-label">{group.label}</p>
{group.sessions.map((session) => renderSession(
session,
favoriteSessions.length + historySessions.findIndex((item) => item.id === session.id),
))}
</div>
))}
</div>
{sessionControls.hasMore ? <div ref={loadMoreRef} aria-hidden className="session-list-sentinel" /> : null}
</SidebarMenu>
)}
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup className="session-nav" aria-label="收藏对话">
<details className="sidebar-section" open>
<summary className="sidebar-section-summary">
<Star size={18} strokeWidth={1.75} aria-hidden="true" />
<span></span>
<ChevronDown className="sidebar-section-chevron" size={16} strokeWidth={1.75} aria-hidden="true" />
</summary>
<SidebarGroupContent className="sidebar-nested">
{favoriteSessions.length === 0 ? <p className="sidebar-empty"></p> : (
<SidebarMenu className="session-list">
{favoriteSessions.map((session, index) => renderSession(session, index))}
</SidebarMenu>
)}
</SidebarGroupContent>
</details>
</SidebarGroup>
<SidebarGroup className="session-nav" aria-label={sessionControls.showingArchived ? "归档记录" : "历史对话"}>
<details className="sidebar-section" open>
<summary className="sidebar-section-summary" ref={historyHeadingRef} tabIndex={-1}>
<Clock3 size={18} strokeWidth={1.75} aria-hidden="true" />
<span>{sessionControls.showingArchived ? "归档记录" : "历史对话"}</span>
<ChevronDown className="sidebar-section-chevron" size={16} strokeWidth={1.75} aria-hidden="true" />
</summary>
<SidebarGroupContent className="sidebar-nested">
{historySessions.length === 0 ? <p className="sidebar-empty"></p> : (
<SidebarMenu className="session-list">
{historyGroups.map((group) => (
<div key={group.key}>
<p className="sidebar-group-label">{group.label}</p>
{group.sessions.map((session) => renderSession(
session,
favoriteSessions.length + historySessions.findIndex((item) => item.id === session.id),
))}
</div>
))}
{sessionControls.hasMore ? <div ref={loadMoreRef} aria-hidden className="session-list-sentinel" /> : null}
</SidebarMenu>
)}
</SidebarGroupContent>
</details>
</SidebarGroup>
</>
) : (
<SidebarMenu>
<SidebarMenuItem>
@@ -31,10 +31,6 @@ function motionPreferred() {
if (motionPreferred()) prefetchOnIdle(loadGsap);
export function AgentAvatar() {
return <span className="agent-avatar" aria-hidden="true" />;
}
export const ChatMessageRow = memo(function ChatMessageRow({
message,
showActivity = message.state !== "settled",
@@ -127,7 +123,6 @@ export const ChatMessageRow = memo(function ChatMessageRow({
className={`message message-${message.role}`}
aria-label={message.role === "assistant" ? assistantLabel : "你"}
>
{message.role === "assistant" && <AgentAvatar />}
<div className="message-content">
<div className="message-bubble">
{message.role === "assistant" ? (
@@ -1,7 +1,6 @@
"use client";
import { ChatMessageContent } from "@/components/chat-message-content";
import { AgentAvatar } from "@/components/chat-message-row";
import { protectOnboardingPhrases } from "@/lib/onboarding-copy";
import type { Message } from "@/lib/home-types";
@@ -10,7 +9,6 @@ export function OnboardingChatMessage({ role, text, streaming = false, length =
const protectedVisibleText = protectOnboardingPhrases(visibleText);
return (
<article className={`message message-${role} onboarding-message${phraseSafe ? " is-phrase-safe" : ""}`} aria-label={role === "assistant" ? "Jyotisha" : "你"}>
{role === "assistant" && <AgentAvatar />}
<div className="message-content">
<div className="message-bubble">
{role === "assistant" ? (
@@ -55,7 +55,6 @@ const tailwindCollisions = new Set([
// on the same element.
const knownUnstyled = new Set([
"birth-time-evidence-receipt",
"chart-nav",
"is-done",
"is-live",
"is-working",
@@ -487,7 +487,7 @@ test("candidate state renders from the snapshot API and never from sentinels", (
assert.match(styles, /\.rectification-snapshot \{/);
assert.match(styles, /\.rectification-house-table \{[^}]*border:\s*1px solid var\(--color-border\)/);
assert.match(styles, /\.rectification-house-table \{[^}]*background:\s*var\(--color-canvas-soft\)/);
assert.match(styles, /\.message-list \{[\s\S]*--assistant-content-inset: calc\(32px \+ var\(--space-3\)\)/);
assert.match(styles, /\.message-list \{[\s\S]*?--assistant-content-inset: 0px/);
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*width: calc\(100% - var\(--assistant-content-inset\)\)/);
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/);
assert.match(styles, /\.rectification-workspace \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) minmax\(18rem, 22\.5rem\)/);
@@ -408,7 +408,7 @@ test("the chat grid has three rows and the timeline row is a fixed height", () =
// stick-to-bottom, since it sits outside the observed scroll container.
assert.doesNotMatch(barRule, /min-height|height:\s*auto/);
// Inner padding matches the assistant column, not the chat-panel edge.
assert.match(chatRule, /--assistant-content-inset: calc\(32px \+ var\(--space-3\)\);/);
assert.match(chatRule, /--assistant-content-inset: 0px;/);
assert.match(barRule, /padding: 0 calc\(var\(--space-8\) \+ var\(--assistant-content-inset\)\);/);
});
+30 -11
View File
@@ -68,7 +68,11 @@ test("the mobile drawer is opaque; the glass surface is a desktop treatment", ()
test("a section heading outranks its own body copy", () => {
// Headings and their empty-state copy both sat on ink-tertiary, so the nav
// read as one flat grey with no hierarchy to scan.
const heading = cssDeclarations(".sidebar-label, .sidebar-section-summary", globalStyles);
// 原值:选择器 `.sidebar-label, .sidebar-section-summary`(两种标题:分组标签与折叠摘要)
// 新值:`.sidebar-section-label` 一种
// 原因:星盘列表分组与两个 <details> 都已删除,侧栏只剩一条平铺列表和它的一个标题。
// 「标题要压过自己的正文」这条规则本身没变,仍然断。
const heading = cssDeclarations(".sidebar-section-label", globalStyles);
assert.match(heading, /color:\s*var\(--color-ink-secondary\)/);
assert.doesNotMatch(topLevelRule(".sidebar-empty"), /--color-ink-secondary/);
assert.match(topLevelRule(".sidebar-empty"), /color:\s*var\(--color-ink-tertiary\)/);
@@ -78,8 +82,12 @@ test("empty sidebar sections name the next step instead of dead-ending", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
// Neutral on purpose: 新建对话 already carries the accent a few rows above,
// and a second tinted call to action would compete with it.
assert.match(appSidebar, /还没有收藏,可在对话的「更多操作」里收藏/);
// 原值:断言「收藏对话」与「历史对话」各有一句空态文案
// 新值:只剩一句——两个分组合并成一条平铺列表后,只有一个空态
// 原因:「还没有收藏」是给独立收藏分区用的。收藏现在是列表顶部的一个标签组,
// 没有收藏时它整块不渲染;再对一个不存在的分区说「还没有」是噪音。
assert.match(appSidebar, /暂无对话,点上方「新建对话」开始/);
assert.doesNotMatch(appSidebar, /还没有收藏/);
assert.doesNotMatch(topLevelRule(".sidebar-empty"), /--color-action/);
});
@@ -240,8 +248,13 @@ test("keeps app sidebar props as product data and callbacks", () => {
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
assert.match(appSidebar, /export type AppSidebarProps/);
assert.match(appSidebar, /onSelectSession: \(sessionId: string\) => void/);
assert.match(appSidebar, /onSelectChart: \(chartId: string\) => void/);
assert.match(appSidebar, /charts: readonly SidebarChart\[\]/);
// 原值:断言 `charts` 与 `onSelectChart` 在 props 里
// 新值:两者连同 `onAddChart`、`SidebarChart` 类型一起删除
// 原因:侧栏的「星盘列表」chip 与加号按钮打开的,和账户菜单「星盘资料」是**同一个弹窗**
// page.tsx 里三个回调都是 openAccountDialog("chart-library"))。一组点了只会开弹窗的
// 名字不提供导航价值。入口收敛到账户菜单一处,见任务书 D6。
assert.doesNotMatch(appSidebar, /onSelectChart|onAddChart|SidebarChart/);
assert.match(appSidebar, /onOpenChartLibrary: \(\) => void/);
assert.doesNotMatch(appSidebar, /supabase|fetch\(|\/api\//i);
});
@@ -394,7 +407,10 @@ test("nests sidebar lists under one heading scale without an archive toggle", ()
assert.doesNotMatch(appSidebar, /归档 \$\{/);
assert.doesNotMatch(appSidebar, /onToggleArchivedView/);
assert.match(appSidebar, /className="sidebar-nested"/);
assert.match(cssBlock(".sidebar-nested"), /calc\(var\(--space-3\) \+ 18px \+ var\(--space-2\)\)/);
// 原值:列表缩进 = 左边距 + 标题图标 18px + 间隙,用来让列表对齐折叠标题的文字
// 新值:`padding: 0 var(--space-3) var(--space-1)`,与侧栏自身的边距一致
// 原因:列表不再嵌在可折叠标题之下,没有需要对齐的标题图标了。
assert.match(cssBlock(".sidebar-nested"), /padding:\s*0 var\(--space-3\) var\(--space-1\)/);
assert.match(cssBlock(".session-title"), /font-size:\s*var\(--type-caption\)/);
assert.match(cssBlock(".session-title"), /font-weight:\s*500/);
assert.match(cssBlock(".session-row"), /color:\s*var\(--sidebar-foreground\)/);
@@ -402,16 +418,19 @@ test("nests sidebar lists under one heading scale without an archive toggle", ()
// headings and their own body copy indistinguishable. The shared scale is
// what this test guards; the rank between the two levels is asserted in
// "a section heading outranks its own body copy".
assert.match(cssBlock(".sidebar-label"), /color:\s*var\(--color-ink-secondary\)/);
assert.match(cssBlock(".sidebar-section-summary"), /color:\s*var\(--color-ink-secondary\)/);
assert.match(cssBlock(".sidebar-label"), /font-size:\s*var\(--type-caption\)/);
assert.match(cssBlock(".sidebar-section-summary"), /font-size:\s*var\(--type-caption\)/);
// 原值:`.sidebar-label` 与 `.sidebar-section-summary` 两套标题各断一次
// 新值:合并为 `.sidebar-section-label` 一套
// 原因:同上,两种标题形态已合一。字号与色阶要求不变。
assert.match(cssBlock(".sidebar-section-label"), /color:\s*var\(--color-ink-secondary\)/);
assert.match(cssBlock(".sidebar-section-label"), /font-size:\s*var\(--type-caption\)/);
assert.match(cssBlock(".new-chat"), /font-size:\s*var\(--type-body-sm\)/);
assert.match(cssBlock(".report-nav-button"), /font-size:\s*var\(--type-body-sm\)/);
assert.match(cssBlock(".new-chat svg"), /width:\s*18px/);
assert.match(cssBlock(".report-nav-button svg"), /width:\s*18px/);
assert.match(cssBlock(".sidebar-label > svg"), /width:\s*18px/);
assert.match(cssBlock(".sidebar-section-summary > svg"), /width:\s*18px/);
// 原值:两种标题的图标各 18px / 新值:标题不再带 18px 图标
// 原因:唯一剩下的标题是「最近」两个字,不带图标;收藏分组的星标是 13px 的
// 行内标记,属于 `.sidebar-group-label`,不是标题图标。
assert.match(cssBlock(".sidebar-group-label > svg"), /color:\s*var\(--color-action\)/);
assert.match(cssBlock(".session-title > svg"), /color:\s*currentColor/);
assert.doesNotMatch(appSidebar, /session-nav-header-inline/);
});
+6 -2
View File
@@ -38,8 +38,12 @@ test("icon and chip controls keep their visual size and expand the hit with a ps
assert.match(actionHit, /width:\s*27px/);
assert.match(actionHit, /height:\s*34px/);
assert.equal(minHeightPx(".chart-nav-chip"), 32);
assert.match(hasHitExpandPseudo(".chart-nav-chip"), /inset:\s*-4px/);
// 原值:`.chart-nav-chip`(侧栏星盘列表的名字 chip
// 新值:`.starter-entry`(空态的两枚入口 pill
// 原因:星盘列表 chip 已随入口收敛删除(D6)。空态的入口 pill 是这一类「视觉小于
// 44px、靠伪元素扩热区」控件的新成员,36px 视觉 + -4px 外扩,正好归这条合同管。
assert.equal(minHeightPx(".starter-entry"), 36);
assert.match(hasHitExpandPseudo(".starter-entry"), /inset:\s*-4px/);
const auth = cssDeclarations(".auth-links button", globalStyles);
assert.match(auth, /min-height:\s*32px/);