Compare commits

...
Author SHA1 Message Date
jesse-uxandGrok (agent loop) 689e01296e 组件库收尾:删掉没人用的旧样式,检查名单只留绘制例外(#111)。
Independent Staging Quality Gate / validate (push) Failing after 9m7s
Independent Staging Quality Gate / publish (push) Skipped
globals.css 从 7249 行减到 7011 行,删了 56 个已经没人用的类,16 个关键帧都还在用。手写控件名单从 9 个文件收到 2 个:天空绘制和校正顶栏探头。星盘和星图的图标豁免仍在规则里。设计说明改成以 shadcn base-nova 和 Motion 预设为准。页面用法没有再改。

自测:tsc --noEmit 退出码 0;frontend 下 npm run lint 0 error、122 warning;白名单、样式隔离、时间线和错误边界测试 18 条通过、失败 0、取消 0;npm run build 后 / 仍是 Static。首屏 JS gzip-9 相对 728ead88 从 644683 到 648993(+0.67%)。没有跑全量 npm test、Python 快速门和 test:db。

Co-Authored-By: Grok (agent loop)
2026-10-25 06:56:39 +08:00
20 changed files with 352 additions and 538 deletions
+7
View File
@@ -1,5 +1,12 @@
# 印度占星 Skill 更新日志
## 2026-10-25 — 组件库收尾:去掉没人用的旧样式
- 删掉样式表里已经没有页面使用的旧类。还在用的类和动画都留着。
- 手写控件的检查名单收到只剩两处:那一刻的天空绘制,以及校正盘面挂在顶栏的探头。星盘和星图的画法仍可例外。
- 设计说明改成以组件库为准。页面上的按钮、文案和操作没有再改一轮。
- 没有新的故障。Skill 版本不变。
## 2026-10-25 — 生时校正的选项卡片换成组件库
- 校正里的选项仍是圆角卡片。点中之后是浅底描边。确定程度仍是三个圆点,大概范围仍是一排标签。
+3 -3
View File
@@ -19370,14 +19370,14 @@
- 状态:enhancement
- 首次发现:2026-10-24
- 最近更新:2026-10-24
- 影响面:前端依赖、对话消息入场、设计令牌别名、新增的 shadcn 组件、手写控件检查。侧栏、对话页、星盘页的控件这一张没有换。
- 最近更新:2026-10-25。K7 把白名单从迁完后剩下的 9 个文件收到 2 个。没有新的故障现象,不另编号。
- 影响面:前端依赖、对话消息入场、设计令牌别名、新增的 shadcn 组件、手写控件检查。侧栏、对话页、星盘页的控件这一张没有换。K7 只删没用的旧样式,并换掉还留在名单里的普通按钮。
- 用户现象:页面上的控件和文案与改前相同。对话新出现的一行仍是一次短入场;系统开了「减少动态效果」时不做这次入场。
- 触发条件:打开首页或已有对话。这一张不改变请求、扣点或落库。
- 根因:不是缺陷。产品在组件库迁移里定了换用 Motion、shadcn、按需加载和 lint 拦截(D1、D2、D3、D4、D7)。这一张只加底座。
- 修复:去掉 GSAP。消息入场改为按需加载的 Motion,时长仍是 160 毫秒。用固定版本的 shadcn CLI 补上任务书列出的组件,不重装已有的 8 个。设计令牌映射到 shadcn 变量,深色浅色都跟着现有色板。Motion 预设和四个 store 只是骨架,没有接到页面上。`src/components/ui` 以外的新手写控件会报错;迁移前已有的文件在白名单里,测试锁住这份名单不能变长。星盘与星图的 SVG 绘制路径豁免图标规则。
- 验证:见 `docs/tasks/PROGRESS-ui-kit-0-base-20261023.md`。没有浏览器登录态,不把界面截图写成通过。
- 防复发:`frontend/tests/ui-kit-whitelist.test.ts` 锁住白名单只减不增,并检查新文件里的 `<button>` 会报错。`frontend/tests/chat-bundle-splitting-contract.test.ts` 与 `frontend/tests/session-conversation-layout.test.ts` 锁住入场仍是按需加载、160 毫秒、尊重减少动态效果。`frontend/tests/ui-kit-tokens.test.ts` 锁住 shadcn 样式导入和颜色别名。
- 防复发:`frontend/tests/ui-kit-whitelist.test.ts` 锁住白名单只减不增,并检查新文件里的 `<button>` 会报错。K7 把上限从 K0 的 62 个文件改成 2 个:`birth-sky-converge.tsx`(天空绘制用原生 dialog 舞台,换掉会改绘制)和 `rectification-agentic-chat.tsx`(盘面探头用 createPortal 挂到顶栏,改成弹窗会改交互)。星盘和星图的图标豁免仍在规则里,不在这份名单。`frontend/tests/chat-bundle-splitting-contract.test.ts` 与 `frontend/tests/session-conversation-layout.test.ts` 锁住入场仍是按需加载、160 毫秒、尊重减少动态效果。`frontend/tests/ui-kit-tokens.test.ts` 锁住 shadcn 样式导入和颜色别名。
- 相关记录:issue #104。任务书 `docs/tasks/TASK-ui-kit-migration-20261023.md` 的 K0。
- 复发自:无
- 修复版本:分支 `codex/ui-kit-0-base-20261023`。
@@ -0,0 +1,62 @@
# PROGRESS · 组件库 K7 收尾(2026-10-25)
工单 #111。分支 `codex/ui-kit-7-cleanup-20261023`。基线 `origin/staging` `728ead8844631747b13346354e7a5fc37a72800c`(K0 到 K6 和启程相关改动都在这一笔里)。只做任务书 **K7 收尾**。没有重做 K1–K6 的交互,没有改页面功能,没有动数据库。
## 结论
删掉了样式表里已经没人用的旧类。手写控件的检查名单收到两处不能换掉的例外。设计说明改成以组件库为准。页面上的字和点下去做的事没有再改一轮。
## 样式
`frontend/src/app/globals.css`:改前 7249 行,改后 7011 行。删了 56 个已经没有组件或测试使用的类。16 个 `@keyframes` 都还有人用,一个都没删。
删掉的类:`admin-page`、`agent-activity-status__row`、`agent-activity-status__trail`、`birth-time-event-list`、`birth-time-event-remove`、`birth-time-event-row`、`birth-time-question`、`birth-time-question-list`、`birth-time-retry-card`、`chart-library-actions`、`chart-library-add`、`chart-library-add--heading`、`chart-library-back`、`chart-library-count`、`chart-library-group`、`chart-library-group-heading`、`chart-library-group-heading-actions`、`chart-library-item`、`chart-library-item-chevron`、`chart-library-item-main`、`chart-library-item-title`、`chat-profile-picker-positioner`、`code-status`、`conversational-candidate`、`conversational-candidate-progress`、`conversational-composer`、`conversational-composer-footer`、`conversational-confirmation`、`conversational-empty-state`、`conversational-evidence-recap`、`conversational-narrative-body`、`conversational-original-question`、`conversational-status`、`default-chart-card`、`empty-library-copy`、`feedback-dialog-panel`、`is-confirmed`、`is-unverified`、`model-selector-options`、`model-selector-positioner`、`payment-qr-badge`、`payment-qr-wrap`、`people-archive-confirm`、`rectification-analysis`、`rectification-analysis-content`、`rectification-board__section`、`rectification-candidate-action`、`rectification-candidate-badge`、`rectification-candidate-time`、`rectification-refinement`、`rectification-refinement__stage`、`rectification-session-surface`、`reply-down-reason-close`、`report-export-field`、`settings-dialog-copy`、`welcome-copy`。
删之前按类名检索过。这些类只出现在样式表里。还在用的规则,包括只剩一部分选择器还在用的规则,都留着。
## 检查名单
迁完 K6 之后名单里还有 9 个文件。这一张去掉 7 个普通按钮,换成已有的 `Button`。字、点击之后做的事、原来的类名都留着。
移出名单的 7 个文件:`error.tsx`、`ayanamsa-preference-field.tsx`、`birth-place-picker.tsx`、`consultation-run-timeline.tsx`、`home-bootstrap-error.tsx`、`home-onboarding-shell.tsx`、`personal-report-markdown-view.tsx`。
名单现在只剩:
- `src/components/birth-sky/birth-sky-converge.tsx`:那一刻的天空绘制。合同锁住原生 `dialog.showModal` 和 `createPortal`。换成普通弹窗会改绘制。
- `src/components/rectification-agentic-chat.tsx`:校正盘面探头用 `createPortal` 挂到顶栏。合同锁住这句。改成弹窗会把 K6 的探头变成弹窗。
星盘 `vedic-chart-svg.tsx` 和星图 `western-wheel-svg.tsx` 的图标豁免仍写在规则里,不在这份名单里。按钮、菜单和 `createPortal` 在这两个文件里仍然会报错。没有为了清名单去改绘制。
规则本身没有放宽。测试仍要求名单只能变短,不能把旧文件加回去。
## 说明
- `frontend/DESIGN.md` 第一节改为「组件一律用 shadcn base-nova,令牌如下」。动效节改为 Motion 预设表。`CLAUDE_DESIGN.md` 降为文首的参照句,文件没有删。外壳、对话、星盘、档案、报告、校正的已有说明留在后面的章节里。
- `frontend/docs/VOICE.md` 的导出说明去掉已经不存在的文件大小和「拿去问 AI」。界面上还在的句子留着。
- 没有新的故障现象,不新编号。`docs/BUG_HISTORY.md` 只更新 BUG-1430 的最近更新和防复发。最大号仍是 BUG-1438。
- 没有改 `page.tsx`。`Home()` 的 useState / useRef 没有增长。没有给 `scripts/jyotish_api_server.py` 加方法。没有改 `.gitea/**`,没有改生产部署,没有升级依赖。
## 断言改动(原值 / 新值 / 原因)
| 位置 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `frontend/tests/ui-kit-whitelist.test.ts` 名单上限 | K0 的 62 个文件 | 天空绘制和校正顶栏探头这两个文件 | K7 清空白名单。星盘和星图的图标豁免在规则里,不在名单。改上限之后仍然不许名单变长 |
| `frontend/tests/site-style-isolation-contract.test.ts` 根错误页 | `error.tsx` 与 not-found 都不得 import Button | `error.tsx` 可以 import Button,颜色仍写在内联样式里;not-found 仍不得 | K7 清空白名单。这一页不引进整站样式表 |
| `frontend/tests/chat-stream-settle-contract.test.ts` 步骤摘要的 class | `className="consultation-run-timeline__summary"` 整段恰好如此 | 同一类名仍在,后面可以再接压掉按钮默认高度和换行的类 | K7 把这一行换成组件库 Button。类名、展开和 180ms 高度过渡不变 |
## 自测
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 类型 | 仓库根 `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 |
| 检查 | 在 `frontend` 下 `npm run lint` | 退出码 0。122 个警告,0 error。警告数和上一张记下的一样 |
| 名单与改过的断言 | 在 `frontend` 下 `node node_modules/tsx/dist/cli.mjs --test`,四个文件:`ui-kit-whitelist.test.ts`、`site-style-isolation-contract.test.ts`、`chat-stream-settle-contract.test.ts`、`root-error-boundaries-contract.test.ts` | 18 条。通过 18,失败 0,取消 0 |
| 构建 | 在 `frontend` 下 `npm run build` | 退出码 0。编译 7.5s,TypeScript 24.0s。79 个静态页。`/` 仍是 ○ Static。5 条既有 skill-package-registry 警告,没有改 |
| 首屏体积 | 首页 `index.html` 引用的 `/_next/static/**.js`,gzip-9 相加,不含 CSS | 基线 `728ead88` 同一方法:37 个文件,缺失 0,644683 字节。本张 38 个文件,缺失 0,648993 字节,多 4310(+0.67%)。±2% 大约是 631789 到 657577 |
没有跑前端全量 `npm test`,没有跑 Python 快速门,没有跑 `test:db`。
## 环境缺口
- 没有登录态。没有在浏览器里逐页看过。不写成通过。可照做的步骤在 `docs/testing/ui-kit-migration-20261023.md` 的「收尾」一节。
- 本轮只推 `codex/ui-kit-7-cleanup-20261023`,没有推 staging。任务索引按要求写成「K0–K7 已在测试环境,待验收」,意思是整串做完、等验收,不是已经有人看过。
+1 -1
View File
@@ -39,7 +39,7 @@
| --- | --- | --- | --- | --- |
| `TASK-rectification-explicit-open-20261023.md` | `PROGRESS-rectification-explicit-open-20261023.md` | **明说要校正只回一句话(issue #103)**:找会话直接读一张对服务角色撤权的表、必然失败被吞(真正进行中的也续不上);`candidate_accepted` 被算作进行中挡住入口和提议卡;兜底只给一句话。产品选 C;只改入口口径、经 RPC 找会话、删兜底,不改共享状态集合、不动表。BUG-1423 | 已合入 staging | 分支 `codex/rectification-explicit-open-20261023` |
| `TASK-chart-table-span-20261023.md` | `PROGRESS-chart-table-span-20261023.md` | **星盘页本命 / 月亮行星表挤进左栏(issue #102)**:#87 给表包了 `ChartSymbolHelp` 后跨栏规则失配;根因修法是 `VedicLayout` 用固定整行容器承载表格,跨栏只认容器;加结构断言防复发。BUG-1422 | 已合入 staging | 分支 `codex/chart-table-span-20261023` |
| `TASK-ui-kit-migration-20261023.md` | — | **界面统一换成组件库**:shadcn base-nova(补 `shadcn/tailwind.css`)· Lucide · Motion · react-resizable-panels · dnd-kit · Zustand;lint 拦手写控件(白名单只减不增);对话旁星盘面板;会话、档案拖动排序(动表);长相不变。产品 2026-10-23 拍板 D1~D8;8 个 issue(K0~K7)串行,取代全站动效 #98 #100 #101 | K0 待验收(K1–K7 仍待领取) | 原型 https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG |
| `TASK-ui-kit-migration-20261023.md` | `PROGRESS-ui-kit-0-base-20261023.md`;`PROGRESS-ui-kit-1-shell-20261023.md`;`PROGRESS-ui-kit-2-chat-20261023.md`;`PROGRESS-ui-kit-3-chart-20261023.md`;`PROGRESS-ui-kit-4-people-20261023.md`;`PROGRESS-ui-kit-5-pages-20261023.md`;`PROGRESS-ui-kit-6-rectification-20261023.md`;`PROGRESS-ui-kit-7-cleanup-20261023.md` | **界面统一换成组件库**:shadcn base-nova(补 `shadcn/tailwind.css`)· Lucide · Motion · react-resizable-panels · dnd-kit · Zustand;lint 拦手写控件(白名单只减不增);对话旁星盘面板;会话、档案拖动排序(动表);长相不变。产品 2026-10-23 拍板 D1~D8;8 个 issue(K0~K7)串行,取代全站动效 #98 #100 #101 | K0–K7 已在测试环境,待验收 | 原型 https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG |
| `TASK-onboarding-journey-20261022.md` | `PROGRESS-onboarding-1-data-20261022.md`;`PROGRESS-onboarding-2-intake-20261022.md`;`PROGRESS-onboarding-4-motion-20261022.md` | **新用户启程**:建档加「先看谁的盘」(自己 / 帮别人看,本人资料可后补)、人物关系字段、首问免费(服务端原子)、起步三问、启程清单(七项从数据推导,收费项标价,老用户可见)、八条一次性提示(含星盘四页签)、启程流程专用动效。产品 2026-10-22 拍板 D1~D6;动表经产品同意进循环,拆四个 issue 串行。BUG-1420 已被文档去重占用,本轮首问免费记 BUG-1421。(一)T1+T2 在 `codex/onboarding-1-data-20261022`。(二)T3+T4 记 BUG-1431,在 `codex/onboarding-2-intake-20261022`。(四)T7 在 `codex/onboarding-4-motion-20261022` | (一)(二)(三)已在 staging;(四)本分支待验收 | 原型 https://claude.ai/artifact/PcTwMVb9RD611R3qStxv3U |
| `TASK-site-motion-20261022.md` | — | **全站动效**:换页按侧栏方向滑入、选中块跟随、页签、盘型切换星体移宫、换人、翻日期、列表补位、弹窗从触发处长出、抽屉、顶部提示、点数滚动、中英交叉淡入;重写 DESIGN §6(推翻 180 ms 上限与 Spatial Instant),不引动画库,先验证 View Transitions。产品 2026-10-22 拍板;三个 issue,排在启程(三)之后、启程(四)之前;**2026-10-23 被 ui-kit-migration 取代** | 已取代 | 原型 https://claude.ai/artifact/7u9goivnmTNSGTNWNeVMQd |
| `TASK-rectification-narrower-delivery-20261016.md` | `PROGRESS-rectification-narrower-delivery-fix-20261017.md` | **交付区间收窄**:显示改按活着的簇分段。第 1 次打回只补两处显示漏段(取整差 8 分、缺区间整段退回)。对半出题和门槛 8 改 6 仍不做。基线与对照各 924 条,12 格包含率都过红线。BUG-1367 起,本轮原记 BUG-1370,因与婚恋合盘保留号冲突改为 BUG-1392 | 待验收 | `codex/rectification-narrower-delivery-20261016` |
+10
View File
@@ -98,3 +98,13 @@
4. 校正进行中可以打开当前盘面,再关掉。关闭是一枚叉,读屏名是「关闭盘面」。窄屏上的探头仍在顶栏,不是弹窗。
5. 系统开了减少动态效果时,新出现的题目只淡入,选项不飞,也没有一直转的动画。
6. 做完或停下来之后,已经记下的范围还在。扣点和保存跟换皮之前一样。
## 收尾
这一轮只删了已经没人用的旧样式,并把设计说明改成以组件库为准。页面上的用法没有再改。没有登录态,没有在浏览器里逐页看过。下面留给产品负责人看。
1. 首页、星盘、星盘档案、婚恋合盘、报告、星历、登录、生时校正,各自仍按上面各节操作。按钮和句子应和上一轮看到的一样。
2. 导出抽屉里仍然没有文件大小,也没有「拿去问 AI」那句说明。按钮仍是「导出整篇」或「导出 N 项」。
3. 打开那一刻的天空。星星仍汇聚到定格,不是普通弹窗。左上关闭、右上分享仍在。
4. 校正进行中,窄屏上的盘面探头仍在顶栏,不是弹窗。
5. 页面出错时的「重试」,以及设置里的岁差、出生地失败后的重试、分析步骤那一行、建档的继续和确定、报告里的上一章和下一章,仍是原来的字,点了仍做原来的事。
+178 -170
View File
@@ -1,154 +1,13 @@
# Jyotisha Web Design System
## 外壳(2026-10-24)
`CLAUDE_DESIGN.md` 只作上游参照,不是实现合同。本文件写已经上线的界面。不要删那份参照文件。
桌面侧栏在可拖分栏里,宽 200–360px,默认 288。拖到最左收成 64px 窄轨;收起不把 64 写成宽度。宽度记在本机 `sidebar_width`。手机用左侧抽屉,宽 `min(86vw, 320px)`。当前页高亮用已有的 `layoutId` 滑动。翻页用已有的页面进入 280ms、离开 150ms,方向跟着侧栏顺序。顶栏点数是描边按钮:硬币图标加数字,44px;「点」只在可读名称里。数字变少时往下滚,变多时往上滚,360ms;系统要求减少动态时立刻换数字。启程清单胶囊仍是 32px,仍在点数左边。账户按钮只显示头像和名字。设置仍是三个分区。会话只在收藏组内、或最近一组内拖动排序;收藏整组仍在最前。翻页、高亮滑动、拖宽和拖动排序用的库在页面出来后再载入,不进首屏。
## 1. 组件一律用 shadcn base-nova,令牌如下
## 对话页(2026-10-25)
控件放在 `src/components/ui`,一律用 shadcn base-nova。缺的组件用固定版本的 shadcn CLI 加,不手抄,也不改组件的行为。图标只用 Lucide。动效只用 Motion,时长和曲线用下一节的预设。可拖宽、可收起的分栏用 react-resizable-panels。拖动排序用 dnd-kit。跨组件状态用 Zustand。
输入框仍是一个框。框底左边是模型菜单,只显示模型名;价格「N 点」只在菜单里。菜单用外壳已有的延后载入,不进首屏;库还没到时按钮上仍是模型名。右边是圆形发送,和框的底边对齐。回答下面赞、踩、复制、重新生成是 26px 的图标按钮,各有一句提示。不满意原因和兑换窗用组件库对话框,句子不改。追问是描边小按钮,仍按启程进场依次浮出(260ms,间隔 64ms),不另写一套时长。
`globals.css` 在 Tailwind 的下一行引入 `shadcn/tailwind.css`。长相不换:下面的色板就是令牌,映射成 shadcn 的 `--background`、`--primary` 等。深色跟着已有的 `--color-*`,不另抄一套。星盘、星图的 SVG 绘制,以及那一刻的天空这张画布,仍按后面的章节手绘。
桌面顶栏、点数右边,是打开或收起本命盘的图标按钮,提示「打开本命盘」或「收起本命盘」。面板在可拖分栏里,宽 280–560px,默认 360。拖到最窄可以收起。开着和宽度记在本机 `chat_chart_panel_open`、`chat_chart_panel_width`。手机(视口还没定,或小于 768)不显示这个按钮,也不因为藏起来就把「开着」写成关掉。面板里是当前人物的本命盘,复用星盘页的绘制,不写骨架、不写「正在加载」。可拖分栏和星盘绘制都在第一次打开后面才载入,不进首屏。
## 新用户启程(2026-10-25)
首页顶栏右侧、点数按钮左边,是一枚胶囊「启程 n/N」(`.onboarding-checklist-trigger`)。它沿用顶栏按钮的 32px 高度,圆角 999px,字和图标间距 6px。左边是 18×18 的进度环,描边按已完成的项画满。视口还没分出手机或桌面时,这枚胶囊不可打开。手机(侧栏视口小于 768)从底部抽屉拉出,带现有的下拉把手。平板和桌面用现有弹出层,靠右对齐,宽 `min(22rem, calc(100vw - 2rem))`,行是 `1.25rem minmax(0, 1fr) auto`,句子 `overflow-wrap: anywhere`。已完成的行用对勾,没有「去做」。底部是「先不看了」。
七项在这一次使用里刚满时,抽屉或弹出层再打开一次,进度环处同时迸一次星点(1000ms,只一次)。标题「都走过一遍了」,正文「以后有新东西,我会在用到的时候再告诉你。」关掉后胶囊消失。进站时已经满的账户,胶囊、这一屏和星点都不出现。
一次性提示用现有弹出层,宽 `min(18rem, calc(100vw - 2rem))`,不抢焦点。侧栏两项从锚点右侧伸出,其余从上方伸出,间距 8px。按钮是「知道了」。同一时刻最多一条。天空过场、兑换窗、任何弹窗或抽屉、启程清单正打开、回答还在生成时,提示不出;挡着的那一条等条件过去再出。侧栏「星盘」、手机菜单和星盘四个页签上的小圆点是 8px,贴在右上角 4px,不是字符。
启程专用动效只在元素第一次出现时播放。重绘、来回切换不重播。除回答流式中的呼吸点外,没有无限循环。只动 `opacity`、`transform`、颜色、`clip-path`,不触发布局。Motion 用到才加载,不进首屏。系统开启「减少动态效果」时,星点和圆点闪烁去掉,其余只淡入,没有位移、缩放和擦除。
| 类别 | 用在哪 | 时长 | 曲线 |
| --- | --- | --- | --- |
| 按下反馈 | 按钮、问题卡、选项、关系标签被按下时轻微缩小 | 120ms | `--ease-out` |
| 进场 | 建档消息与卡片、起步问题、追问胶囊、档案卡、目录行;依次上浮,间隔 64ms(落在 50–80ms) | 260ms | `--ease-out` |
| 弹出 | 一次性提示、「这一次不扣点」、勾 | 380ms | 回弹 `cubic-bezier(.34, 1.56, .64, 1)` |
| 跟手 | 称呼从输入框飞成气泡;起步问题飞成气泡 | 380ms | `--ease-out` |
| 勾飞进进度环 | 清单每完成一项:从操作位置沿弧线飞向胶囊,进度环走一格,胶囊轻跳一下 | 700ms | 弧线 |
| 画出来 | 建档进度条;星盘格线、西洋盘圆与分隔线、大运条 | 640–860ms(落在 520–900ms) | `--ease-out` |
| 场景转换 | 出生地→天空:从点击处圆形展开;天空→首页:淡出放大,首页同时上浮 | 680ms(落在 650–700ms) | `--ease-out` |
| 合盘 | 两个头像从两侧靠拢,泛起一圈光,再展开关系画像 | 1.2s | `--ease-out` |
| 庆祝 | 七项全满时,进度环处迸一次星点 | 1000ms,只一次 | `--ease-out` |
侧栏「星盘」和星盘页签上的小圆点,第一次出现时闪两下(800ms,有限次,不是循环)。减少动态效果时不闪。弹出层和抽屉本身仍走组件库已有的进出,不另加一套时长。
## 婚恋合盘报告(2026-10-19)
选人页不重复标题和副标题,页名在顶栏(2026-10-25)。两个人的选择框左右并排,中间一个叉;窄屏改成上下排,叉仍在正中。各自是下拉,最多二十人。资料不全的人不能选;已经在另一边的人不能再选。下面是「新增星盘」,点开一张卡片:称呼、性别(男 / 女)、出生日期、出生时间、出生地,右下角「取消」和「保存并选中」。满二十人时这一行换成去星盘档案删人。「开始合盘」在桌面和手机上都居中,按钮上不写价格。点了先问「会用掉 N 点,余额 M 点。」,按钮是「取消 / 开始」。余额不够时说「需要 N 点,余额只有 M 点。」,主按钮换成「去补点数」。N 从计价表读,M 从账户读。计价读不到时不弹确认,只说「计费配置不可用」。这两人已经有写好的合盘、且没有一份正在生成时,按钮下面再写已经合过几次、再生成会另扣多少点。之前的合盘只列人名和日期。生成中是轨道和四步(算两张完整命盘、整理两人之间的证据、写报告、检查报告),句子是「正在写 甲 × 乙 的合盘报告…」。报告页回到「婚恋合盘」,标题下是日期和「已保存,再次打开不扣点」,语言是「中文 / English」。第一次打开时两个头像仍从两侧靠拢;减少动态效果时只淡入。九节编号,分析逻辑默认收起。英文报告同样收起分析逻辑(Analysis,依据行是 Chart 和 So),时间线每段标题写出起止年月;第一段从本月起时,起点写「现在」或 Now。英文准备时同一套轨道,写「正在写英文版…」,揭幕后整份替换。报告末尾「对哪一段想再问清楚?」,下面是八枚胶囊(`.synastry-report-ask`),不含共同练习。点一枚只新建对话并预填问题,不自动发送。对话里人物选择器锁住,显示「甲 × 乙」。合盘删除后输入框占位是「这份合盘已删除」,发送停用。报告在整宽的外层滚动,两侧空白也能滚;中间一栏只负责居中排版。合盘报告单独走 `/synastry/报告`。开始合盘、之前的合盘和报告中心的合盘卡片都进这个地址,侧栏仍亮「婚恋合盘」。旧的 `/reports/报告` 会换成新地址,并保留语言。
## 生时校正从对话进入、做完回到原对话(2026-10-07,BUG-1247~1251,在途)
首页不再有「生时校正」pill,`.starter-entries` 只剩今日星语一枚(`.starter-entry-hint` 那句「生时校正暂时只支持本人。」随之删除)。入口在对话里:
- **提议卡**(`.rectification-offer-card`):挂在提议了校正的那条回答下面,与追问 chip 同一缩进(`--assistant-content-inset`),浅底细边圆角卡,一句说明(大约几题、几点、答第一题时才扣)+ 一个实心按钮「开始核对出生时间」。校正已开始时同一张卡改成「出生时间核对已经开始。」+ 描边按钮「继续核对出生时间」;回来之后卡片消失。对话进行中(loading / 取消中)按钮不可点。
- **回到刚才的对话**:从对话进来的校正,在校正面输入框上方一行描边小按钮(`.rectification-return-row`);没有来源对话的校正(历史里打开的、之前开的)不出这一行。采用后核对结束、或 case 结束时,在这个视图里做过操作的校正 2.5 秒后自动回去;从历史打开一个已结束的校正不会自动跳走。
- **分隔线**(`.rectification-return-divider`):回到原对话后追加一行居中小字「出生时间改为 HH:MM(生时校正)」或「出生时间没有改」,不是一条消息、没有操作行;改了时间时下面一个描边按钮「按新时间重新看:「原问题」」,点了才发送(普通一轮,照常计费)。
- **按校正前的时间**(`.message-before-change`):改了时间后,分隔线之前的每条回答上方一行灰色小字;原文不动。
不新增等待动画;打开校正沿用既有一次揭幕。
**选项和按钮换成组件库(2026-10-25,K6)**:选项卡片仍是原来的圆角卡片,点中后浅底描边,读屏仍读题干和选项原文,不另写「已选择」。确定程度仍是三个圆点。大概范围仍是一排标签。盘面关闭仍是一枚叉,探头仍挂在顶栏原来的位置,不是弹窗。其余确认、重新打开和采用用组件库按钮,样子沿用原来的实心、描边和文字按钮。题目出现时,若动效已经载入,只淡入;减少动态效果时同样只淡入,没有位移,也没有循环。题目、计分、扣点和保存没有改。明确要求校正仍直接开始,没做完的接着做。
**明确要求则直接打开(2026-10-12,BUG-1306)**:用户这一轮明说要做校正时,不再先出卡再点。回答只留一句过渡,界面沿用既有一次揭幕直接打开校正面。没有新的按钮、输入框、等待动画或 Home 状态。推得不准时的主动提议仍用 `.rectification-offer-card`。做不了时只显示一句话,不静默,也不在对话里假装校正。开案还没成功时,同一张提议卡仍是普通提议(不说「已经开始」,按钮仍是「开始核对出生时间」)。开案成功并绑定来源后才换成已开始的文案。打开失败时按钮仍可点。
## 用量页分段计时(2026-10-05,在途)
`/admin/usage` 在原表格上多一列「分段」。格子里四行:分类耗时、第 0 步、第 1 步、第 1 步开始到第一个正文字符。每步带推理、输出、输入、缓存输入 token。供应商没返回的数显示「—」。不新开页面,不改表,数字来自 `usage_ledger.metadata` 里已有的 JSON。没有新的等待动画或输入框。
This file adapts the full visual analysis in `CLAUDE_DESIGN.md` to the shipped Jyotisha application. `CLAUDE_DESIGN.md` remains the upstream reference; this file is the implementation contract.
## 校正按盘型交付(2026-09-30,在途,BUG-1115~1117)
复用范围卡布局:主行逐张写目标盘的上升与可信档位,分钟范围降为次行。完整扫描缺失时明确不能证明全窗一致;超过 61 分钟的 D9/D10 不作解读推荐。采用按钮沿用旧 owner/result/candidate 与门控,不另造候选:采用服务端头段交集/优先级/clamp 后的实际日期和分钟。已采用状态只认服务端核验的 result、候选锚点、date/time 与 segment provenance 身份,不能仅凭钟点相同点亮;只读与锁门仍禁用。来源偏移按实际采用日历日期计算,原填报资料保留,采用不等于确认。不增加 Home 状态、等待动画或新输入框。当前自动化/真机验收未闭环,不视作上线完成。折叠的「查看验证报告」同样不出分钟排名:无并列/代表分钟行、无「本命上升(该分钟)」、无候选排名表(BUG-1151)。采用后核对题让服务端采用分钟离开了已采用分钟所在的盘型时(`readopt_minute`),交付卡挂到说明这件事的回复下面,按钮为「改用 HH:MM」,下方一行 switch-note「现在按 HH:MM 排盘;改用后按 HH:MM 排盘,原填报资料保留。」;未离开时已采用的卡保持「已采用」,不因采用分钟在同一盘型内挪动而重新亮起(BUG-1173)。不新增按钮或组件。
### 新产品缺扫描与部分扫描(T4 子组,在途)
持久产品领域构造 `segment-product-v1` 身份,不以可选扫描是否存在决定新旧布局。新案缺摘要仍复用盘型卡,主句明确目标盘「上升盘型暂未知,完整窗口扫描暂不可用」,分钟范围保次行;不退回「最可能」分钟卡、不提供采用按钮。缺扫描不是出生精度不足,报告仍阻止分盘推荐,但不混写「需要更准的出生时间」。部分扫描卡、气泡、timeline 与报告均注明仅是候选盘型、不能证明全窗一致。顶层持久身份覆盖嵌套缺失,目标不匹配摘要不呈现;显式历史 null 不凭 receipt 自动升级。无新增入口、Home 状态、等待动画或输入框。真实 golden + 故障注入只证明投影/文案/持久化调用合同,不替代真 SQL、账户浏览器或完整 T4。
### 全窗一致开场卡(T3,在途)
只有持久产品目标集合每张盘完整全窗上升唯一,才在原assistant server turn下挂「盘型一致,不用校正」卡;逐行上升后写「全窗一致」,不以候选质量假称较可信,也不确认某分钟。复用原范围卡class和现消息live/reload映射,无第二composer/scroll/loading、无Home状态增长。服务端检查先于模型解析/收费/采集focus;结束步骤与下一动作改为看盘,不再暗示继续采集。扫描不可用、disjoint或超过1440分钟不能触发。历史domain null保旧开场,正常锁窗回复不被一致出口吞掉。
### 咨询来源与旧案呈现隔离(2026-09-30,在途)
咨询转校正只传已持久来源session引用;服务端核owner、consultation类型与主题allowlist决定婚恋D1+D9、事业D1+D10、其它D1+D9+D10。浏览器不传domain、不以每题focus或用户文本猜领域;新案一次固定,resume不漂移。旧nullable domain即使重计分也不生成新段卡,原native raw/signature审计仍保留;exact-session身份链不升级旧案。来源失败复用原错误区域,无新等待或入口。当前DB opening权限阻塞,浏览器生命周期未验,不称上线。
## 普通咨询:步骤栏与写作行(2026-09-27,BUG-1071~1073;2026-10-06 首轮无标题,BUG-1244)
步骤栏(思考计划)第一节标题仍是「先回答你问的这件事」;首轮它的唯一步骤是「用盘上的反差回答这个问题」,追问轮是「直接回答这句话」。2026-10-06 起首轮正文没有二级标题,思考计划里不再有「这周可以做什么」。正文没有二级标题时,各节在流式中保持未开始,结算后一起收口,不留一节一直转圈。历史回答里已经写出的标题仍对上「盘里支持这个判断的地方」「时间怎么看」。无新组件、无新动效。
写作行:首轮没有标题时只有「组织回答」那一条。进行中写「正在组织回答…」,写完是「组织回答」。历史消息里已经写出的标题仍按标题拆写作行。`shortenAnswerHeading` 里「先回答你的问题 → 先回答」的缩写保留,给历史消息用。
## 校正回答没有「重新生成」;范围句结算时出现一次(2026-09-27,BUG-1055~1058)
生时校正里助手回答下面的操作条只有「赞 / 踩 / 复制」三个按钮,不再有「重新生成回答」(BUG-1056:那条路是盲写,不校验就落库)。普通对话的操作条不变,最近一条回答仍可重新生成。`ChatMessageActions` 不传 `onRegenerate` 时不画这个按钮,其余按钮位置不变。
「范围从 A 变为 B。」「这次没有重新比较,稍后再比一次。」「候选比较这次没跑成,下一句话时会自动再试。」这几句服务端事实句只在结算那一帧随最终正文出现一次(`answer.delta` replace),之后不会再被撤回;流式过程中先出现的是模型正文,结算时模型正文可能被裁成一句或去掉数字不符的整句(BUG-606 / BUG-615 口径不变)。没有新组件、没有新动效。
## 普通咨询:数据卡与「多看一眼」(2026-09-27,TASK-consult-evidence-card)
写回答的模型只读数据卡;界面上唯一新增的是写作行在模型补取一段卡外数据时的进行中句:「正在多看一眼:D60 分盘…」(分盘写「Dxx 分盘」,其余用中文名:格局明细、八分法(Ashtakavarga)、六力(Shadbala)、太阳回归等,见 `evidenceLookupSectionLabel`)。它复用写作行(`answer-composition` 活动),不加新行、新组件或动效;第一段正文出来时照常换成写作句。每轮最多一次。补取发生在已放出正文之后时,界面上的正文不会被截断,也不会重复出现:模型若把已写的开头原样重写一遍,重复部分在服务端丢掉(记 `answer-restart-dropped` 校验步)。一步里调工具前没写完的半句过程说明不会再粘到下一步的正文开头(BUG-1059)。数据卡 v2(TASK-consult-evidence-card-v2-20260927)只改卡的内容和可多看的段,界面不变:写作行同一句式,新增名称见 VOICE.md。
## 普通咨询一遍成文(2026-09-27,BUG-1053)
本命咨询不再有单独的「写结论」阶段:算完盘后,同一个模型循环接着就写回答。时间线的变化只有两处,没有新组件、没有新动效。一是不再出现「正在整理判断依据」「正在写结论」这两个阶段事件,也不再发 `think.step`;计划里的思考行在第一段正文出现时收口(沿用 `completeLiveThink`),写作行的进行中句统一为「正在组织回答」。二是第一段正文要等模型写出第一个二级标题或满 160 字才出现(为了把工具前后的过程说明挡在正文外),开场句会整段一起到达,之后照常按帧放出。截断、停止、失败三种收口与提示不变(BUG-1051)。
## 校正消息:一屏一题干、复述变短、结算后无回执(2026-10-01,BUG-1135~1137)
- **一屏一题干。** 屏幕上只有当前有效焦点打印题干。被替换掉、没人答过的旧题(`superseded` 且无答案,或不是当前焦点的未答题)在它的消息上什么都不画——不留「题干没选项」(推翻 BUG-917 的那种画法)。GET 组装时,最后一条助手消息挂着这种死题而当前焦点还没挂上时,当前焦点占这个位置;客户端挂题时同样把死题换掉,并把正文末尾残留的同一句题干剥掉(`stripQuestionSentences`)。最新消息挂着死题、而 Case 另有活题时,不再显示「没有拿到下一个问题」。
- **复述变短。** 证据轮正文由服务器按本轮入账条目写:两件以内「记下了:2016年入学、2020年毕业。」,三件以上「记下了 N 件事。」。条目来自证据账本(按说出它的那一轮挂到该轮回复上,拒绝与被替换的不算),三件以上时在正文下面放一个默认收起的 `<details class="rectification-recorded-evidence">`,摘要「看记下的 N 件」,展开是逐条列表;两件以内不放。中文日期标签直接接事件(「2016年入学」),ISO 标签保留空格(「2016-09 入学」)。
- **结算后无步骤回执。** 校正回复写作中,步骤行照旧是等待态;结算后整块不画(包括分盘说明句),不再出现「已完成 N 步」。普通对话的结算回执不变。
## 校正开场与步骤回执(2026-09-26,BUG-1049 / BUG-1050)
开场一轮只有两段字:正文讲做法(要把出生时间缩小到更准的范围、现在先在哪段时间里找;你说几件大事和大概年月,我拿去和星盘对照),下面是问题块里的题干(六个例子 + 一个带年月的回答示例)。题干只出现一次:正文里和题干逐句相同或几乎相同的句子才会被去掉;只是开头几个字一样、内容不同的句子保留(以前按前 12 个字判定,把正文里的例子句删掉了)。
2026-10-12(BUG-1307):服务端已经量过换段时,正文在原来两句中间多一句,说明填的时间离最近会换的地方差几分钟、两边哪几件事不同。本命上升也换时,同一句先说「这里连本命上升都换」,再写其余会变的事;没有其余的事时,这一句就停在「这里连本命上升都换。」,不另起一句。没有新组件、没有新动效。句数上限仍是 3。只知道大概时段、这段时间里不换、或扫描没做成时,口径见 VOICE 同一节。选题顺序的开关默认关,开场不承诺「接下来先问这几件」。只有本命上升在换时也不说这句。
> 2026-10-01 修订:校正回复结算后不再显示步骤回执(BUG-1137),本段只适用于写作中的步骤行。
回答上方的步骤时间线:同一个完成名只出现一行(同一工具调两次、或两个工具共用一个名字,都只算一步),折叠后的「已完成 N 步」数的是读者看得见的行数。完成名与进行中句都写大白话,进行中句沿用 BUG-1047 已批准的「正在准备下一个问题…」「正在记下这件事…」「正在重新对照盘面…」;失败行写「重新对照盘面未完成」这类「在做什么 + 未完成」,不写「重新对照了盘面未完成」。没有新组件、没有新动效。
## 校正打字回答的阶段进度句(2026-09-26,BUG-1047)
生时校正里打字回答一句话,发送的同一帧,这条回复的时间线 live 行就是「收到,正在对照你的档案…」,不再先出现「正在处理…」再干等。服务端先建流、第一行就推这一句,再做意图分类;之后由真实动作推动 `turn.progress`:写入经历 →「正在记下这件事…」,引擎重算 →「正在重新对照盘面…」,经历写完或定下一问 →「正在准备下一个问题…」。阶段只往前走;工具步骤进行中那一行也显示当前阶段句,完成后仍写工具的完成名(「看了你的资料」等,BUG-1050 起为大白话);步骤之间不再落回「正在分析…」。这仍是 §9 的「流式生成中」:同一个 `InlineSpinner` live 行,没有新组件、没有新动效;结算时连同 live 行一起消失,不进正文、不进历史。选择题点选、开场与只读续轮不变。
## 读者版年运与正文投影(2026-09-25)
年主、Muntha 星座、年度上升与返照时刻分别来自对应年度的真实计算字段,不从年主对象猜补。两种口径不一致时并列注明,不合成单一结论;本地结果不等于完成外部验证。姓名缺失不显示空行,五要素字段使用中文列名。
Bhava Bala 保留完整十二宫力量表;Score / Weight 等领域表头不单独触发整表删除。内部表连同紧邻说明段移除,标题留下,避免后续正常表归错章节。PL9 审计词和长英文段过滤只用于报告正文,聊天导出与安全图盘沿用既有安全规则。不增加入口、等待动画、布局或颜色。
## 报告核对层与独立附录(2026-09-23)
个人报告保留原 Markdown 阅读、目录隔离与 memo 边界;文后独立渲染服务端分盘和事实表。分盘复用 VedicChartSvg 与 chart-grid/chart-card,不使用 float 或负 margin。事实表默认折叠、可键盘展开,横向溢出仅在表格区域滚动,缺数据不补写。Markdown 表里,只有表头含速度、Speed、度数或黄经的数值表(`.personal-report-table-numeric`)单行并按内容撑宽,放不下时在表自己的框里横滑;作用说明、建议这类整句表在页内换行。打印时所有报告表都是页宽,单元格可以换行,纸上不横滑。Sade Sati 明确缺三轮日期。09-24 reader-actions D2 撤下操作区「下载原始附录」入口;相关组件、路由和 helper 因删除权限阻塞暂留源码,不当作完成清理。不接普通下载 fallback、不增加 spinner。旧缓存不升级、不重算;没有结构化层就不显示占位盘。文案遵守 VOICE:不把可计算写成已验证。
事实表用中文列名和领域行列呈现,不把引擎路径当行名。主运、分运与小运、SAV 与 BAV、年度位置与 Saham 分为同组子表;缺失的年度行星位置明确说明,不用本命盘替代。机器溯源路径保留在数据中、不渲染。打印或保存 PDF 前自动展开此核对层全部折叠组,完成或取消后恢复原状态;打印解除表格横向滚动裁剪、重复表头,宽 BAV 表仍保留十二星座列。不新增打印入口或等待动画。
## 校正跨午夜日期与采用(2026-09-20)
复用现有时段选择卡追问一次午夜前/后;不知道或跳过保留填报当天凌晨与深夜两段,不补白昼或次日。选项用中文钟点,继续遵守题卡禁止暴露数字钟点的现有校验,不新增入口、状态或等待动画。
区间交付卡按实际日期序号显示分段;跨申报日明确「前一天 / 后一天」,候选列同时给实际日期与钟点,说明「采用后,排盘使用这一日期和时间;原填报日期保留。」图表库默认显示当前采用的有效日期;原申报日期仍独立保存,不猜补历史结果。沿用原按钮、只读门与代表分钟免责声明,不把采用写成唯一分钟确认。文案已对照 VOICE 的服务端数字来源、短句与采用不等于确认原则。
本命上升的分段展示给出完整本地日历日期与钟点,避免同一个钟点被误读为同一天;历史缺日期的结果仍显示 HH:MM,不猜补。沿用既有布局与颜色,不增加入口、状态或动画。
## 校正历史结果身份只读(2026-09-20)
历史打开、刷新与比较返回的结果若无法核实为当前算法及策略,消息区顶部复用 `rectification-pending-note`、`role="status"` 显示「按旧算法产出。当前暂不能核实结果,请先查看,暂不能选择或采用。」保留原历史内容和来源,不出现强制重算入口;关闭旧结果选项、候选采用及普通输入,输入提示为「当前结果仅供查看」。可信当前身份已知但与旧结果不同,非终止会话可显式点「重新比较」,复用既有发送链路;身份未知时无此入口,服务端再次探测失败也不能重算。此状态不等同 Case 已结束,不显示误导的结束提示或额外「再次校正」按钮。真正终止的历史仍沿用原结束操作。服务端独立核验所有结果写入,不以界面禁用代替权限。无新颜色、加载动画或 Home 状态。
## 普通对话寒暄轮(2026-09-20)
服务端标记 `responseKind: smalltalk` 的回复仅展示正文与既有头像/消息操作,不展示思考面板、活动步骤、已完成步骤或技法证据;刷新后的历史也遵循此规则。分类尚未确定、且没有任何真实 activity/thinking/timeline 时,保留消息位置,不虚构「正在分析」或步骤,不另加 spinner。咨询收到真实活动后恢复既有呈现。完成后读取服务端账户余额,寒暄预留点数原额退回,净点数不变;不增加 Home 状态。
## 1. Atmosphere & Identity
Jyotisha feels like a private reading room: warm, editorial, grounded, and quiet enough for reflective conversation. The signature is a pale parchment canvas, translucent warm-gray navigation, fine neutral hairlines, and restrained deep-brown accents. Serif display type gives astrological guidance the gravity of a considered essay rather than a generic chatbot.
## 2. Color
### Palette
@@ -212,7 +71,7 @@ Rules: neutrals stay warm; the action color is scarce; roughly ninety percent of
Tailwind `bg-primary` stays the locked alias `--color-action` (text rank). Semantic `--primary`, which new shadcn components read, is `--color-action-fill`. In dark mode those are different colors (`#d78064` and `#8f4a33`).
New controls live in `src/components/ui` and are the shadcn base-nova components. Outside that folder, lint rejects a new native `button`, `dialog`, `select`, checkbox or radio `input`, the dialog/menu/tab/listbox roles, `createPortal`, and a short list of characters used as icons. Chart drawing SVGs are exempt from the icon rule only: `personal-report/vedic-chart-svg.tsx` and `chart-page/western-wheel-svg.tsx`. Buttons in those files are still rejected. Files that already did this before K0 are listed in `frontend/eslint/ui-primitive-whitelist.json`. That list may shrink and may not grow.
New controls live in `src/components/ui` and are the shadcn base-nova components. Outside that folder, lint rejects a new native `button`, `dialog`, `select`, checkbox or radio `input`, the dialog/menu/tab/listbox roles, `createPortal`, and a short list of characters used as icons. Chart and star-map SVG drawings are exempt from the icon rule only: `personal-report/vedic-chart-svg.tsx` and `chart-page/western-wheel-svg.tsx`. Buttons in those files are still rejected. `frontend/eslint/ui-primitive-whitelist.json` may only shrink. After K7 it keeps two files that cannot move without changing locked behavior: `birth-sky/birth-sky-converge.tsx` (the sky drawing uses a native dialog stage) and `rectification-agentic-chat.tsx` (the board peek is portaled into the header, not a dialog).
### Where the action color appears
@@ -329,6 +188,36 @@ deferred, or every load flashes the other theme before hydration. Blocked storag
degrades to following the OS. The preference is per-device, not per-account, and
is read through an external store so a change in one tab reaches the others.
## 动效
动效用 Motion 预设,数据在 `src/lib/motion/presets.ts`。曲线 `--ease-out` 是 `cubic-bezier(.22, 1, .36, 1)`。回弹是 `cubic-bezier(.34, 1.56, .64, 1)`。调用处包在 `LazyMotion` 里,特性包 `domAnimation` 用到才加载,并尊重 `MotionConfig reducedMotion="user"`。减少动态时去掉位移和缩放,淡入不超过 150ms。点数变化在减少动态时立刻换数字。
只动 `transform`、`opacity` 和颜色。循环只留给正在进行的等待,见后面的等待节。侧栏收起和展开不播过渡:Sidebar state changes are immediate. 思考步骤的高度仍是同一元素上 180ms 的 `grid-template-rows`。消息行入场仍是 `chat-message-row.tsx` 里的 160ms,不改用下面 `sendMessage` 的 260ms。启程专用的时长和回弹写在后面的「新用户启程」,不改这张表。
| 预设 | 用在哪 | 时长 | 曲线 | 方向 |
| --- | --- | --- | --- | --- |
| press | 按下 | 120ms | ease-out | 无位移 |
| pageEnter | 翻页进入 | 280ms | ease-out | 纵向 16px,跟随侧栏顺序 |
| pageExit | 翻页离开 | 150ms | ease-out | 无位移 |
| selectionFollow | 当前项高亮滑动 | 260ms | ease-out | 跟布局,不另加位移 |
| tabSwitch | 页签内容 | 260ms | ease-out | 横向,跟随控件 |
| chartType | 换盘型,星体移宫 | 440ms | ease-out | 调用方量实际位移 |
| personSwitch | 换人,格线重画 | 300ms,跟上 800ms | ease-out | 无预设位移 |
| dateTurn | 翻日期 | 260ms | ease-out | 横向,跟随控件 |
| sendMessage | 从输入框送出(消息行入场仍是 160ms) | 260ms | ease-out | 从输入框 |
| sessionSwitch | 换会话 | 200ms | ease-out | 交叉淡入 |
| listReorder | 列表补位 | 260ms,离开 280ms | ease-out | 纵向 |
| dialog | 弹窗 | 220ms,离开 160ms | ease-out | 从触发处,带缩放 |
| drawer | 抽屉 | 280ms,离开 200ms | ease-out | 从边缘 |
| toast | 顶部提示 | 320ms | 回弹 | 向下 |
| numberChange | 点数数字滚动 | 360ms | ease-out | 变少向下,变多向上 |
| localeSwitch | 中英切换 | 320ms,错开 40ms | ease-out | 交叉淡入 |
| disclosure | 展开和收起 | 240ms | ease-out | 纵向 |
## Atmosphere & Identity
Jyotisha feels like a private reading room: warm, editorial, grounded, and quiet enough for reflective conversation. The signature is a pale parchment canvas, translucent warm-gray navigation, fine neutral hairlines, and restrained deep-brown accents. Serif display type gives astrological guidance the gravity of a considered essay rather than a generic chatbot.
## 3. Typography
### Font stacks
@@ -919,30 +808,6 @@ admin page — so nothing here inherits the tokens above.
card that it is anonymous, summary-only (no per-row view or export) and kept
for 180 days.
## 6. Motion & Interaction
`--ease-out` is `cubic-bezier(.22, 1, .36, 1)` and is the only easing curve in the
system.
| Type | Duration | Easing | Usage |
|---|---:|---|---|
| Micro | 120ms | ease-out / `--ease-out` | Button, row, popover and tooltip feedback |
| Standard | 180ms | `--ease-out` | Dialogs, onboarding cards, auth steps |
| Message | 160ms | `--ease-out` | A message row entering the transcript |
| Thinking collapse | 180ms | `--ease-out` | The step timeline opening or closing in place (`grid-template-rows` 0fr ↔ 1fr) |
| Timeline label swap | 120ms | ease-out | “正在分析” → “已完成 N 步”, and a live row's label changing |
| Text reveal | per-frame release, not a duration | none | Streamed answer and thinking text; see Streaming states |
| Sheet | 280ms | `--ease-out` | Rectification sheet rising from the bottom |
| Spatial | Instant | None | Sidebar state changes and mobile drawer |
Looping animation is covered separately in section 9.
Only `transform`, `opacity`, and color/filter transitions animate. Reduced-motion disables non-essential animation. Motion communicates state; decorative looping is limited to an active loading state.
K0 stores the later site-motion rows as data in `src/lib/motion/presets.ts` (duration, curve, direction) and wraps `LazyMotion` plus `MotionConfig reducedMotion="user"` in `src/lib/motion/provider.tsx`. No page mounts that provider yet, and the presets are not wired. Sidebar and other spatial changes stay instant. The one live entrance is still the message row at 160ms.
启程专用的时长和回弹曲线写在文首「新用户启程」,不改上面这张表。
## 7. Depth & Surface
Strategy: warm tonal shifts, restrained translucency, and fine hairlines. Most depth comes from the page floor, main canvas, warm cards, and selected white glass. Standard UI uses no drop shadow. Modal, popover, floating-control and raised-sheet depth use one restrained two-stage token: `--shadow-elevated: 0 1px 2px rgba(29, 29, 31, .07), 0 12px 28px -16px rgba(29, 29, 31, .18)`. A second, quieter token, `--shadow-soft: 0 1px 2px rgba(29, 29, 31, .06), 0 8px 20px -18px rgba(29, 29, 31, .2)`, belongs to surfaces inside the reading column — the conversational narrative card, the composer field within a session and the auth mode tab. Floating controls and overlays (jump to latest, popovers, sheets) always take `--shadow-elevated`. Interactive rings use `0 0 0 1px` or the focus ring; no generic card shadows, no utility-class shadows, no atmospheric gradients. When transparency is reduced, frosted surfaces fall back to opaque warm neutrals.
@@ -1399,3 +1264,146 @@ Yogini 还在取时,右栏用和 Chara 相同的轨道等待(`OrbitWaiting`
- **确认块:** 原地展开,不再叠一层弹窗。浅危险色底(`--color-danger-muted`)、危险色 35% 描边;说明、删除清单、保留说明、联系邮箱(次级墨色)、「输入「注销」确认」输入框,底部「取消」+ 实心危险「确认注销」(`.danger-primary`)。没输对字之前确认按钮禁用。
- **冷静期门:** 登录后若有待执行的注销,`(app)` 布局渲染全屏 `role="alertdialog"`(`.account-deletion-gate`,z-index 90,`--color-canvas-soft` 底),中间 440px 卡片:宋体标题「账号注销中」、日期句、「退出登录」「撤销注销」两个按钮居中。不做入场动画,不出现 spinner;撤销后整页刷新。
- 代码:`components/account-deletion-section.tsx`、`components/account-deletion-gate.tsx`;`page.tsx` 只在 `renderGeneral()` 里多挂一个组件,`Home()` 的 `useState` / `useRef` 数不变。
## 外壳(2026-10-24)
桌面侧栏在可拖分栏里,宽 200–360px,默认 288。拖到最左收成 64px 窄轨;收起不把 64 写成宽度。宽度记在本机 `sidebar_width`。手机用左侧抽屉,宽 `min(86vw, 320px)`。当前页高亮用已有的 `layoutId` 滑动。翻页用已有的页面进入 280ms、离开 150ms,方向跟着侧栏顺序。顶栏点数是描边按钮:硬币图标加数字,44px;「点」只在可读名称里。数字变少时往下滚,变多时往上滚,360ms;系统要求减少动态时立刻换数字。启程清单胶囊仍是 32px,仍在点数左边。账户按钮只显示头像和名字。设置仍是三个分区。会话只在收藏组内、或最近一组内拖动排序;收藏整组仍在最前。翻页、高亮滑动、拖宽和拖动排序用的库在页面出来后再载入,不进首屏。
## 对话页(2026-10-25)
输入框仍是一个框。框底左边是模型菜单,只显示模型名;价格「N 点」只在菜单里。菜单用外壳已有的延后载入,不进首屏;库还没到时按钮上仍是模型名。右边是圆形发送,和框的底边对齐。回答下面赞、踩、复制、重新生成是 26px 的图标按钮,各有一句提示。不满意原因和兑换窗用组件库对话框,句子不改。追问是描边小按钮,仍按启程进场依次浮出(260ms,间隔 64ms),不另写一套时长。
桌面顶栏、点数右边,是打开或收起本命盘的图标按钮,提示「打开本命盘」或「收起本命盘」。面板在可拖分栏里,宽 280–560px,默认 360。拖到最窄可以收起。开着和宽度记在本机 `chat_chart_panel_open`、`chat_chart_panel_width`。手机(视口还没定,或小于 768)不显示这个按钮,也不因为藏起来就把「开着」写成关掉。面板里是当前人物的本命盘,复用星盘页的绘制,不写骨架、不写「正在加载」。可拖分栏和星盘绘制都在第一次打开后面才载入,不进首屏。
## 新用户启程(2026-10-25)
首页顶栏右侧、点数按钮左边,是一枚胶囊「启程 n/N」(`.onboarding-checklist-trigger`)。它沿用顶栏按钮的 32px 高度,圆角 999px,字和图标间距 6px。左边是 18×18 的进度环,描边按已完成的项画满。视口还没分出手机或桌面时,这枚胶囊不可打开。手机(侧栏视口小于 768)从底部抽屉拉出,带现有的下拉把手。平板和桌面用现有弹出层,靠右对齐,宽 `min(22rem, calc(100vw - 2rem))`,行是 `1.25rem minmax(0, 1fr) auto`,句子 `overflow-wrap: anywhere`。已完成的行用对勾,没有「去做」。底部是「先不看了」。
七项在这一次使用里刚满时,抽屉或弹出层再打开一次,进度环处同时迸一次星点(1000ms,只一次)。标题「都走过一遍了」,正文「以后有新东西,我会在用到的时候再告诉你。」关掉后胶囊消失。进站时已经满的账户,胶囊、这一屏和星点都不出现。
一次性提示用现有弹出层,宽 `min(18rem, calc(100vw - 2rem))`,不抢焦点。侧栏两项从锚点右侧伸出,其余从上方伸出,间距 8px。按钮是「知道了」。同一时刻最多一条。天空过场、兑换窗、任何弹窗或抽屉、启程清单正打开、回答还在生成时,提示不出;挡着的那一条等条件过去再出。侧栏「星盘」、手机菜单和星盘四个页签上的小圆点是 8px,贴在右上角 4px,不是字符。
启程专用动效只在元素第一次出现时播放。重绘、来回切换不重播。除回答流式中的呼吸点外,没有无限循环。只动 `opacity`、`transform`、颜色、`clip-path`,不触发布局。Motion 用到才加载,不进首屏。系统开启「减少动态效果」时,星点和圆点闪烁去掉,其余只淡入,没有位移、缩放和擦除。
| 类别 | 用在哪 | 时长 | 曲线 |
| --- | --- | --- | --- |
| 按下反馈 | 按钮、问题卡、选项、关系标签被按下时轻微缩小 | 120ms | `--ease-out` |
| 进场 | 建档消息与卡片、起步问题、追问胶囊、档案卡、目录行;依次上浮,间隔 64ms(落在 50–80ms) | 260ms | `--ease-out` |
| 弹出 | 一次性提示、「这一次不扣点」、勾 | 380ms | 回弹 `cubic-bezier(.34, 1.56, .64, 1)` |
| 跟手 | 称呼从输入框飞成气泡;起步问题飞成气泡 | 380ms | `--ease-out` |
| 勾飞进进度环 | 清单每完成一项:从操作位置沿弧线飞向胶囊,进度环走一格,胶囊轻跳一下 | 700ms | 弧线 |
| 画出来 | 建档进度条;星盘格线、西洋盘圆与分隔线、大运条 | 640–860ms(落在 520–900ms) | `--ease-out` |
| 场景转换 | 出生地→天空:从点击处圆形展开;天空→首页:淡出放大,首页同时上浮 | 680ms(落在 650–700ms) | `--ease-out` |
| 合盘 | 两个头像从两侧靠拢,泛起一圈光,再展开关系画像 | 1.2s | `--ease-out` |
| 庆祝 | 七项全满时,进度环处迸一次星点 | 1000ms,只一次 | `--ease-out` |
侧栏「星盘」和星盘页签上的小圆点,第一次出现时闪两下(800ms,有限次,不是循环)。减少动态效果时不闪。弹出层和抽屉本身仍走组件库已有的进出,不另加一套时长。
## 婚恋合盘报告(2026-10-19)
选人页不重复标题和副标题,页名在顶栏(2026-10-25)。两个人的选择框左右并排,中间一个叉;窄屏改成上下排,叉仍在正中。各自是下拉,最多二十人。资料不全的人不能选;已经在另一边的人不能再选。下面是「新增星盘」,点开一张卡片:称呼、性别(男 / 女)、出生日期、出生时间、出生地,右下角「取消」和「保存并选中」。满二十人时这一行换成去星盘档案删人。「开始合盘」在桌面和手机上都居中,按钮上不写价格。点了先问「会用掉 N 点,余额 M 点。」,按钮是「取消 / 开始」。余额不够时说「需要 N 点,余额只有 M 点。」,主按钮换成「去补点数」。N 从计价表读,M 从账户读。计价读不到时不弹确认,只说「计费配置不可用」。这两人已经有写好的合盘、且没有一份正在生成时,按钮下面再写已经合过几次、再生成会另扣多少点。之前的合盘只列人名和日期。生成中是轨道和四步(算两张完整命盘、整理两人之间的证据、写报告、检查报告),句子是「正在写 甲 × 乙 的合盘报告…」。报告页回到「婚恋合盘」,标题下是日期和「已保存,再次打开不扣点」,语言是「中文 / English」。第一次打开时两个头像仍从两侧靠拢;减少动态效果时只淡入。九节编号,分析逻辑默认收起。英文报告同样收起分析逻辑(Analysis,依据行是 Chart 和 So),时间线每段标题写出起止年月;第一段从本月起时,起点写「现在」或 Now。英文准备时同一套轨道,写「正在写英文版…」,揭幕后整份替换。报告末尾「对哪一段想再问清楚?」,下面是八枚胶囊(`.synastry-report-ask`),不含共同练习。点一枚只新建对话并预填问题,不自动发送。对话里人物选择器锁住,显示「甲 × 乙」。合盘删除后输入框占位是「这份合盘已删除」,发送停用。报告在整宽的外层滚动,两侧空白也能滚;中间一栏只负责居中排版。合盘报告单独走 `/synastry/报告`。开始合盘、之前的合盘和报告中心的合盘卡片都进这个地址,侧栏仍亮「婚恋合盘」。旧的 `/reports/报告` 会换成新地址,并保留语言。
## 生时校正从对话进入、做完回到原对话(2026-10-07,BUG-1247~1251,在途)
首页不再有「生时校正」pill,`.starter-entries` 只剩今日星语一枚(`.starter-entry-hint` 那句「生时校正暂时只支持本人。」随之删除)。入口在对话里:
- **提议卡**(`.rectification-offer-card`):挂在提议了校正的那条回答下面,与追问 chip 同一缩进(`--assistant-content-inset`),浅底细边圆角卡,一句说明(大约几题、几点、答第一题时才扣)+ 一个实心按钮「开始核对出生时间」。校正已开始时同一张卡改成「出生时间核对已经开始。」+ 描边按钮「继续核对出生时间」;回来之后卡片消失。对话进行中(loading / 取消中)按钮不可点。
- **回到刚才的对话**:从对话进来的校正,在校正面输入框上方一行描边小按钮(`.rectification-return-row`);没有来源对话的校正(历史里打开的、之前开的)不出这一行。采用后核对结束、或 case 结束时,在这个视图里做过操作的校正 2.5 秒后自动回去;从历史打开一个已结束的校正不会自动跳走。
- **分隔线**(`.rectification-return-divider`):回到原对话后追加一行居中小字「出生时间改为 HH:MM(生时校正)」或「出生时间没有改」,不是一条消息、没有操作行;改了时间时下面一个描边按钮「按新时间重新看:「原问题」」,点了才发送(普通一轮,照常计费)。
- **按校正前的时间**(`.message-before-change`):改了时间后,分隔线之前的每条回答上方一行灰色小字;原文不动。
不新增等待动画;打开校正沿用既有一次揭幕。
**选项和按钮换成组件库(2026-10-25,K6)**:选项卡片仍是原来的圆角卡片,点中后浅底描边,读屏仍读题干和选项原文,不另写「已选择」。确定程度仍是三个圆点。大概范围仍是一排标签。盘面关闭仍是一枚叉,探头仍挂在顶栏原来的位置,不是弹窗。其余确认、重新打开和采用用组件库按钮,样子沿用原来的实心、描边和文字按钮。题目出现时,若动效已经载入,只淡入;减少动态效果时同样只淡入,没有位移,也没有循环。题目、计分、扣点和保存没有改。明确要求校正仍直接开始,没做完的接着做。
**明确要求则直接打开(2026-10-12,BUG-1306)**:用户这一轮明说要做校正时,不再先出卡再点。回答只留一句过渡,界面沿用既有一次揭幕直接打开校正面。没有新的按钮、输入框、等待动画或 Home 状态。推得不准时的主动提议仍用 `.rectification-offer-card`。做不了时只显示一句话,不静默,也不在对话里假装校正。开案还没成功时,同一张提议卡仍是普通提议(不说「已经开始」,按钮仍是「开始核对出生时间」)。开案成功并绑定来源后才换成已开始的文案。打开失败时按钮仍可点。
## 用量页分段计时(2026-10-05,在途)
`/admin/usage` 在原表格上多一列「分段」。格子里四行:分类耗时、第 0 步、第 1 步、第 1 步开始到第一个正文字符。每步带推理、输出、输入、缓存输入 token。供应商没返回的数显示「—」。不新开页面,不改表,数字来自 `usage_ledger.metadata` 里已有的 JSON。没有新的等待动画或输入框。
## 校正按盘型交付(2026-09-30,在途,BUG-1115~1117)
复用范围卡布局:主行逐张写目标盘的上升与可信档位,分钟范围降为次行。完整扫描缺失时明确不能证明全窗一致;超过 61 分钟的 D9/D10 不作解读推荐。采用按钮沿用旧 owner/result/candidate 与门控,不另造候选:采用服务端头段交集/优先级/clamp 后的实际日期和分钟。已采用状态只认服务端核验的 result、候选锚点、date/time 与 segment provenance 身份,不能仅凭钟点相同点亮;只读与锁门仍禁用。来源偏移按实际采用日历日期计算,原填报资料保留,采用不等于确认。不增加 Home 状态、等待动画或新输入框。当前自动化/真机验收未闭环,不视作上线完成。折叠的「查看验证报告」同样不出分钟排名:无并列/代表分钟行、无「本命上升(该分钟)」、无候选排名表(BUG-1151)。采用后核对题让服务端采用分钟离开了已采用分钟所在的盘型时(`readopt_minute`),交付卡挂到说明这件事的回复下面,按钮为「改用 HH:MM」,下方一行 switch-note「现在按 HH:MM 排盘;改用后按 HH:MM 排盘,原填报资料保留。」;未离开时已采用的卡保持「已采用」,不因采用分钟在同一盘型内挪动而重新亮起(BUG-1173)。不新增按钮或组件。
### 新产品缺扫描与部分扫描(T4 子组,在途)
持久产品领域构造 `segment-product-v1` 身份,不以可选扫描是否存在决定新旧布局。新案缺摘要仍复用盘型卡,主句明确目标盘「上升盘型暂未知,完整窗口扫描暂不可用」,分钟范围保次行;不退回「最可能」分钟卡、不提供采用按钮。缺扫描不是出生精度不足,报告仍阻止分盘推荐,但不混写「需要更准的出生时间」。部分扫描卡、气泡、timeline 与报告均注明仅是候选盘型、不能证明全窗一致。顶层持久身份覆盖嵌套缺失,目标不匹配摘要不呈现;显式历史 null 不凭 receipt 自动升级。无新增入口、Home 状态、等待动画或输入框。真实 golden + 故障注入只证明投影/文案/持久化调用合同,不替代真 SQL、账户浏览器或完整 T4。
### 全窗一致开场卡(T3,在途)
只有持久产品目标集合每张盘完整全窗上升唯一,才在原assistant server turn下挂「盘型一致,不用校正」卡;逐行上升后写「全窗一致」,不以候选质量假称较可信,也不确认某分钟。复用原范围卡class和现消息live/reload映射,无第二composer/scroll/loading、无Home状态增长。服务端检查先于模型解析/收费/采集focus;结束步骤与下一动作改为看盘,不再暗示继续采集。扫描不可用、disjoint或超过1440分钟不能触发。历史domain null保旧开场,正常锁窗回复不被一致出口吞掉。
### 咨询来源与旧案呈现隔离(2026-09-30,在途)
咨询转校正只传已持久来源session引用;服务端核owner、consultation类型与主题allowlist决定婚恋D1+D9、事业D1+D10、其它D1+D9+D10。浏览器不传domain、不以每题focus或用户文本猜领域;新案一次固定,resume不漂移。旧nullable domain即使重计分也不生成新段卡,原native raw/signature审计仍保留;exact-session身份链不升级旧案。来源失败复用原错误区域,无新等待或入口。当前DB opening权限阻塞,浏览器生命周期未验,不称上线。
## 普通咨询:步骤栏与写作行(2026-09-27,BUG-1071~1073;2026-10-06 首轮无标题,BUG-1244)
步骤栏(思考计划)第一节标题仍是「先回答你问的这件事」;首轮它的唯一步骤是「用盘上的反差回答这个问题」,追问轮是「直接回答这句话」。2026-10-06 起首轮正文没有二级标题,思考计划里不再有「这周可以做什么」。正文没有二级标题时,各节在流式中保持未开始,结算后一起收口,不留一节一直转圈。历史回答里已经写出的标题仍对上「盘里支持这个判断的地方」「时间怎么看」。无新组件、无新动效。
写作行:首轮没有标题时只有「组织回答」那一条。进行中写「正在组织回答…」,写完是「组织回答」。历史消息里已经写出的标题仍按标题拆写作行。`shortenAnswerHeading` 里「先回答你的问题 → 先回答」的缩写保留,给历史消息用。
## 校正回答没有「重新生成」;范围句结算时出现一次(2026-09-27,BUG-1055~1058)
生时校正里助手回答下面的操作条只有「赞 / 踩 / 复制」三个按钮,不再有「重新生成回答」(BUG-1056:那条路是盲写,不校验就落库)。普通对话的操作条不变,最近一条回答仍可重新生成。`ChatMessageActions` 不传 `onRegenerate` 时不画这个按钮,其余按钮位置不变。
「范围从 A 变为 B。」「这次没有重新比较,稍后再比一次。」「候选比较这次没跑成,下一句话时会自动再试。」这几句服务端事实句只在结算那一帧随最终正文出现一次(`answer.delta` replace),之后不会再被撤回;流式过程中先出现的是模型正文,结算时模型正文可能被裁成一句或去掉数字不符的整句(BUG-606 / BUG-615 口径不变)。没有新组件、没有新动效。
## 普通咨询:数据卡与「多看一眼」(2026-09-27,TASK-consult-evidence-card)
写回答的模型只读数据卡;界面上唯一新增的是写作行在模型补取一段卡外数据时的进行中句:「正在多看一眼:D60 分盘…」(分盘写「Dxx 分盘」,其余用中文名:格局明细、八分法(Ashtakavarga)、六力(Shadbala)、太阳回归等,见 `evidenceLookupSectionLabel`)。它复用写作行(`answer-composition` 活动),不加新行、新组件或动效;第一段正文出来时照常换成写作句。每轮最多一次。补取发生在已放出正文之后时,界面上的正文不会被截断,也不会重复出现:模型若把已写的开头原样重写一遍,重复部分在服务端丢掉(记 `answer-restart-dropped` 校验步)。一步里调工具前没写完的半句过程说明不会再粘到下一步的正文开头(BUG-1059)。数据卡 v2(TASK-consult-evidence-card-v2-20260927)只改卡的内容和可多看的段,界面不变:写作行同一句式,新增名称见 VOICE.md。
## 普通咨询一遍成文(2026-09-27,BUG-1053)
本命咨询不再有单独的「写结论」阶段:算完盘后,同一个模型循环接着就写回答。时间线的变化只有两处,没有新组件、没有新动效。一是不再出现「正在整理判断依据」「正在写结论」这两个阶段事件,也不再发 `think.step`;计划里的思考行在第一段正文出现时收口(沿用 `completeLiveThink`),写作行的进行中句统一为「正在组织回答」。二是第一段正文要等模型写出第一个二级标题或满 160 字才出现(为了把工具前后的过程说明挡在正文外),开场句会整段一起到达,之后照常按帧放出。截断、停止、失败三种收口与提示不变(BUG-1051)。
## 校正消息:一屏一题干、复述变短、结算后无回执(2026-10-01,BUG-1135~1137)
- **一屏一题干。** 屏幕上只有当前有效焦点打印题干。被替换掉、没人答过的旧题(`superseded` 且无答案,或不是当前焦点的未答题)在它的消息上什么都不画——不留「题干没选项」(推翻 BUG-917 的那种画法)。GET 组装时,最后一条助手消息挂着这种死题而当前焦点还没挂上时,当前焦点占这个位置;客户端挂题时同样把死题换掉,并把正文末尾残留的同一句题干剥掉(`stripQuestionSentences`)。最新消息挂着死题、而 Case 另有活题时,不再显示「没有拿到下一个问题」。
- **复述变短。** 证据轮正文由服务器按本轮入账条目写:两件以内「记下了:2016年入学、2020年毕业。」,三件以上「记下了 N 件事。」。条目来自证据账本(按说出它的那一轮挂到该轮回复上,拒绝与被替换的不算),三件以上时在正文下面放一个默认收起的 `<details class="rectification-recorded-evidence">`,摘要「看记下的 N 件」,展开是逐条列表;两件以内不放。中文日期标签直接接事件(「2016年入学」),ISO 标签保留空格(「2016-09 入学」)。
- **结算后无步骤回执。** 校正回复写作中,步骤行照旧是等待态;结算后整块不画(包括分盘说明句),不再出现「已完成 N 步」。普通对话的结算回执不变。
## 校正开场与步骤回执(2026-09-26,BUG-1049 / BUG-1050)
开场一轮只有两段字:正文讲做法(要把出生时间缩小到更准的范围、现在先在哪段时间里找;你说几件大事和大概年月,我拿去和星盘对照),下面是问题块里的题干(六个例子 + 一个带年月的回答示例)。题干只出现一次:正文里和题干逐句相同或几乎相同的句子才会被去掉;只是开头几个字一样、内容不同的句子保留(以前按前 12 个字判定,把正文里的例子句删掉了)。
2026-10-12(BUG-1307):服务端已经量过换段时,正文在原来两句中间多一句,说明填的时间离最近会换的地方差几分钟、两边哪几件事不同。本命上升也换时,同一句先说「这里连本命上升都换」,再写其余会变的事;没有其余的事时,这一句就停在「这里连本命上升都换。」,不另起一句。没有新组件、没有新动效。句数上限仍是 3。只知道大概时段、这段时间里不换、或扫描没做成时,口径见 VOICE 同一节。选题顺序的开关默认关,开场不承诺「接下来先问这几件」。只有本命上升在换时也不说这句。
> 2026-10-01 修订:校正回复结算后不再显示步骤回执(BUG-1137),本段只适用于写作中的步骤行。
回答上方的步骤时间线:同一个完成名只出现一行(同一工具调两次、或两个工具共用一个名字,都只算一步),折叠后的「已完成 N 步」数的是读者看得见的行数。完成名与进行中句都写大白话,进行中句沿用 BUG-1047 已批准的「正在准备下一个问题…」「正在记下这件事…」「正在重新对照盘面…」;失败行写「重新对照盘面未完成」这类「在做什么 + 未完成」,不写「重新对照了盘面未完成」。没有新组件、没有新动效。
## 校正打字回答的阶段进度句(2026-09-26,BUG-1047)
生时校正里打字回答一句话,发送的同一帧,这条回复的时间线 live 行就是「收到,正在对照你的档案…」,不再先出现「正在处理…」再干等。服务端先建流、第一行就推这一句,再做意图分类;之后由真实动作推动 `turn.progress`:写入经历 →「正在记下这件事…」,引擎重算 →「正在重新对照盘面…」,经历写完或定下一问 →「正在准备下一个问题…」。阶段只往前走;工具步骤进行中那一行也显示当前阶段句,完成后仍写工具的完成名(「看了你的资料」等,BUG-1050 起为大白话);步骤之间不再落回「正在分析…」。这仍是 §9 的「流式生成中」:同一个 `InlineSpinner` live 行,没有新组件、没有新动效;结算时连同 live 行一起消失,不进正文、不进历史。选择题点选、开场与只读续轮不变。
## 读者版年运与正文投影(2026-09-25)
年主、Muntha 星座、年度上升与返照时刻分别来自对应年度的真实计算字段,不从年主对象猜补。两种口径不一致时并列注明,不合成单一结论;本地结果不等于完成外部验证。姓名缺失不显示空行,五要素字段使用中文列名。
Bhava Bala 保留完整十二宫力量表;Score / Weight 等领域表头不单独触发整表删除。内部表连同紧邻说明段移除,标题留下,避免后续正常表归错章节。PL9 审计词和长英文段过滤只用于报告正文,聊天导出与安全图盘沿用既有安全规则。不增加入口、等待动画、布局或颜色。
## 报告核对层与独立附录(2026-09-23)
个人报告保留原 Markdown 阅读、目录隔离与 memo 边界;文后独立渲染服务端分盘和事实表。分盘复用 VedicChartSvg 与 chart-grid/chart-card,不使用 float 或负 margin。事实表默认折叠、可键盘展开,横向溢出仅在表格区域滚动,缺数据不补写。Markdown 表里,只有表头含速度、Speed、度数或黄经的数值表(`.personal-report-table-numeric`)单行并按内容撑宽,放不下时在表自己的框里横滑;作用说明、建议这类整句表在页内换行。打印时所有报告表都是页宽,单元格可以换行,纸上不横滑。Sade Sati 明确缺三轮日期。09-24 reader-actions D2 撤下操作区「下载原始附录」入口;相关组件、路由和 helper 因删除权限阻塞暂留源码,不当作完成清理。不接普通下载 fallback、不增加 spinner。旧缓存不升级、不重算;没有结构化层就不显示占位盘。文案遵守 VOICE:不把可计算写成已验证。
事实表用中文列名和领域行列呈现,不把引擎路径当行名。主运、分运与小运、SAV 与 BAV、年度位置与 Saham 分为同组子表;缺失的年度行星位置明确说明,不用本命盘替代。机器溯源路径保留在数据中、不渲染。打印或保存 PDF 前自动展开此核对层全部折叠组,完成或取消后恢复原状态;打印解除表格横向滚动裁剪、重复表头,宽 BAV 表仍保留十二星座列。不新增打印入口或等待动画。
## 校正跨午夜日期与采用(2026-09-20)
复用现有时段选择卡追问一次午夜前/后;不知道或跳过保留填报当天凌晨与深夜两段,不补白昼或次日。选项用中文钟点,继续遵守题卡禁止暴露数字钟点的现有校验,不新增入口、状态或等待动画。
区间交付卡按实际日期序号显示分段;跨申报日明确「前一天 / 后一天」,候选列同时给实际日期与钟点,说明「采用后,排盘使用这一日期和时间;原填报日期保留。」图表库默认显示当前采用的有效日期;原申报日期仍独立保存,不猜补历史结果。沿用原按钮、只读门与代表分钟免责声明,不把采用写成唯一分钟确认。文案已对照 VOICE 的服务端数字来源、短句与采用不等于确认原则。
本命上升的分段展示给出完整本地日历日期与钟点,避免同一个钟点被误读为同一天;历史缺日期的结果仍显示 HH:MM,不猜补。沿用既有布局与颜色,不增加入口、状态或动画。
## 校正历史结果身份只读(2026-09-20)
历史打开、刷新与比较返回的结果若无法核实为当前算法及策略,消息区顶部复用 `rectification-pending-note`、`role="status"` 显示「按旧算法产出。当前暂不能核实结果,请先查看,暂不能选择或采用。」保留原历史内容和来源,不出现强制重算入口;关闭旧结果选项、候选采用及普通输入,输入提示为「当前结果仅供查看」。可信当前身份已知但与旧结果不同,非终止会话可显式点「重新比较」,复用既有发送链路;身份未知时无此入口,服务端再次探测失败也不能重算。此状态不等同 Case 已结束,不显示误导的结束提示或额外「再次校正」按钮。真正终止的历史仍沿用原结束操作。服务端独立核验所有结果写入,不以界面禁用代替权限。无新颜色、加载动画或 Home 状态。
## 普通对话寒暄轮(2026-09-20)
服务端标记 `responseKind: smalltalk` 的回复仅展示正文与既有头像/消息操作,不展示思考面板、活动步骤、已完成步骤或技法证据;刷新后的历史也遵循此规则。分类尚未确定、且没有任何真实 activity/thinking/timeline 时,保留消息位置,不虚构「正在分析」或步骤,不另加 spinner。咨询收到真实活动后恢复既有呈现。完成后读取服务端账户余额,寒暄预留点数原额退回,净点数不变;不增加 Home 状态。
+1 -1
View File
@@ -277,7 +277,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
- 列表已完成行只留「查看报告」。阅读页操作条只留「返回报告中心」、「中文 / English」切换(只在有英文版的报告上出现)与带字的「导出」按钮(读屏名「导出报告」,2026-09-29 起不再是只有图标的按钮)。不再显示「下载原始附录」或「打印」入口文案;浏览器 Ctrl+P 排版仍保留。
- 中文导出里的分盘宫位列表写「第 1 宫 · 白羊:……」,星座名用星盘页那组(白羊、金牛、双子、巨蟹、狮子、处女、天秤、天蝎、射手、摩羯、水瓶、双鱼),不带「座」,不写 Aries 这类英文名。英文列表仍写 `House 1 · Aries: …`。
- 导出面板(2026-09-30 重设计):标题「导出报告」;语言「中文 / English」下一句「拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。」;范围「整篇报告」(副行如「7 章正文 + 22 张分盘 + 8 张核对表」)/「自选章节」(副行「只带走需要的部分」);分组名「正文 / 分盘 / 核对表」带「n/N」;按钮「导出整篇 · 约 X KB」「导出 n 项 · 约 X KB」,一项没选时「先勾选要导出的部分」。成功提示「已导出」。没有英文版时弹窗内不写字,打开时顶部浮层提示一次:「这份报告生成于英文版上线前,重新生成即可获得中英两版。」或「这份报告的英文版没有生成成功,只能导出中文版。」
- 导出面板:标题「导出报告」。语言是「中文 / English」,不再写「拿去问 ChatGPT、Claude 等 AI」那句说明。范围「整篇报告」(副行是章、分盘、核对表的数量)/「自选章节」(副行「只带走需要的部分」)。分组名「正文 / 分盘 / 核对表」带「n/N」。按钮是「导出整篇」或「导出 N 项」,不写文件大小;一项没选时「先勾选要导出的部分」。成功提示「已导出」。没有英文版时抽屉里不写字,打开时顶部浮层提示一次:「这份报告生成于英文版上线前,重新生成即可获得中英两版。」或「这份报告的英文版没有生成成功,只能导出中文版。」
- 删除确认:「确定删除这份报告?」;按钮:「删除 / 取消」;请求中:「删除中…」;失败:「报告没能删除,请稍后重试。」失败保留原行。
- 日期中文全写;深度「简版 / 标准深度 / 深度 / 研究级」;主题复用现有中文映射,未知 id 原样回退,不造含义。
@@ -6,6 +6,11 @@ import { readFileSync } from "node:fs";
* - src/components/personal-report/vedic-chart-svg.tsx (星盘)
* - src/components/chart-page/western-wheel-svg.tsx (星图)
* Buttons, menus, roles, and createPortal in those files are still reported.
*
* ui-primitive-whitelist.json may only shrink. After K7 it keeps two files
* whose behavior is locked by source tests:
* - birth-sky-converge.tsx: the sky drawing stage (native dialog.showModal).
* - rectification-agentic-chat.tsx: the board peek portaled into the header slot.
*/
const ICON_GLYPH = /[☰↑↓←→×✕✖✓✔⋯▾▸▴‹›+]/u;
@@ -1,11 +1,4 @@
[
"src/app/error.tsx",
"src/components/ayanamsa-preference-field.tsx",
"src/components/birth-place-picker.tsx",
"src/components/birth-sky/birth-sky-converge.tsx",
"src/components/consultation-run-timeline.tsx",
"src/components/home-bootstrap-error.tsx",
"src/components/home-onboarding-shell.tsx",
"src/components/personal-report/personal-report-markdown-view.tsx",
"src/components/rectification-agentic-chat.tsx"
]
+4 -2
View File
@@ -10,6 +10,8 @@ import { useEffect } from "react";
import { TriangleAlert } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
const canvas = "var(--bp-canvas)";
const ink = "var(--bp-ink)";
const inkSecondary = "var(--bp-ink-soft)";
@@ -115,7 +117,7 @@ export default function RootError({
marginTop: "8px",
}}
>
<button
<Button
className="root-error-action"
onClick={() => reset()}
style={{
@@ -128,7 +130,7 @@ export default function RootError({
type="button"
>
重试
</button>
</Button>
<Link
className="root-error-action"
href="/"
+39 -277
View File
@@ -17,7 +17,6 @@
.personal-report-fact-tables tr { break-inside: avoid; }
}
@theme inline {
--color-background: var(--color-canvas);
--color-foreground: var(--color-ink);
@@ -462,7 +461,6 @@
--color-element-water: #2a4448;
}
* { box-sizing: border-box; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.phrase-nowrap { white-space: nowrap; }
@@ -490,9 +488,9 @@ button:disabled:where(:not([data-slot="button"])) { cursor: default; opacity: .4
.credit-balance .credit-roll { overflow: hidden; display: inline-grid; height: 1.2em; }
.credit-balance .credit-roll-value { display: inline-flex; align-items: center; margin: 0; color: inherit; font-size: inherit; line-height: 1; }
.credit-balance .credit-icon { width: 16px; height: 16px; flex: 0 0 auto; }
.welcome-mark { margin-bottom: 20px; }
.welcome h1, .auth-panel h1 { margin: 0; font-size: clamp(30px, 4vw, 42px); font-weight: 600; letter-spacing: -.035em; line-height: 1.1; }
.welcome-copy { max-width: 520px; margin: var(--space-3) 0 28px; color: var(--color-ink-secondary); font-size: var(--type-body-md); line-height: 1.65; }
.onboarding-card-reveal { min-width: 0; max-width: 100%; animation: onboarding-card-enter 180ms var(--ease-out) both; }
.onboarding-card-reveal-inner { min-width: 0; max-width: 100%; min-height: 0; }
.onboarding-card-heading { display: grid; gap: 4px; padding-bottom: 2px; }
@@ -640,7 +638,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
background: transparent;
color: inherit;
}
.agent-activity-status__row { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); min-width: 0; width: 100%; }
.agent-activity-status__live { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.agent-activity-status__elapsed,
.agent-thinking-elapsed {
@@ -648,11 +646,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
color: var(--color-ink-tertiary);
font-variant-numeric: tabular-nums;
}
.agent-activity-status__trail {
margin: 0;
padding-left: 28px;
color: var(--color-ink-tertiary);
}
.agent-activity-status__text {
color: var(--color-ink-tertiary);
background: linear-gradient(
@@ -726,10 +720,8 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.section-title { display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); }
.code-form { display: grid; grid-template-columns: 120px 120px 200px minmax(180px, 1fr) auto; align-items: end; gap: var(--space-3); margin-top: var(--space-5); }
.code-form label { display: grid; gap: var(--space-2); }
.generated-list { display: grid; gap: 8px; margin-top: 16px; }
.generated-list code { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-已过期, .status-已兑换 { color: var(--color-ink-secondary); }
.empty-cell { color: var(--color-ink-secondary); text-align: center !important; }
.sidebar-nav-link[data-nav="new"]:not(:disabled):active, .session-main:not(:disabled):active, .session-menu-trigger:not(:disabled):active, .session-action-item:not([data-disabled]):active, .sidebar-account-trigger:not(:disabled):active, .credit-balance:not(:disabled):active, .account-menu-item:not([data-disabled]):active, .composer button:not(:disabled):active, .button-primary:not(:disabled):active, .button-secondary:not(:disabled):active, .dialog-close:not(:disabled):active, .generated-list button:not(:disabled):active, .inline-actions button:not(:disabled):active{ transform: scale(.96); }
@@ -1054,7 +1046,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.chat-profile-picker-chevron { width: 14px; height: 14px; flex: 0 0 auto; color: var(--color-ink-tertiary); }
.chat-profile-picker-add { margin-top: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--color-border); }
.chat-profile-picker-add a { min-height: 44px; display: flex; align-items: center; padding: 0 var(--space-3); border-radius: var(--radius-md); color: var(--color-ink); font-size: var(--type-body-sm); text-decoration: none; }
.chat-profile-picker-positioner { z-index: 30; outline: 0; }
.chat-profile-picker-popup { width: max-content; min-width: 220px; max-width: min(320px, calc(100vw - var(--space-6))); border: 1px solid var(--color-border); padding: var(--space-3); border-radius: var(--radius-lg); background: var(--color-canvas); box-shadow: var(--shadow-elevated); transform-origin: var(--transform-origin); transition: opacity 120ms ease-out, transform 120ms var(--ease-out); }
.chat-profile-picker-popup[data-starting-style], .chat-profile-picker-popup[data-ending-style] { opacity: 0; transform: translateY(calc(var(--space-1) * -1)); }
.chat-profile-picker-title { margin: 0; color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-sm); font-weight: 500; }
@@ -1287,24 +1279,13 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.birth-time-range-summary dt { color: var(--color-ink-tertiary); font-size: var(--type-overline); }
.birth-time-range-summary dd { margin: 0; color: var(--color-ink); font-size: var(--type-body-sm); font-variant-numeric: tabular-nums; }
.birth-time-assistant-intent, .birth-time-assessment-unavailable { margin: 0; padding: var(--space-3) 0; border-top: 1px solid var(--color-border); background: transparent; color: var(--color-ink); font-size: var(--type-body-sm); line-height: 1.6; word-break: keep-all; overflow-wrap: break-word; }
.birth-time-question-list { display: grid; gap: var(--space-4); }
.birth-time-question-progress { display: flex; justify-content: space-between; color: var(--color-ink-secondary); font-size: var(--type-caption); }
.birth-time-question { display: grid; gap: var(--space-3); margin: 0; padding: var(--space-4) 0 0; border: 0; border-top: 1px solid var(--color-border); }
.birth-time-question legend { display: flex; align-items: flex-start; gap: var(--space-2); color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 600; line-height: 1.55; }
.birth-time-question legend span { width: 24px; height: 24px; display: inline-grid; flex: 0 0 24px; place-items: center; border: 1px solid var(--color-border-strong); border-radius: var(--radius-xs); color: var(--color-ink-secondary); font-size: var(--type-overline); }
.birth-time-answer-list { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); }
.birth-time-answer-list button { min-height: 52px; display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); cursor: pointer; text-align: left; font-size: var(--type-caption); line-height: 1.4; transition: border-color 120ms ease-out, background-color 120ms ease-out, transform 120ms ease-out; }
.birth-time-answer-list button > span { color: var(--color-action); font-family: var(--font-mono); font-weight: 600; }
.birth-time-answer-list button.is-selected { border-color: var(--color-action); background: var(--color-action-soft); }
.birth-time-question > small { color: var(--color-ink-secondary); font-size: var(--type-overline); }
.birth-time-evidence-note, .birth-time-evidence-boundary { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.6; word-break: keep-all; overflow-wrap: break-word; }
.birth-time-event-list { display: grid; gap: var(--space-3); }
.birth-time-event-row { display: grid; grid-template-columns: 1.1fr .9fr 1fr; gap: var(--space-3); margin: 0; padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); }
.birth-time-event-row legend { grid-column: 1 / -1; padding: 0 var(--space-1); color: var(--color-ink); font-size: var(--type-caption); font-weight: 600; }
.birth-time-event-row label { min-width: 0; display: grid; gap: var(--space-2); }
.birth-time-event-row label > span { color: var(--color-ink-secondary); font-size: var(--type-overline); font-weight: 600; }
.birth-time-event-remove { min-height: 44px; grid-column: 1 / -1; justify-self: start; border: 0; padding: 0; background: transparent; color: var(--color-action); cursor: pointer; font-size: var(--type-caption); }
.birth-time-evidence-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
.birth-time-candidate-result { display: grid; gap: var(--space-4); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); }
.birth-time-candidate-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.birth-time-candidate-heading > b { color: var(--color-ink); font-size: var(--type-body-sm); }
@@ -1321,8 +1302,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.birth-time-next-step > p { margin: 0; color: var(--color-ink-secondary); }
.birth-time-next-step > .birth-time-guided-action { width: 100%; min-height: 48px; }
.birth-time-next-step > .form-error { width: 100%; color: var(--color-danger); }
.birth-time-retry-card { display: grid; justify-items: start; gap: var(--space-3); }
.birth-time-retry-card p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; }
.birth-time-guide-turn, .birth-time-evidence-draft-card, .birth-time-scoring-status, .birth-time-candidate-terminal { min-width: 0; max-width: 100%; display: grid; gap: var(--space-4); }
.birth-time-guide-question { margin: 0; color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-md); line-height: 1.55; text-wrap: pretty; word-break: auto-phrase; }
.birth-time-guide-composer, .birth-time-draft-fields label { min-width: 0; display: grid; gap: var(--space-2); }
@@ -1343,7 +1323,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.birth-time-success-note { padding: var(--space-3) 0; border-top: 1px solid var(--color-border); background: transparent; color: var(--color-success); }
.birth-time-candidate-terminal { justify-items: start; }
.birth-time-candidate-result, .birth-time-candidate-result > *, .birth-time-confirmation-panel > * { min-width: 0; max-width: 100%; }
.rectification-session-surface { width: min(720px, 100%); min-width: 0; margin: 0 auto; }
.conversational-rectification { width: 100%; min-width: 0; max-width: 720px; display: grid; gap: var(--space-5); margin: 0 auto; overflow-wrap: anywhere; color: var(--color-ink); }
.conversational-rectification > *, .conversational-rectification form, .conversational-rectification fieldset { min-width: 0; max-width: 100%; }
.conversational-rectification button { min-height: 44px; max-width: 100%; overflow-wrap: anywhere; }
@@ -1351,48 +1331,16 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.conversational-rectification .birth-time-choice-option:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
.conversational-rectification .birth-time-choice-option[data-selected="true"]:focus-visible { outline: none; }
.conversational-narrative { overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); box-shadow: var(--shadow-soft); }
.conversational-narrative-body { padding: var(--space-4) var(--space-5); }
.conversational-narrative .message-markdown { overflow-wrap: anywhere; color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.8; }
.conversational-narrative .message-markdown > :first-child { margin-top: 0; }
.conversational-narrative .message-markdown > :last-child { margin-bottom: 0; }
.conversational-narrative .message-markdown h2, .conversational-narrative .message-markdown h3 { margin: var(--space-4) 0 var(--space-2); font-family: var(--font-body); font-size: var(--type-body-md); font-weight: 600; letter-spacing: 0; }
.conversational-answer-pending { min-width: 0; display: grid; grid-template-columns: 40px minmax(0, 1fr); align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid color-mix(in srgb, var(--color-action) 24%, var(--color-border)); border-radius: var(--radius-lg); background: var(--color-action-soft); }
.conversational-answer-pending .app-loading-symbol { width: 40px; height: 40px; }
.conversational-answer-pending .app-loading-mark { width: 22px; height: 22px; }
.conversational-answer-pending > div:last-child { min-width: 0; display: grid; gap: var(--space-1); }
.conversational-answer-pending strong { color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 600; }
.conversational-answer-pending span { color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.5; }
.conversational-composer { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); }
.conversational-composer label { display: grid; gap: var(--space-1); font-size: var(--type-body-sm); font-weight: 600; }
.conversational-composer label span { color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 400; line-height: 1.5; }
.conversational-composer textarea { width: 100%; min-width: 0; min-height: 112px; resize: vertical; padding: var(--space-3); border: 1px solid var(--color-border-strong); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); line-height: 1.55; }
.conversational-composer-footer { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.conversational-composer-footer small { color: var(--color-ink-tertiary); font-size: var(--type-overline); }
.conversational-evidence-recap, .conversational-candidate, .conversational-confirmation, .conversational-original-question, .conversational-empty-state { min-width: 0; display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); }
.conversational-evidence-recap > header, .conversational-candidate > header { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.conversational-evidence-recap h3, .conversational-candidate h3, .conversational-confirmation h3 { margin: 0; font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 500; }
.conversational-evidence-recap > header span, .conversational-candidate > header span { color: var(--color-ink-secondary); font-size: var(--type-caption); }
.conversational-candidate > header .is-unverified { color: var(--color-warning); font-weight: 600; }
.conversational-candidate > header .is-confirmed { color: var(--color-success); font-weight: 600; }
.conversational-evidence-recap ul { min-width: 0; display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
.conversational-evidence-recap li { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-canvas-soft); }
.conversational-evidence-recap li > div { min-width: 0; }
.conversational-evidence-recap time { color: var(--color-ink-tertiary); font-size: var(--type-overline); }
.conversational-evidence-recap p, .conversational-candidate p, .conversational-confirmation p, .conversational-original-question p, .conversational-empty-state p, .conversational-status p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; }
.conversational-evidence-recap li button { min-width: 56px; padding: 0 var(--space-3); border: 0; background: transparent; color: var(--color-action); cursor: pointer; }
.conversational-candidate dl { min-width: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin: 0; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-border); }
.conversational-candidate dl > div { min-width: 0; display: grid; gap: var(--space-1); padding: var(--space-3); background: var(--color-canvas-soft); }
.conversational-candidate dt { color: var(--color-ink-tertiary); font-size: var(--type-overline); }
.conversational-candidate dd { min-width: 0; margin: 0; color: var(--color-ink); font-size: var(--type-body-sm); overflow-wrap: anywhere; }
.conversational-candidate time { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.conversational-candidate-progress { min-width: 0; display: grid; gap: var(--space-1); padding-top: var(--space-3); border-top: 1px solid var(--color-border); }
.conversational-candidate-progress strong { color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 600; }
.conversational-candidate-progress span { color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.55; text-wrap: pretty; }
.conversational-confirmation { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); background: var(--color-action-soft); }
.conversational-status { min-width: 0; min-height: 1px; }
.conversational-original-question { background: var(--color-success-muted); }
.phrase-nowrap { white-space: nowrap; }
.phrase-nowrap { white-space: nowrap; }
.message-list {
/* The assistant reply starts at the column edge (no avatar since
@@ -1625,8 +1573,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
font-size: var(--type-caption);
}
.reply-down-reason-head, .reply-down-reason-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.reply-down-reason-close { display: grid; place-items: center; min-width: 32px; min-height: 32px; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--color-ink-tertiary); cursor: pointer; }
.reply-down-reason-close svg { width: 16px; height: 16px; }
.reply-down-reason-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.reply-down-reason-chips button { min-height: 32px; padding: 0 var(--space-3); border: 1px solid var(--color-border); border-radius: 999px; background: transparent; color: var(--color-ink-secondary); font: inherit; cursor: pointer; }
.reply-down-reason-chips button[aria-pressed="true"] { border-color: var(--color-ink-secondary); background: var(--color-canvas-muted); color: var(--color-ink); }
@@ -1717,56 +1664,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.message-actions button:focus-visible { outline: 2px solid color-mix(in srgb, var(--color-focus) 52%, transparent); outline-offset: 1px; }
.message-actions button:disabled { cursor: default; opacity: 0.32; }
.message-actions svg { width: 13px; height: 13px; stroke-width: 1.65; }
.rectification-analysis {
width: min(620px, calc(100% - 42px));
margin: -2px 0 var(--space-1) 42px;
color: var(--color-ink-secondary);
font-size: var(--type-caption);
}
.rectification-analysis > summary {
width: fit-content;
display: flex;
align-items: center;
gap: var(--space-2);
min-height: 30px;
padding: 0 var(--space-2);
border-radius: var(--radius-sm);
color: var(--color-ink-tertiary);
cursor: pointer;
list-style: none;
transition: background-color 120ms ease-out, color 120ms ease-out;
}
.rectification-analysis > summary::-webkit-details-marker { display: none; }
.rectification-analysis > summary::before {
width: 6px;
height: 6px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
content: "";
transform: rotate(-45deg);
transition: transform 120ms ease-out;
}
.rectification-analysis[open] > summary::before { transform: rotate(45deg) translate(-1px, -1px); }
.rectification-analysis > summary:hover { background: var(--color-canvas-muted); color: var(--color-ink-secondary); }
.rectification-analysis > summary:focus-visible { outline: 2px solid color-mix(in srgb, var(--color-focus) 52%, transparent); outline-offset: 1px; }
.rectification-analysis > summary small { color: var(--color-ink-muted); font-size: inherit; }
.rectification-analysis-content {
display: grid;
gap: var(--space-3);
margin: var(--space-1) 0 var(--space-2);
padding: var(--space-3) var(--space-4);
border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--color-canvas-muted) 54%, transparent);
}
.rectification-analysis-content section { display: grid; gap: var(--space-2); }
.rectification-analysis-content h4 { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 600; }
.rectification-analysis-content ol,
.rectification-analysis-content ul { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
.rectification-analysis-content li { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.rectification-analysis-content li span { min-width: 0; color: var(--color-ink-secondary); }
.rectification-analysis-content li small { flex: 0 0 auto; color: var(--color-ink-muted); }
.rectification-analysis-content p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.55; }
/* No `text-wrap: pretty` on answer prose (BUG-1252). WebKit (every iPhone
browser) applies it to the whole paragraph: lines come out evenly short with
a blank strip on the right, and every streamed character re-breaks the
@@ -2035,24 +1933,23 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.model-selector-trigger > b { min-width: 0; overflow: visible; overflow-wrap: anywhere; text-align: left; white-space: normal; font-size: var(--type-caption); font-weight: 500; line-height: 1.35; }
.model-selector-trigger svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--color-ink-tertiary); transition: transform 120ms ease-out; }
.model-selector-trigger[data-popup-open] svg { transform: rotate(180deg); }
.model-selector-positioner { z-index: 30; outline: 0; }
.model-selector-popup { width: max-content; min-width: 180px; max-width: min(420px, calc(100vw - var(--space-6))); border: 1px solid var(--color-border); padding: var(--space-3); border-radius: var(--radius-lg); background: var(--color-canvas); box-shadow: var(--shadow-elevated); transform-origin: var(--transform-origin); transition: opacity 120ms ease-out, transform 120ms var(--ease-out); }
.model-selector-popup[data-starting-style], .model-selector-popup[data-ending-style] { opacity: 0; transform: translateY(var(--space-1)); }
.model-selector-title { margin: 0; color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-sm); font-weight: 500; }
.model-selector-options { display: grid; gap: var(--space-1); margin: var(--space-2) 0 0; padding: 0; border: 0; }
.model-selector-option { width: 100%; min-height: 48px; display: grid; grid-template-columns: auto 20px; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border: 1px solid transparent; border-radius: var(--radius-md); color: var(--color-ink-secondary); cursor: pointer; transition: border-color 120ms ease-out, background-color 120ms ease-out, color 120ms ease-out; }
.model-selector-option[data-selected] { border-color: var(--color-border); background: var(--color-selected); color: var(--color-ink); }
.model-selector-option:has(input:focus-visible) { outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent); outline-offset: 1px; }
.model-selector-copy { width: max-content; min-width: 0; max-width: min(320px, calc(100vw - 96px)); display: grid; gap: var(--space-1); }
.model-selector-copy b { overflow: visible; overflow-wrap: anywhere; color: inherit; white-space: normal; font-size: var(--type-body-sm); font-weight: 500; line-height: 1.35; }
.model-selector-check { width: 18px; height: 18px; opacity: 0; color: var(--color-action); }
.model-selector-option[data-selected] .model-selector-check { opacity: 1; }
@media (max-width: 1023px) {
.model-selector-trigger { max-width: 100%; grid-column: 1; grid-row: 1; }
}
:has(> .account-menu-popup) { z-index: 30; max-width: calc(100vw - var(--space-6)); max-height: calc(100dvh - var(--space-6)); outline: 0; }
.account-menu-popup { width: min(280px, calc(100vw - var(--space-6))); max-height: var(--available-height); overflow-y: auto; padding: var(--space-2); border: 1px solid var(--sidebar-border); border-radius: var(--radius-xl); background: var(--color-canvas); color: var(--sidebar-foreground); box-shadow: var(--shadow-elevated); transform-origin: var(--transform-origin); transition: opacity 120ms ease-out, transform 120ms var(--ease-out); }
.account-menu-popup[data-starting-style], .account-menu-popup[data-ending-style] { opacity: 0; transform: translateY(var(--space-1)); }
@@ -2146,7 +2043,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
children, never on the scroll container or dialog box, so pane switches keep the
same frame (BUG-554 / BUG-698). List panes remain full-bleed within this inset. */
.settings-dialog-content--form > * { max-width: 560px; margin-right: auto; }
.settings-dialog-copy { max-width: 760px; margin: 0 0 var(--space-5); color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; }
.avatar-section { padding-top: 0; }
.avatar-editor { display: grid; grid-template-columns: 56px minmax(0, 1fr); align-items: center; gap: var(--space-5); margin-top: var(--space-5); }
.avatar-editor-controls { min-width: 0; display: grid; gap: var(--space-4); }
@@ -2185,35 +2082,12 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.sheet-section { border-bottom: 1px solid var(--color-border); padding: var(--space-6) 0; border-color: var(--color-border); }
.section-toggle b, .section-heading b { display: block; font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 500; }
.section-toggle small, .section-heading small { display: block; margin-top: 4px; color: var(--color-ink-secondary); font-weight: 400; font-size: var(--type-caption); }
.default-chart-card { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-top: var(--space-5); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas-muted); }
.default-chart-card > div, .chart-library-item > div:first-child, .chart-library-actions { min-width: 0; }
.default-chart-card span, .default-chart-card small { display: block; color: var(--color-ink-secondary); font-size: var(--type-caption); }
.default-chart-card strong { display: block; margin: 4px 0; color: var(--color-ink); font-size: var(--type-body-md); font-weight: 500; }
.chart-library-panel { display: grid; gap: var(--space-5); margin-top: var(--space-5); }
.chart-library-group { display: grid; gap: var(--space-3); }
.chart-library-group-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.chart-library-group-heading > div:first-child { min-width: 0; }
.chart-library-group-heading-actions { display: flex; align-items: center; flex: 0 0 auto; gap: var(--space-2); }
.chart-library-group-heading b, .chart-library-group-heading small { display: block; }
.chart-library-group-heading b { color: var(--color-ink); font-size: var(--type-caption); font-weight: 600; }
.chart-library-group-heading small { margin-top: 3px; color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 400; }
.chart-library-count { display: grid; min-width: 24px; height: 24px; place-items: center; border-radius: 999px; background: var(--color-canvas-muted); color: var(--color-ink-secondary); font-size: var(--type-overline); }
.chart-library-item { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); width: 100%; min-height: 44px; padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); }
button.chart-library-item { cursor: pointer; text-align: left; }
.chart-library-item-chevron { width: 16px; height: 16px; flex: 0 0 auto; color: var(--color-ink-tertiary); }
.chart-library-back { min-height: 44px; display: inline-flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-4); padding: 0; border: 0; background: transparent; color: var(--color-ink-secondary); cursor: pointer; font-size: var(--type-body-sm); }
.chart-library-add { margin-top: var(--space-4); }
.chart-library-add--heading { margin-top: 0; white-space: nowrap; }
.chart-library-item-main { min-width: 0; }
.chart-library-item strong, .chart-library-item small { display: block; }
.chart-library-item strong { color: var(--color-ink); font-size: var(--type-body-md); font-weight: 500; }
.chart-library-item small, .chart-library-item > span, .empty-library-copy { color: var(--color-ink-secondary); font-size: var(--type-caption); }
.chart-library-item small + small { margin-top: 3px; }
.chart-library-item-title { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-1); }
.chart-role-badge, .chart-default-badge { display: inline-flex; align-items: center; min-height: 20px; padding: 0 var(--space-2); border-radius: 999px; background: var(--color-canvas-muted); color: var(--color-ink-secondary); font-size: var(--type-overline); font-weight: 600; }
.chart-role-badge.is-self, .chart-default-badge { background: var(--color-action-soft); color: var(--color-action-hover); }
.chart-library-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
.chart-library-actions > * { max-width: 100%; }
.chart-library-form { padding-top: var(--space-4); border-top: 1px solid var(--color-border); }
.synastry-report-card { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas-muted); }
.synastry-report-card span, .synastry-report-card small, .synastry-report-card li { color: var(--color-ink-secondary); font-size: var(--type-caption); }
@@ -2311,16 +2185,12 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
content: "";
}
.admin-page { background: var(--color-canvas-soft); }
.admin-app-shell { height: 100vh; min-height: 0; overflow-y: auto; }
.admin-app-shell > *, .admin-app-shell .ant-layout { min-height: 100%; }
.code-form > * { min-width: 0; }
.section-title h2 { margin: 0; font-family: var(--font-display); font-size: var(--type-title-lg); font-weight: 500; letter-spacing: -.3px; }
.section-title p { margin: 6px 0 0; color: var(--color-ink-secondary); font-size: var(--type-caption); }
.generated-list > div { min-height: 48px; display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 12px; padding: 0 12px; border: 0; border-radius: var(--radius-md); background: var(--color-canvas-muted); }
.generated-list code { font-family: var(--font-mono); }
.generated-list button { min-width: 44px; min-height: 44px; border: 0; background: transparent; cursor: pointer; color: var(--color-action); }
.code-status { display: inline-flex; min-height: 28px; align-items: center; padding: 0 var(--space-2); border-radius: var(--radius-md); background: var(--color-canvas-muted); color: var(--color-ink-secondary); }
.status-可用 { background: var(--color-success-muted); color: var(--color-success); }
.status-已过期, .status-已兑换, .empty-cell { color: var(--color-ink-tertiary); }
@@ -2370,7 +2240,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.welcome { padding: var(--space-6) 0 var(--space-10); }
.welcome > .onboarding-message:first-child .message-bubble p { font-size: var(--type-display-sm); }
.message-list { width: 100%; padding: var(--space-5) var(--space-4) var(--space-12); }
.message-content { max-width: 88%; }
.composer-wrap { padding: var(--space-2) var(--space-3) max(var(--space-3), env(safe-area-inset-bottom)); }
@@ -2388,9 +2257,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.avatar-editor { grid-template-columns: 56px minmax(0, 1fr); gap: var(--space-4); }
.avatar-editor > .user-avatar { width: 56px !important; height: 56px !important; }
.avatar-palette-list { grid-template-columns: repeat(2, minmax(64px, 1fr)); }
.chart-library-group-heading { align-items: flex-start; }
.chart-library-group-heading-actions { align-items: flex-end; flex-direction: column; gap: var(--space-1); }
.chart-library-add--heading { min-height: 40px; padding-inline: var(--space-3); }
.auth-page { height: 100vh; overflow-x: hidden; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 0; grid-template-rows: minmax(0, 1fr) auto; justify-items: stretch; }
.auth-page > .auth-shell { grid-row: 1; }
.auth-page > .auth-legal-footer { grid-row: 2; background: var(--color-canvas); }
@@ -2400,12 +2267,11 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.auth-story > div > p, .auth-footnote { display: none; }
.auth-panel { justify-content: flex-start; padding: var(--space-8) var(--space-6) max(var(--space-8), env(safe-area-inset-bottom)); }
.auth-panel h1 { font-size: var(--type-display-md); }
.birth-time-event-row { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
.default-chart-card, .chart-library-item { flex-direction: column; align-items: stretch; }
.chart-library-actions { justify-content: flex-start; }
.birth-time-assessment-heading { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
.birth-time-status-badge { justify-self: start; }
.birth-time-detail-grid, .birth-time-range-summary, .birth-time-answer-list, .birth-time-candidate-grid { grid-template-columns: 1fr; }
@@ -2434,10 +2300,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
@media (max-width: 480px) {
.conversational-rectification { width: 100%; max-width: 100%; gap: var(--space-4); overflow-x: clip; }
.conversational-narrative-body, .conversational-composer, .conversational-evidence-recap, .conversational-candidate, .conversational-confirmation, .conversational-original-question, .conversational-empty-state { padding: var(--space-3); }
.conversational-candidate dl { grid-template-columns: minmax(0, 1fr); }
.conversational-composer-footer { align-items: stretch; flex-direction: column; }
.conversational-composer-footer button, .conversational-confirmation button, .conversational-original-question button { width: 100%; }
}
/* ── Secondary pages inside the app shell ─────────────────────────────────
@@ -2680,13 +2543,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.starter-entry-boundary { color: var(--color-ink-tertiary); }
@media (hover: hover) {
}
/* One column width across both states: the empty-state composer used to be
1040px while the session composer was 760px, so the input visibly jumped
width the moment the first message landed. */
@@ -2702,17 +2558,11 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
backdrop-filter: none;
}
@media (min-width: 768px) and (max-width: 1023px) {
}
@media (max-width: 767px) {
.welcome {
padding: var(--space-4) 0 var(--space-8);
}
}
@media (max-width: 480px) {
@@ -2743,7 +2593,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-top: var(--space-8);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant .message-bubble {
padding: 0 0 var(--space-2);
}
@@ -2826,16 +2675,10 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-top: var(--space-6);
}
.conversation:not(.is-empty):not(.is-rectification) .message-user .message-content {
max-width: 88%;
}
.rectification-analysis {
width: calc(100% - 38px);
margin-left: 38px;
}
.conversation:not(.is-empty):not(.is-rectification) + .composer-wrap {
padding-top: var(--space-3);
padding-bottom: var(--space-3);
@@ -3417,17 +3260,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.rectification-board .technique-audit > .rectification-board__group + .rectification-board__minutes {
margin-top: var(--space-2);
}
.rectification-board__section {
display: grid;
gap: var(--space-3);
}
.rectification-board__section h3 {
margin: 0;
font-family: var(--font-display);
font-size: var(--type-title-sm);
font-weight: 600;
letter-spacing: -.2px;
}
/* Names the list that follows it now that 换升时刻 is a group inside 候选分钟
rather than a fold of its own. Not a <summary>: this content is always open
once its section is, so a second disclosure to click would be one more step
@@ -3734,17 +3567,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
border-color: color-mix(in srgb, var(--color-action) 55%, var(--color-border));
background: color-mix(in srgb, var(--color-action-soft) 60%, var(--color-canvas));
}
.rectification-candidate-time { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rectification-candidate-time strong { font-size: var(--type-title-md); font-family: var(--font-display); font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.2px; }
.rectification-candidate-badge {
padding: var(--space-1) var(--space-2);
border-radius: 999px;
color: var(--color-action) !important;
background: color-mix(in srgb, var(--color-action) 10%, transparent);
font-size: var(--type-overline) !important;
font-weight: 600;
}
.rectification-candidate-action { align-self: end; color: var(--color-action); font-size: 13px; font-weight: 600; }
.rectification-range-delivery {
display: grid;
gap: var(--space-4);
@@ -3921,44 +3744,13 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
color: var(--color-ink);
font-size: var(--type-body-sm);
}
.rectification-saved-wrap {
display: grid;
gap: var(--space-3);
width: calc(100% - var(--assistant-content-inset));
margin: 8px 0 16px;
margin-inline-start: var(--assistant-content-inset);
}
.rectification-saved-wrap .rectification-saved {
width: auto;
margin: 0;
margin-inline-start: 0;
}
.rectification-refinement {
display: grid;
gap: var(--space-3);
width: calc(100% - var(--assistant-content-inset));
margin: 8px 0 16px;
margin-inline-start: var(--assistant-content-inset);
padding: var(--space-5);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas-soft);
color: var(--color-ink);
font-size: var(--type-body-sm);
line-height: 1.55;
}
.rectification-refinement strong {
font-size: var(--type-title-sm);
font-family: var(--font-display);
font-weight: 600;
letter-spacing: -.2px;
}
.rectification-refinement ul {
margin: 0;
padding-inline-start: 1.1em;
}
.rectification-refinement p,
.rectification-refinement__stage { margin: 0; color: var(--color-ink-secondary); }
.rectification-readonly-range {
display: block;
width: calc(100% - var(--assistant-content-inset));
@@ -4015,10 +3807,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
/* The QR stays on literal white in both themes: scanners need the light modules
to actually be light, so this surface is not themed. */
.payment-qr-wrap { position: relative; width: min(220px, 72vw); aspect-ratio: 1; margin: 14px auto; padding: 10px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: #fff; box-shadow: var(--shadow-elevated); }
.payment-qr-wrap img { display: block; width: 100%; height: 100%; object-fit: contain; }
.payment-qr-badge { position: absolute; top: 50%; left: 50%; display: grid; width: 44px; height: 44px; padding: 4px; transform: translate(-50%, -50%); border: 4px solid #fff; border-radius: 12px; background: #fff; box-shadow: 0 2px 10px rgb(0 0 0 / 18%); }
.payment-qr-badge svg { display: block; width: 100%; height: 100%; }
/* Global report centre — quiet archive, independent from chat sessions. */.report-center-section{
width: min(900px, 100%);
@@ -4235,8 +4023,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.report-center-menu summary::-webkit-details-marker { display: none; }
.report-center-menu > div { background: var(--color-canvas); border: 1px solid var(--color-border); border-radius: var(--radius-md); }
.report-center-menu summary:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.report-center-delete-confirm { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.report-center-delete-confirm p { margin: 0; }
.report-center-delete-confirm button, .report-center-menu button { min-height: 44px; }
/* Export sheet (K5): the component library slides it from the right, or from the
@@ -4257,7 +4044,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.report-export-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: var(--space-3) var(--space-3) var(--space-1) var(--space-5); flex-shrink: 0; }
.report-export-header h2 { margin: 0; font-size: var(--type-title-md); font-weight: 600; }
.report-export-body { display: grid; gap: var(--space-4); min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--space-2) var(--space-5) var(--space-4); }
.report-export-field { display: grid; gap: var(--space-2); }
.report-export-segment { display: grid; grid-template-columns: 1fr 1fr; padding: 3px; border-radius: var(--radius-md); background: var(--color-canvas-muted); }
.report-export-segment button { border: 0; border-radius: calc(var(--radius-md) - 2px); background: transparent; color: var(--color-ink-secondary); font: inherit; font-size: var(--type-body-sm); cursor: pointer; }
.report-export-segment button[aria-checked="true"] { background: var(--color-canvas); color: var(--color-action); font-weight: 600; box-shadow: 0 1px 2px color-mix(in srgb, var(--color-ink) 12%, transparent), inset 0 0 0 1px var(--color-action-strong); }
@@ -4311,7 +4098,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
overflow: hidden;
}
.feedback-dialog::backdrop { background: var(--color-scrim); }
.feedback-dialog-panel { display: flex; flex-direction: column; max-height: 90dvh; }
.feedback-dialog-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: var(--space-3) var(--space-3) var(--space-1) var(--space-5); flex-shrink: 0; }
.feedback-dialog-header h2 { margin: 0; font-size: var(--type-title-md); font-weight: 600; }
.feedback-dialog-body { display: grid; gap: var(--space-4); min-height: 0; overflow-y: auto; padding: var(--space-2) var(--space-5) max(var(--space-5), env(safe-area-inset-bottom)); }
@@ -4330,12 +4117,11 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.feedback-dialog input:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
@media (min-width: 860px) {
.feedback-dialog { inset: 0; width: min(520px, 92vw); max-height: 80vh; margin: auto; border-radius: var(--radius-lg); }
.feedback-dialog-panel { max-height: calc(80vh - 2px); }
}
@media (max-width: 480px) { .feedback-dialog-types { grid-template-columns: repeat(2, 1fr); } }
@media print { .feedback-dialog { display: none !important; } }
/* ============================================================
personal-report: unique block — editorial reader + A4 print
Only /reports/[reportId] uses these semantic classes. The report owns its
@@ -5877,7 +5663,7 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); }
overflow: hidden;
}
.chart-page-type-sheet::backdrop { background: var(--color-scrim); }
.chart-page-type-sheet-panel { display: flex; flex-direction: column; max-height: 80vh; }
@supports (height: 1dvh) {
.chart-page-type-sheet, .chart-page-type-sheet-panel { max-height: 80dvh; }
}
@@ -6052,22 +5838,7 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); }
/* The ascendant status cell is "/" plus the same bubble. Padding moves onto the
button so the tap target fills the cell without a second chrome. */
.chart-page-planet-table td.chart-page-asc-status { padding: 0; }
.chart-page-asc-status-mark {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
min-height: 44px;
margin: 0;
padding: var(--space-3);
border: 0;
background: transparent;
color: inherit;
font: inherit;
line-height: inherit;
text-align: inherit;
cursor: help;
}
.chart-page-planet-table td.chart-page-planet-mid .chart-page-asc-status-mark { justify-content: center; }
/* Only the small type is lifted for the chart page's narrow stage. Raising
every `text` would also resize the planet symbols, which are already sized
@@ -7149,15 +6920,6 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); }
text-align: center;
}
.people-archive-foot { display: grid; gap: var(--space-2); justify-items: center; text-align: center; }
.people-archive-confirm {
display: grid;
gap: var(--space-3);
}
.people-archive-confirm p {
margin: 0;
color: var(--color-ink-strong);
font-size: var(--type-body-sm);
}
@media (max-width: 1023px) {
.people-archive { width: min(640px, 100%); }
@@ -2,6 +2,8 @@
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
AYANAMSA_OPTIONS,
AYANAMSA_SWITCH_HINT,
@@ -25,10 +27,11 @@ export function AyanamsaPreferenceField({
{AYANAMSA_OPTIONS.map((option) => {
const selected = value === option.value;
return (
<button
className="theme-preference-option ayanamsa-preference-option"
<Button
className="theme-preference-option ayanamsa-preference-option h-auto whitespace-normal font-normal shadow-none"
key={option.value}
type="button"
variant="ghost"
role="radio"
aria-checked={selected}
onClick={() => onChange(option.value)}
@@ -38,7 +41,7 @@ export function AyanamsaPreferenceField({
<small>{option.description}</small>
</span>
{selected ? <Check aria-hidden="true" /> : null}
</button>
</Button>
);
})}
</div>
@@ -3,6 +3,7 @@
import { Check, MapPin, RotateCcw } from "lucide-react";
import { InlineSpinner } from "@/components/inline-spinner";
import { useEffect, useId, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
birthPlaceDraftFromLevels,
@@ -487,9 +488,9 @@ export function BirthPlacePicker({
{(settled.status === "idle" || settled.status === "error") && <MapPin aria-hidden="true" />}
<span>{statusText}</span>
{settled.status === "error" && (
<button type="button" className="birth-place-retry" onClick={() => setRetryToken((token) => token + 1)}>
<Button type="button" variant="ghost" className="birth-place-retry h-auto px-0 font-normal shadow-none" onClick={() => setRetryToken((token) => token + 1)}>
<RotateCcw aria-hidden="true" />重试
</button>
</Button>
)}
</p>
</div>
@@ -4,6 +4,7 @@ import { useId, useState } from "react";
import { BookOpen, Check, ChevronDown, Layers, ListTodo, LoaderCircle, PenLine, type LucideIcon } from "lucide-react";
import { InlineSpinner } from "@/components/inline-spinner";
import { Button } from "@/components/ui/button";
import { CONSULTATION_RECEIVED_LABEL } from "@/lib/consultation-activity-labels";
void LoaderCircle; // source lock: tests/chat-stream-layout.test.ts matches /LoaderCircle/
@@ -82,9 +83,10 @@ export function ConsultationRunTimeline({
className={`consultation-run-timeline${open ? " is-open" : ""}${live ? " is-live" : ""}`}
aria-label="分析步骤"
>
<button
<Button
type="button"
className="consultation-run-timeline__summary"
variant="ghost"
className="consultation-run-timeline__summary h-auto whitespace-normal rounded-none px-0 font-normal shadow-none"
aria-expanded={open}
aria-controls={bodyId}
onClick={() => setUserOpen(!open)}
@@ -96,7 +98,7 @@ export function ConsultationRunTimeline({
{summary}
</span>
<ChevronDown className="consultation-run-timeline__chevron" aria-hidden="true" />
</button>
</Button>
<div className="consultation-run-timeline__body-wrap" id={bodyId} inert={open ? undefined : true}>
<div className="consultation-run-timeline__body-inner">
<ol className="agent-thinking-timeline consultation-run-timeline__list" aria-label="步骤">
@@ -2,6 +2,8 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
export function HomeBootstrapError({
accountError,
onRetry,
@@ -15,7 +17,7 @@ export function HomeBootstrapError({
<strong>暂时无法进入 Jyotisha</strong>
<span>{accountError}</span>
<div className="app-loading-actions">
<button className="button-primary" type="button" onClick={onRetry}>重试</button>
<Button className="button-primary" type="button" onClick={onRetry}>重试</Button>
<Link className="button-secondary" href="/login">返回登录</Link>
</div>
</div>
@@ -7,6 +7,7 @@ import { BirthTimeIntakeFields } from "@/components/birth-time-intake";
import { rememberSkyOrigin } from "@/components/birth-sky/sky-origin";
import { OnboardingChatMessage } from "@/components/onboarding-chat-message";
import { OnboardingWhoStep } from "@/components/onboarding-who-step";
import { Button } from "@/components/ui/button";
import type { BirthTimeGuidedController } from "@/hooks/use-birth-time-guided-journey";
import {
applyBirthTimeDraftPatch,
@@ -117,7 +118,7 @@ export function HomeOnboardingShell({
<BirthTimeIntakeFields value={profileDraft} onPatch={(patch) => setProfileDraft((current) => applyBirthTimeDraftPatch(current, patch))} />
<div className="onboarding-card-actions">
{birthTimeContinueHint ? <p className="onboarding-card-action-hint">{birthTimeContinueHint}</p> : <span />}
<button className="button-primary" type="submit" data-onboarding-press="" disabled={profileSaving || !isBirthTimeDraftReady(profileDraft)}>{profileSaving ? "保存中" : "继续"}</button>
<Button className="button-primary" type="submit" data-onboarding-press="" disabled={profileSaving || !isBirthTimeDraftReady(profileDraft)}>{profileSaving ? "保存中" : "继续"}</Button>
</div>
</fieldset>
<BirthTimeAssessmentOverlay phase={birthTimeAssessmentPhase} />
@@ -133,7 +134,7 @@ export function HomeOnboardingShell({
<div className="onboarding-card-heading"><b>出生地点</b><small>搜索全球城市或区县,系统会匹配出生当日的时区。</small></div>
<fieldset className="birth-time-transition-fields" disabled={birthTimeAssessmentPhase !== null}>
<BirthLocationFields value={profileDraft} onChange={setProfileDraft} />
<div className="onboarding-card-actions"><button className="button-primary" type="submit" data-onboarding-press="" onClick={(event) => rememberSkyOrigin(event.clientX, event.clientY)} disabled={profileSaving || !selectedBirthPlace(profileDraft)}>{profileSaving ? "保存中" : "确定"}</button></div>
<div className="onboarding-card-actions"><Button className="button-primary" type="submit" data-onboarding-press="" onClick={(event) => rememberSkyOrigin(event.clientX, event.clientY)} disabled={profileSaving || !selectedBirthPlace(profileDraft)}>{profileSaving ? "保存中" : "确定"}</Button></div>
</fieldset>
<BirthTimeAssessmentOverlay phase={birthTimeAssessmentPhase} />
</form>
@@ -17,6 +17,7 @@ import { parseReportFocusBlock, reportTitleByline } from "@/lib/report-focus-blo
import { reportChartGrid } from "@/lib/report-chart-grid-rehype";
import { NorthIndianChartSvg } from "@/components/personal-report/vedic-chart-svg";
import { ReportEdgeJump } from "@/components/personal-report/report-edge-jump";
import { Button } from "@/components/ui/button";
import type { ReportLanguage } from "@/lib/report-language";
import {
buildLongformOutline,
@@ -381,9 +382,9 @@ export function PersonalReportMarkdownView({
</section>
) : null}
<nav aria-label="章节" className="personal-report-chapter-pager personal-report-screen-only">
<button type="button" disabled={safeChapter <= 0} onClick={() => setChapter(safeChapter - 1)}>上一章</button>
<Button type="button" variant="ghost" className="h-auto font-normal shadow-none" disabled={safeChapter <= 0} onClick={() => setChapter(safeChapter - 1)}>上一章</Button>
<span>{safeChapter + 1} / {chapters.length}</span>
<button type="button" disabled={safeChapter >= chapters.length - 1} onClick={() => setChapter(safeChapter + 1)}>下一章</button>
<Button type="button" variant="ghost" className="h-auto font-normal shadow-none" disabled={safeChapter >= chapters.length - 1} onClick={() => setChapter(safeChapter + 1)}>下一章</Button>
</nav>
</article>
<ReportEdgeJump revealAll={revealAll} />
@@ -54,7 +54,10 @@ test("the timeline collapses in place with a 180ms height transition and honours
assert.match(timelineSource, /const open = userOpen \?\? live/);
assert.match(timelineSource, /aria-expanded=\{open\}/);
assert.match(timelineSource, /inert=\{open \? undefined : true\}/);
assert.match(timelineSource, /className="consultation-run-timeline__summary"/);
// 原值:className="consultation-run-timeline__summary"
// 新值:同一类名仍在,后面可以再接压掉按钮默认高度和换行的类
// 原因:K7 把这一行换成组件库 Button。类名、展开和 180ms 高度过渡不变
assert.match(timelineSource, /className="consultation-run-timeline__summary(?: [^"]*)?"/);
assert.match(timelineSource, /agent-activity-status__text/);
assert.match(globalStyles, /\.consultation-run-timeline__body-wrap \{[^}]*grid-template-rows: 0fr/);
assert.match(globalStyles, /\.consultation-run-timeline__body-wrap \{[^}]*transition: grid-template-rows 180ms var\(--ease-out\)/);
@@ -82,13 +82,18 @@ test("site chrome owns globals.css; admin does not import it", () => {
/--font-body: var\(--font-inter, Inter\)/);
});
test("root error and not-found pages do not depend on Tailwind utilities or Button", () => {
test("root error and not-found pages do not depend on the site stylesheet", () => {
const errorPage = readFileSync(new URL("../src/app/error.tsx", import.meta.url), "utf8");
const notFoundPage = readFileSync(new URL("../src/app/not-found.tsx", import.meta.url), "utf8");
// 原值:error.tsx 与 not-found 都不得 import Button
// 新值:error.tsx 可以 import Button,颜色仍写在内联样式里;not-found 仍不得 import Button
// 原因:K7 清空白名单。这一页不引进整站样式表,避免聊天样式进后台路由
for (const source of [errorPage, notFoundPage]) {
assert.doesNotMatch(source, /className="flex/);
assert.doesNotMatch(source, /min-h-svh/);
assert.doesNotMatch(source, /text-muted-foreground/);
assert.doesNotMatch(source, /@\/components\/ui\/button/);
assert.doesNotMatch(source, /site-styles|globals\.css/);
}
assert.match(errorPage, /import \{ Button \} from "@\/components\/ui\/button";/);
assert.doesNotMatch(notFoundPage, /@\/components\/ui\/button/);
});
+8 -61
View File
@@ -5,70 +5,17 @@ import { fileURLToPath } from "node:url";
import { ESLint } from "eslint";
/** K0 snapshot. Later issues may delete entries. Adding one fails this test. */
/**
* K7 ceiling. The list may still shrink. Adding one fails this test.
* 原值:K0 的 62 个文件。
* 新值:天空绘制和校正顶栏探头这两个文件。
* 原因:K7 清空白名单。星盘和星图的图标豁免在规则里,不在这份名单。
* 天空过场必须留着原生 dialog.showModal 和 createPortal,合同测试锁住这段,换组件会改绘制。
* 校正探头必须留着 createPortal 挂到顶栏,合同测试锁住这句;改成弹窗会改 K6 的交互。
*/
const BASELINE = [
"src/app/(app)/page.tsx",
"src/app/error.tsx",
"src/app/global-error.tsx",
"src/components/account-deletion-gate.tsx",
"src/components/account-deletion-section.tsx",
"src/components/account-dialog-overlay.tsx",
"src/components/ayanamsa-preference-field.tsx",
"src/components/billing-panel.tsx",
"src/components/birth-place-picker.tsx",
"src/components/birth-sky/birth-sky-converge.tsx",
"src/components/birth-sky/birth-sky-replay.tsx",
"src/components/birth-time-candidate-result.tsx",
"src/components/birth-time-choice-question.tsx",
"src/components/birth-time-evidence-draft-card.tsx",
"src/components/birth-time-guide-turn.tsx",
"src/components/birth-time-intake.tsx",
"src/components/birth-time-legacy-rectification.tsx",
"src/components/birth-time-rectification.tsx",
"src/components/chart-page/chart-birth-editor.tsx",
"src/components/chart-page/chart-dasha-tab.tsx",
"src/components/chart-page/chart-page-view.tsx",
"src/components/chart-page/chart-type-picker.tsx",
"src/components/chart-page/chart-vedic-tab.tsx",
"src/components/chart-profile-form.tsx",
"src/components/chat-composer.tsx",
"src/components/chat-message-actions.tsx",
"src/components/consultation-run-timeline.tsx",
"src/components/conversation-follow-ups.tsx",
"src/components/email-otp-login.tsx",
"src/components/ephemeris/ephemeris-page.tsx",
"src/components/feedback-dialog.tsx",
"src/components/home-bootstrap-error.tsx",
"src/components/home-onboarding-shell.tsx",
"src/components/jump-to-latest-button.tsx",
"src/components/legal/legal-consent-gate.tsx",
"src/components/legal/login-consent.tsx",
"src/components/markdown-code-block.tsx",
"src/components/model-selector.tsx",
"src/components/onboarding-redeem-paywall.tsx",
"src/components/people/people-page.tsx",
"src/components/person-relation-field.tsx",
"src/components/personal-report/personal-report-center.tsx",
"src/components/personal-report/personal-report-markdown-view.tsx",
"src/components/personal-report/report-actions.tsx",
"src/components/personal-report/report-edge-jump.tsx",
"src/components/personal-report/report-export-drawer.tsx",
"src/components/personal-report/report-row-delete.tsx",
"src/components/profile-gender-field.tsx",
"src/components/profile-panel.tsx",
"src/components/rectification-agentic-chat.tsx",
"src/components/rectification-board.tsx",
"src/components/rectification-choice-card.tsx",
"src/components/rectification-composer-reopen.tsx",
"src/components/rectification-range-delivery.tsx",
"src/components/rectification-segment-delivery.tsx",
"src/components/reply-down-reason.tsx",
"src/components/starter-home.tsx",
"src/components/subject-switcher.tsx",
"src/components/synastry/synastry-add-dialog.tsx",
"src/components/synastry/synastry-picker.tsx",
"src/components/synastry/synastry-report-view.tsx",
"src/components/theme-preference-menu.tsx",
] as const;
const RULE = "jyotisha-ui/no-handwritten-control";