From 728ead8844631747b13346354e7a5fc37a72800c Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Sun, 25 Oct 2026 06:29:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=A1=E6=AD=A3=E7=9A=84=E9=80=89=E9=A1=B9?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E6=8D=A2=E6=88=90=E7=BB=84=E4=BB=B6=E5=BA=93?= =?UTF-8?q?=EF=BC=88#110=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 题目、计分、扣点和保存没有改。明确说要校正的人仍然直接开始,没做完的仍然接着做。选项改用单选标签,确定程度改用圆点,其余按钮改用已有组件。动效只淡入。 自测:tsc 通过;frontend lint 0 error(122 条原有警告);校正前端测试 2291 条,通过 2270、失败 21,与改前名单一致;Python test_rectification_*.py 26 个文件退出码 0;next build 后 / 仍是 Static;首屏 JS gzip-9 644683,相对 1516ad56 记下的 642174 增加 0.39%。隐私标记测试通过。 Co-Authored-By: Grok (agent loop) --- CHANGELOG.md | 8 ++ docs/BUG_HISTORY.md | 16 ++++ ...ROGRESS-ui-kit-6-rectification-20261023.md | 71 +++++++++++++++ docs/testing/ui-kit-migration-20261023.md | 11 +++ frontend/DESIGN.md | 2 + frontend/eslint/ui-primitive-whitelist.json | 14 +-- frontend/src/app/birth-time-choice.css | 11 +-- frontend/src/app/globals.css | 27 ++++-- .../birth-time-candidate-result.tsx | 15 ++-- .../components/birth-time-choice-question.tsx | 81 ++++++++++------- .../birth-time-evidence-draft-card.tsx | 41 +++++---- .../src/components/birth-time-guide-turn.tsx | 9 +- frontend/src/components/birth-time-intake.tsx | 88 +++++++++++-------- .../birth-time-legacy-rectification.tsx | 5 +- .../components/birth-time-rectification.tsx | 9 +- .../src/components/rectification-board.tsx | 11 ++- .../components/rectification-choice-card.tsx | 65 +++++++++----- .../rectification-composer-reopen.tsx | 5 +- .../src/components/rectification-fade.tsx | 50 +++++++++++ .../rectification-range-delivery.tsx | 6 +- .../rectification-segment-delivery.tsx | 5 +- .../tests/rectification-agentic-entry.test.ts | 5 +- 22 files changed, 400 insertions(+), 155 deletions(-) create mode 100644 docs/tasks/PROGRESS-ui-kit-6-rectification-20261023.md create mode 100644 frontend/src/components/rectification-fade.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d7d73c..af8f857e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 印度占星 Skill 更新日志 +## 2026-10-25 — 生时校正的选项卡片换成组件库 + +- 校正里的选项仍是圆角卡片。点中之后是浅底描边。确定程度仍是三个圆点,大概范围仍是一排标签。 +- 盘面的关闭仍是一枚叉。重新打开、采用和其余确认都换成了同一套按钮。 +- 题目出现时只淡入。系统开了减少动态效果时同样只淡入,没有位移,也没有来回循环。 +- 题目、计分、扣点和保存都没有改。明确说要校正的人仍然直接开始,没做完的仍然接着做。 +- Skill 版本不变(BUG-1438)。 + ## 2026-10-25 — 报告、星历和登录页换成组件库 - 报告的中文和 English 是一排分段。切换之后,页面上不留下上一种语言的正文。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b9c870a7..409be2db 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -19494,4 +19494,20 @@ - 复发自:无 - 修复版本:分支 `codex/ui-kit-5-pages-20261023`。 +## BUG-1438 | 生时校正界面换成组件库 + +- 状态:enhancement +- 首次发现:2026-10-25 +- 最近更新:2026-10-25 +- 影响面:校正里的选项卡片、确定程度、大概范围、经历草稿的精度、盘面的关闭和重新打开、采用按钮。题目怎么出、怎么计分、怎么落库、怎么扣点,都不在这次的改动里。校正入口也不在这次:明确要求校正的人仍然直接开始,没做完的仍然接着做。 +- 用户现象:选项仍是原来的圆角卡片,点中后浅底描边。确定程度仍是三个圆点。范围仍是一排标签。盘面关闭仍是一枚叉。题目出现时,如果动效已经载入,只淡入;系统开了减少动态效果时同样只淡入,没有位移,也没有循环。 +- 触发条件:打开一次生时校正,或在建档时填写出生时间的确定程度。 +- 根因:不是缺陷。组件库迁移的生时校正这一张(任务书 K6,决策 D4、D7、D9)。 +- 修复:选项卡片和大概范围改用单选标签组。确定程度改用单选圆点。其余确认、关闭、重新打开和采用改用已有按钮。经历草稿的精度改用已有下拉,可见文字仍是「记得的精度」。动效用已有的展开预设,只淡入。校正对话仍在首页里,没有为了体积把整段校正拆出首页。盘面探头仍挂在顶栏原来的位置,没有改成弹窗。 +- 验证:见 `docs/tasks/PROGRESS-ui-kit-6-rectification-20261023.md`。没有登录态,不把浏览器里走完一次校正写成已经做过。 +- 防复发:选项卡片的合同仍锁住选中态、不出现「已选择」、不在卡片上抢焦点。白名单测试锁住名单只减不增。入口行为仍由 #103 的合同锁住,本张不改那条路径。 +- 相关记录:issue #110。任务书 `docs/tasks/TASK-ui-kit-migration-20261023.md` 的 K6。校正入口沿用 #103,不另开编号。 +- 复发自:无 +- 修复版本:分支 `codex/ui-kit-6-rectification-20261023`。 + diff --git a/docs/tasks/PROGRESS-ui-kit-6-rectification-20261023.md b/docs/tasks/PROGRESS-ui-kit-6-rectification-20261023.md new file mode 100644 index 00000000..58ffe11b --- /dev/null +++ b/docs/tasks/PROGRESS-ui-kit-6-rectification-20261023.md @@ -0,0 +1,71 @@ +# PROGRESS · 组件库 K6 生时校正界面(2026-10-25) + +工单 #110。分支 `codex/ui-kit-6-rectification-20261023`。基线 `origin/staging` `1516ad56ec66c5e80ef2a1f0ef4d82152b163699`(#109 报告、星历、登录和 #103 校正入口都在这个提交里)。只做任务书 **K6 生时校正界面**。K7 没收尾。没有重做校正入口、报告、登录、星盘页。 + +## 结论 + +校正的选项卡片换成了组件库的单选标签。题目、计分、扣点和保存都没改。明确说要校正的人仍然直接开始,没做完的仍然接着做。 + +## 做了什么 + +- 选项卡片(引导题和对话里的判断卡)改用 `ToggleGroup`,一次只选一个。原来的圆角、浅底描边、题干和选项原文都留着。不写「已选择」,卡片自己也不抢焦点。 +- 「你对这个时间有多确定?」改用 `RadioGroup`,仍是三个圆点。「选一个最接近的范围」改用 `ToggleGroup`。 +- 其余确认、暂停、重试、关闭、重新打开、采用,改用已有的 `Button`。关闭盘面仍是 Lucide 的叉,读屏名仍是「关闭盘面」。没有用字符当图标。 +- 经历草稿的精度改用已有下拉。可见文字仍是「记得的精度」,触发按钮用 `aria-labelledby` 指到这句,方便读屏。对照 `frontend/docs/VOICE.md`:没有新文案,没有改题目。 +- 题目出现时,若 Motion 在这次挂载之前已经载入,用已有的 `disclosure` 预设只淡入。减少动态效果时时长用预设里的 0.15 秒淡入,没有位移。没有新增循环。动效还没载入时不包一层,避免已经在屏幕上的内容再闪一次。 +- 盘面探头仍用原来的 `createPortal` 挂到顶栏,没有改成弹窗。`rectification-agentic-chat.tsx` 因此仍留在白名单里。 +- 白名单去掉 12 个校正文件,没有加长。测试里写死的基线名单没有改。 +- 没有改 `page.tsx`,没有把整段校正拆出首页。`Home()` 的 useState / useRef 没有增长。没有给 `scripts/jyotish_api_server.py` 加方法。没有改 `.gitea/**`,没有改生产部署,没有改数据库,没有升级依赖。 + +移出白名单的 12 个文件:`birth-time-candidate-result.tsx`、`birth-time-choice-question.tsx`、`birth-time-evidence-draft-card.tsx`、`birth-time-guide-turn.tsx`、`birth-time-intake.tsx`、`birth-time-legacy-rectification.tsx`、`birth-time-rectification.tsx`、`rectification-board.tsx`、`rectification-choice-card.tsx`、`rectification-composer-reopen.tsx`、`rectification-range-delivery.tsx`、`rectification-segment-delivery.tsx`。 + +## 断言改动(原值 / 新值 / 原因) + +| 位置 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| `frontend/tests/rectification-agentic-entry.test.ts` 持久化选择题的选项 class | `class="birth-time-choice-option is-primary"` 整段恰好 4 次 | 同一串作为 class 的一部分出现 4 次 | K6 选项改用 ToggleGroup,组件库会合并上自己的类。选项文案和读屏名称不变 | + +选中态源码 `data-selected={selectedKey === option.key ? "true" : "false"}` 没有改。卡片仍不得出现 `Check`、「已选择」和 `.focus(`。 + +## 自测 + +改前先在同一批测试上记过一笔。Windows 上本来就有 21 条失败。改完再跑,失败名单没有变长,通过的没有变少。 + +| 检查 | 命令 | 结果 | +| --- | --- | --- | +| 类型 | 仓库根 `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`,233 个 `rectification-*` / `birth-time-*` 测试文件加 `ui-kit-whitelist.test.ts` | 2291 条。通过 2270,失败 21,取消 0。与改前一致 | +| Python | `.venv\Scripts\python.exe -m pytest`,`tests/test_rectification_*.py` 共 26 个文件 | 退出码 0,进度到 100% | +| 构建 | 在 `frontend` 下 `npm run build` | 退出码 0。编译 10.3s,TypeScript 30.5s。79 个静态页。`/` 仍是 ○ Static。5 条既有 skill-package-registry 警告,没有改 | +| 首屏体积 | 首页 `index.html` 引用的 `/_next/static/**.js`,gzip-9 相加,不含 CSS | 基线是 #109 在 `1516ad56` 记下的 35 个文件、642174 字节。本张 37 个文件,缺失 0,644683 字节,多 2509(+0.39%)。±2% 大约是 629331 到 655017。没有把校正拆出首页 | + +改前就失败、改完仍是这 21 条(没有新增): + +1. public code never imports the internal dynamic choice contract(路径被拼成 `G:\G:\...`) +2. candidate scores stay out of the specified client ownership boundary(同上) +3. A · typed answer to a pending choice(BUG-1045,`getComputedStyle`) +4. A · collect denial with a next choice(BUG-1045) +5. B · failed choice submit 409(BUG-1046,`window is not defined`) +6. B · failed choice submit network error(BUG-1046) +7. B · typed answer that never reaches the server +8. D3 · live focus moves to the latest message(BUG-635) +9. D3 · persisted question with no asked turn +10. D3 · standalone block fallback +11. D4 · willContinue +12. D4 · send() settle +13. C · superseded focus(BUG-1135) +14. C · client guard(BUG-1135) +15. C · stem left in the body(BUG-1135) +16. Skill bump 10.0.32 keeps Cases pinned(BUG-621) +17. D3 · first stage line +18. D3 · turn.rejected +19. opening on the real surface · body only +20. opening on the real surface · body + stem +21. agent pins the dedicated rectification skill and its fixed version + +没有跑前端全量 `npm test`,没有跑 Python 快速门,没有跑 `test:db`。没有为了让测试通过去改计分或落库。 + +## 环境缺口 + +- 没有登录态,没有模型凭据。没有在浏览器里走完一次校正。不写成通过。可照做的步骤在 `docs/testing/ui-kit-migration-20261023.md` 的「生时校正」一节。 diff --git a/docs/testing/ui-kit-migration-20261023.md b/docs/testing/ui-kit-migration-20261023.md index 48a13bf4..9417ed89 100644 --- a/docs/testing/ui-kit-migration-20261023.md +++ b/docs/testing/ui-kit-migration-20261023.md @@ -87,3 +87,14 @@ 2. 走到填写六位验证码时,是六格分开的输入。恢复码仍是一条输入框。 3. 注册账号、忘记密码、返回登录仍是这几个字。没勾同意就继续,仍会提示先阅读并勾选。 4. 打开那一刻的天空。星星仍会汇聚到定格。左上关闭、右上分享仍在。没有重画星星。 + +## 生时校正 + +没有登录态,没有在浏览器里走完一次校正,也没有模型凭据。下面这些步骤留给有账号的人照做。 + +1. 在对话里明确说要校正出生时间。界面直接开始一次新的校正,不再先出一张卡让你点。已经开始、还没做完的,接着上次做,不另开一轮。 +2. 题目仍是原来的句子。选项是圆角卡片,点中后变成浅底描边。读屏能读到题干和选项原文,没有多出来的「已选择」。 +3. 建档或改出生时间时,「你对这个时间有多确定?」仍是三个圆点。选了大概时间之后,「选一个最接近的范围」仍是一排标签。 +4. 校正进行中可以打开当前盘面,再关掉。关闭是一枚叉,读屏名是「关闭盘面」。窄屏上的探头仍在顶栏,不是弹窗。 +5. 系统开了减少动态效果时,新出现的题目只淡入,选项不飞,也没有一直转的动画。 +6. 做完或停下来之后,已经记下的范围还在。扣点和保存跟换皮之前一样。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 5aa31346..f08658c4 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -49,6 +49,8 @@ 不新增等待动画;打开校正沿用既有一次揭幕。 +**选项和按钮换成组件库(2026-10-25,K6)**:选项卡片仍是原来的圆角卡片,点中后浅底描边,读屏仍读题干和选项原文,不另写「已选择」。确定程度仍是三个圆点。大概范围仍是一排标签。盘面关闭仍是一枚叉,探头仍挂在顶栏原来的位置,不是弹窗。其余确认、重新打开和采用用组件库按钮,样子沿用原来的实心、描边和文字按钮。题目出现时,若动效已经载入,只淡入;减少动态效果时同样只淡入,没有位移,也没有循环。题目、计分、扣点和保存没有改。明确要求校正仍直接开始,没做完的接着做。 + **明确要求则直接打开(2026-10-12,BUG-1306)**:用户这一轮明说要做校正时,不再先出卡再点。回答只留一句过渡,界面沿用既有一次揭幕直接打开校正面。没有新的按钮、输入框、等待动画或 Home 状态。推得不准时的主动提议仍用 `.rectification-offer-card`。做不了时只显示一句话,不静默,也不在对话里假装校正。开案还没成功时,同一张提议卡仍是普通提议(不说「已经开始」,按钮仍是「开始核对出生时间」)。开案成功并绑定来源后才换成已开始的文案。打开失败时按钮仍可点。 ## 用量页分段计时(2026-10-05,在途) diff --git a/frontend/eslint/ui-primitive-whitelist.json b/frontend/eslint/ui-primitive-whitelist.json index 9805b463..e0685741 100644 --- a/frontend/eslint/ui-primitive-whitelist.json +++ b/frontend/eslint/ui-primitive-whitelist.json @@ -3,21 +3,9 @@ "src/components/ayanamsa-preference-field.tsx", "src/components/birth-place-picker.tsx", "src/components/birth-sky/birth-sky-converge.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/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", - "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/rectification-agentic-chat.tsx" ] diff --git a/frontend/src/app/birth-time-choice.css b/frontend/src/app/birth-time-choice.css index 1c3998e7..29f9cd42 100644 --- a/frontend/src/app/birth-time-choice.css +++ b/frontend/src/app/birth-time-choice.css @@ -5,14 +5,15 @@ .birth-time-choice-question legend { width: 100%; margin: 0 0 var(--space-3); color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 400; line-height: 1.55; text-wrap: pretty; word-break: auto-phrase; } .birth-time-primary-choices { display: grid; gap: var(--space-2); } .birth-time-special-choices, .birth-time-reframe-actions, .birth-time-journey-controls { display: flex; flex-wrap: wrap; gap: var(--space-2); } -.birth-time-choice-option { min-height: 52px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; font-size: var(--type-body-sm); line-height: 1.5; text-align: left; text-wrap: pretty; word-break: auto-phrase; transition: border-color 120ms ease-out, background-color 120ms ease-out, transform 120ms ease-out; } +.birth-time-choice-option { min-height: 52px; height: auto; justify-content: flex-start; white-space: normal; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; font-size: var(--type-body-sm); font-weight: 400; line-height: 1.5; text-align: left; text-wrap: pretty; word-break: auto-phrase; transition: border-color 120ms ease-out, background-color 120ms ease-out, transform 120ms ease-out; } .birth-time-choice-option.is-primary { width: 100%; padding: var(--space-3) var(--space-4); } -.birth-time-choice-option.is-secondary { min-height: 44px; flex: 1 1 0; padding: var(--space-2) var(--space-4); color: var(--color-ink-secondary); text-align: center; } -.birth-time-choice-option:hover:not(:disabled) { border-color: var(--color-border-strong); background: var(--color-canvas-soft); } +.birth-time-choice-option.is-secondary { min-height: 44px; flex: 1 1 0; justify-content: center; padding: var(--space-2) var(--space-4); color: var(--color-ink-secondary); text-align: center; } +.birth-time-choice-option:hover:not(:disabled) { border-color: var(--color-border-strong); background: var(--color-canvas-soft); color: var(--color-ink); } +.birth-time-choice-option.is-secondary:hover:not(:disabled) { color: var(--color-ink-secondary); } .birth-time-choice-option:active:not(:disabled) { transform: scale(.96); } .birth-time-choice-option[data-selected="true"] { border-color: var(--color-action); background: var(--color-action-soft); color: var(--color-ink); } -.birth-time-choice-option:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; } -.birth-time-choice-option[data-selected="true"]:focus-visible { outline: none; } +.birth-time-choice-option:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; box-shadow: none; border-color: var(--color-border); } +.birth-time-choice-option[data-selected="true"]:focus-visible { outline: none; box-shadow: none; border-color: var(--color-action); } @media (forced-colors: active) { .birth-time-choice-option[data-selected="true"]:focus-visible { outline: 2px solid Highlight; outline-offset: -2px; } } diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 9151f841..34c2843b 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1261,14 +1261,17 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页 .birth-time-source-option { min-height: 64px; display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); cursor: pointer; transition: border-color 120ms ease-out, background-color 120ms ease-out, transform 120ms ease-out; } .birth-time-source-option.is-selected { border-color: var(--color-action); background: var(--color-action-soft); } .birth-time-source-option input { width: 16px; height: 16px; margin: 0; accent-color: var(--color-action); } +.birth-time-source-option [data-slot="radio-group-item"] { width: 16px; height: 16px; margin: 0; accent-color: var(--color-action); } .birth-time-source-option > span { display: grid; gap: 3px; } .birth-time-source-option b { color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 600; } .birth-time-source-option small, .birth-time-detail-note, .birth-time-legacy-note { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.5; } .birth-time-uncertainty-fieldset { margin: var(--space-3) 0 0; padding: 0; border: 0; } .birth-time-uncertainty-fieldset legend { margin-bottom: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 600; } -.birth-time-uncertainty-list { display: flex; flex-wrap: wrap; gap: var(--space-2); } -.birth-time-uncertainty-option { min-height: 44px; padding: 0 var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); font-size: var(--type-body-sm); cursor: pointer; } -.birth-time-uncertainty-option.is-selected { border-color: var(--color-action); background: var(--color-action-soft); } +.birth-time-uncertainty-list { display: flex; flex-wrap: wrap; gap: var(--space-2); width: 100%; } +.birth-time-uncertainty-option { min-height: 44px; height: auto; justify-content: center; white-space: normal; padding: 0 var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 400; cursor: pointer; } +.birth-time-uncertainty-option:hover { background: var(--color-canvas); color: var(--color-ink); } +.birth-time-uncertainty-option.is-selected, +.birth-time-uncertainty-option.is-selected:hover { border-color: var(--color-action); background: var(--color-action-soft); color: var(--color-ink); } .birth-time-detail-grid { display: grid; grid-template-columns: minmax(0, 180px) minmax(0, 1fr); align-items: end; gap: var(--space-3); } .birth-time-detail-grid > label { min-width: 0; } .birth-time-detail-note { padding-bottom: 12px; } @@ -1327,8 +1330,9 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页 .birth-time-guide-composer textarea { min-height: var(--space-24); resize: vertical; line-height: 1.55; } .birth-time-guide-hint, .birth-time-scoring-status p, .birth-time-candidate-terminal p, .birth-time-success-note { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; text-wrap: pretty; word-break: auto-phrase; } .birth-time-guided-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); } -.birth-time-guided-action { min-height: 44px; padding-inline: var(--space-4); font-size: var(--type-body-sm); } +.birth-time-guided-action { min-height: 44px; height: auto; padding-inline: var(--space-4); font-size: var(--type-body-sm); white-space: normal; } .button-text.birth-time-guided-action { border: 0; background: transparent; color: var(--color-action); cursor: pointer; } +.button-text.birth-time-guided-action:hover { background: transparent; color: var(--color-action); } .birth-time-draft-domain { margin: 0; padding: var(--space-3) var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); } .birth-time-draft-domain > div { display: grid; gap: var(--space-1); } .birth-time-draft-domain dt { color: var(--color-ink-tertiary); font-size: var(--type-overline); } @@ -2893,6 +2897,10 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class .birth-time-source-option.is-selected { border-color: var(--color-action); background: var(--color-action-soft); box-shadow: inset 0 0 0 1px var(--color-action); } .birth-time-source-option input { appearance: none; align-self: center; justify-self: center; position: static; box-sizing: border-box; width: 16px; min-width: 16px; max-width: 16px; height: 16px; min-height: 16px; max-height: 16px; margin: 0; overflow: visible; clip: auto; clip-path: none; border: 1.5px solid var(--color-border-strong); border-radius: 50%; background: var(--color-canvas); } .birth-time-source-option input:checked { border-color: var(--color-action); background: var(--color-action-strong); box-shadow: inset 0 0 0 4px var(--color-action-soft); } +.birth-time-source-option [data-slot="radio-group-item"] { appearance: none; align-self: center; justify-self: center; position: static; box-sizing: border-box; width: 16px; min-width: 16px; max-width: 16px; height: 16px; min-height: 16px; max-height: 16px; margin: 0; overflow: visible; clip: auto; clip-path: none; border: 1.5px solid var(--color-border-strong); border-radius: 50%; background: var(--color-canvas); box-shadow: none; } +.birth-time-source-option [data-slot="radio-group-item"][data-checked] { border-color: var(--color-action); background: var(--color-action-strong); box-shadow: inset 0 0 0 4px var(--color-action-soft); } +.birth-time-source-option [data-slot="radio-group-item"]:focus-visible { outline: none; box-shadow: none; } +.birth-time-source-option [data-slot="radio-group-indicator"] { display: none; } .birth-time-source-option > span { gap: 4px; } .birth-time-source-option b { font-size: var(--type-body-sm); line-height: 1.35; } .birth-time-source-option small { font-size: var(--type-caption); line-height: 1.5; } @@ -2906,7 +2914,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class font-size: var(--type-caption); line-height: 1.5; } -.birth-time-window-details .birth-time-skip-button { grid-column: 1 / -1; justify-self: start; min-height: 44px; padding-inline: 13px; border-color: transparent; background: transparent; color: var(--color-action); font-size: var(--type-caption); } +.birth-time-window-details .birth-time-skip-button { grid-column: 1 / -1; justify-self: start; min-height: 44px; height: auto; padding-inline: 13px; border-color: transparent; background: transparent; color: var(--color-action); font-size: var(--type-caption); font-weight: 400; white-space: normal; } .birth-time-window-details .birth-time-skip-button:hover { background: color-mix(in srgb, var(--color-action-soft) 72%, transparent); } .birth-time-detail-grid > label { gap: var(--space-2); } .birth-time-detail-grid > label > span { color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 600; } @@ -3500,6 +3508,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class justify-content: space-between; gap: var(--space-3); width: 100%; + height: auto; min-height: 44px; margin: 0 0 var(--space-2); padding: var(--space-2) var(--space-4); @@ -3507,7 +3516,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink); + font-weight: 400; text-align: start; + white-space: normal; cursor: pointer; } .rectification-board-peek strong { font-size: var(--type-body-sm); } @@ -3805,6 +3816,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class } .rectification-range-delivery__accept { justify-self: stretch; + height: auto; min-height: 44px; padding: 0 12px; border: 1px solid var(--color-border); @@ -3813,8 +3825,13 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class background: var(--color-canvas); font: inherit; font-weight: 600; + white-space: normal; cursor: pointer; } +.rectification-range-delivery__accept:hover { + background: var(--color-canvas); + color: var(--color-action); +} .rectification-range-delivery__accept[aria-pressed="true"] { border-color: color-mix(in srgb, var(--color-action) 55%, var(--color-border)); } diff --git a/frontend/src/components/birth-time-candidate-result.tsx b/frontend/src/components/birth-time-candidate-result.tsx index 369728fb..dd75d069 100644 --- a/frontend/src/components/birth-time-candidate-result.tsx +++ b/frontend/src/components/birth-time-candidate-result.tsx @@ -4,6 +4,7 @@ import type { JourneyClientResponse } from "@/lib/birth-time-journey-client"; import type { BirthTimeGuidedController } from "@/hooks/use-birth-time-guided-journey"; import { guidedTerminalPath } from "@/lib/birth-time-guided-terminal"; import { ErrorToast } from "@/components/error-toast"; +import { Button } from "@/components/ui/button"; type CandidateResultProps = { readonly journey: JourneyClientResponse; @@ -44,7 +45,7 @@ export function BirthTimeCandidateResult({ journey, controller, error }: Candida

尚未进入分钟计算:还没有可评分的关键经历资料。

当前范围仅为填报范围,不是校正结果;请补充跨领域、可注明年月的重大事件后重新评估。

- {terminalPath && } + {terminalPath && }
); } @@ -110,9 +111,9 @@ export function BirthTimeCandidateResult({ journey, controller, error }: Candida {action.kind === "present_medium_result" && winner && !dynamic && (

可以保存候选时间范围,代表时间不会用于排盘。

- +
)} {action.kind === "candidate_saved" && ( @@ -125,15 +126,15 @@ export function BirthTimeCandidateResult({ journey, controller, error }: Candida
确认当前排盘使用时间

候选时间为 {winner.representativeTime}。确认后它会成为当前排盘使用时间,原始填报时间仍会保留。

- +
)} {action.kind === "ready" && (

当前排盘使用时间已更新为 {action.activeTime},原始填报时间仍已保留。

- +
)} {!terminalPath ? : null} @@ -150,7 +151,7 @@ function TerminalAction({ controller, error, path }: {
尚未达到采用条件

候选范围已保留,但当前证据不足以将具体分钟写入当前排盘时间。补充经历后可重新评估。

- + 会建立新的记录,当前结果仍会保留。
diff --git a/frontend/src/components/birth-time-choice-question.tsx b/frontend/src/components/birth-time-choice-question.tsx index f2a3b624..7098dc99 100644 --- a/frontend/src/components/birth-time-choice-question.tsx +++ b/frontend/src/components/birth-time-choice-question.tsx @@ -2,6 +2,9 @@ import { useEffect, useRef, useState } from "react"; import { CharacterRemaining } from "@/components/character-remaining"; +import { RectificationFade } from "@/components/rectification-fade"; +import { Button } from "@/components/ui/button"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { characterRemainingVisible } from "@/lib/character-remaining"; import { choiceQuestionGroups, @@ -62,41 +65,55 @@ export function BirthTimeChoiceQuestion(props: ChoiceQuestionProps) { }; return ( +
{props.question.prompt} -
- {groups.primary.map((option, index) => ( - - ))} -
-
- {[groups.unknown, groups.unmatched].map((option) => ( - - ))} -
+ { + const id = next[0]; + if (!id || id === selectedId) return; + const option = [...groups.primary, groups.unknown, groups.unmatched].find((item) => item.optionId === id); + if (option) select(option); + }} + > +
+ {groups.primary.map((option, index) => ( + + {option.label} + + ))} +
+
+ {[groups.unknown, groups.unmatched].map((option) => ( + + {option.label} + + ))} +
+
{props.pending &&

正在缩小候选范围…

}
+
); } @@ -111,6 +128,7 @@ export function BirthTimeUnmatchedClarification(props: ClarificationProps) { } }, [props.error, props.pending, props.progress.answeredCount]); return ( +
@@ -129,13 +147,14 @@ export function BirthTimeUnmatchedClarification(props: ClarificationProps) { />
- - + +
{props.pending &&

正在换一道更合适的问题…

}
+
); } @@ -143,8 +162,8 @@ function JourneyControls(props: SharedQuestionProps) { return ( <>
- - + +
diff --git a/frontend/src/components/birth-time-evidence-draft-card.tsx b/frontend/src/components/birth-time-evidence-draft-card.tsx index 497db8e6..0c62cabc 100644 --- a/frontend/src/components/birth-time-evidence-draft-card.tsx +++ b/frontend/src/components/birth-time-evidence-draft-card.tsx @@ -1,6 +1,8 @@ "use client"; -import { useState } from "react"; +import { useId, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { lifeEventSchema } from "@/lib/birth-time-evidence"; import type { EvidenceDraft } from "@/lib/birth-time-journey-turn"; import type { EvidenceDatePrecision, LegacyGuidedEvidenceDomain } from "@/lib/birth-time-question-planner"; @@ -21,12 +23,8 @@ type DraftCardProps = { readonly onSkip: () => void; }; -function parsePrecision(value: string): EvidenceDatePrecision { - if (value === "year" || value === "month" || value === "day") return value; - throw new TypeError("Unsupported evidence date precision"); -} - export function BirthTimeEvidenceDraftCard(props: DraftCardProps) { + const precisionLabelId = useId(); const [precision, setPrecision] = useState(props.draft.precision ?? "year"); const [date, setDate] = useState(props.draft.date ?? ""); const isValid = lifeEventSchema.safeParse({ @@ -43,12 +41,25 @@ export function BirthTimeEvidenceDraftCard(props: DraftCardProps) {
经历领域
{domainLabels[props.draft.domain]}
{!isValid &&

请填写与所选精度一致的有效日期;不会自动补猜缺失时间。

}
- - + +
); diff --git a/frontend/src/components/birth-time-guide-turn.tsx b/frontend/src/components/birth-time-guide-turn.tsx index fa5c9c46..0785655b 100644 --- a/frontend/src/components/birth-time-guide-turn.tsx +++ b/frontend/src/components/birth-time-guide-turn.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { CharacterRemaining } from "@/components/character-remaining"; +import { Button } from "@/components/ui/button"; import { characterRemainingVisible } from "@/lib/character-remaining"; import type { JourneyProgress } from "@/lib/birth-time-journey-turn"; import { protectOnboardingPhrases } from "@/lib/onboarding-copy"; @@ -57,11 +58,11 @@ export function BirthTimeGuideTurn(props: BirthTimeGuideTurnProps) {

Enter 发送,Shift+Enter 换行。说出大概年份也可以,不需要为了校正而猜测。

- - - + + +
); diff --git a/frontend/src/components/birth-time-intake.tsx b/frontend/src/components/birth-time-intake.tsx index 63ffd73a..9e1612cf 100644 --- a/frontend/src/components/birth-time-intake.tsx +++ b/frontend/src/components/birth-time-intake.tsx @@ -2,7 +2,10 @@ import { useId, useState } from "react"; import { BirthDatePicker } from "@/components/birth-date-picker"; +import { Button } from "@/components/ui/button"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { birthTimeConsultationOptionsCopy } from "@/lib/birth-time-consultation-consent"; import { approximateUncertaintyOptions, @@ -83,7 +86,8 @@ export function BirthTimeIntakeFields({ value, onPatch, lockReportedTime = false if (lockReportedTime) return; onPatch(patch); }; - const groupId = useId(); + const sourceLegendId = useId(); + const uncertaintyLegendId = useId(); const source = value.birthTimeSource; const isConfirmed = value.birthTimeStatus === "confirmed"; const displayState = birthTimeDisplayState(value); @@ -143,33 +147,36 @@ export function BirthTimeIntakeFields({ value, onPatch, lockReportedTime = false )} {!isConfirmed &&
- 你对这个时间有多确定? + 你对这个时间有多确定?

只用在这里问一次。校正会按你选的范围去比,不再追问你有多确定。

-
+ { + const option = birthTimeSourceOptions.find((item) => item.value === next); + if (!option) return; + emitTime({ + birthTimeSource: option.value, + birthTimeStatus: "reported", + time: "", + ...birthTimeSourceDefaults[option.value], + }); + }} + > {birthTimeSourceOptions.map((option) => ( ))} -
+
} {usesClockTime && ( @@ -187,28 +194,38 @@ export function BirthTimeIntakeFields({ value, onPatch, lockReportedTime = false )} {(source === "approximate" || source === "family_exact") && (
- 选一个最接近的范围 -
+ 选一个最接近的范围 + { + const minutes = Number(next[0]); + const option = approximateUncertaintyOptions.find((item) => item.minutes === minutes); + if (!option) return; + emitTime( + option.minutes === 15 && source === "family_exact" + ? { + uncertaintyBeforeMinutes: 0, + uncertaintyAfterMinutes: 0, + } + : { + birthTimeSource: "approximate", + uncertaintyBeforeMinutes: option.minutes, + uncertaintyAfterMinutes: option.minutes, + }, + ); + }} + > {approximateUncertaintyOptions.map((option) => ( - + value={String(option.minutes)} + >{option.label} ))} -
+
)} @@ -239,9 +256,10 @@ export function BirthTimeIntakeFields({ value, onPatch, lockReportedTime = false onChange={(declaredWindowEnd) => emitTime({ declaredWindowEnd })} /> - + >完全不清楚,跳过出生时间 )} diff --git a/frontend/src/components/birth-time-legacy-rectification.tsx b/frontend/src/components/birth-time-legacy-rectification.tsx index 7309b902..dd414311 100644 --- a/frontend/src/components/birth-time-legacy-rectification.tsx +++ b/frontend/src/components/birth-time-legacy-rectification.tsx @@ -9,6 +9,7 @@ import type { LegacyJourneyClientResponse } from "@/lib/birth-time-journey-respo import { guidedTurnIdentity } from "@/lib/birth-time-guided-turn-identity"; import type { NextAction } from "@/lib/birth-time-journey-turn"; import { ErrorToast } from "@/components/error-toast"; +import { Button } from "@/components/ui/button"; type LegacyRectificationProps = { readonly journey: LegacyJourneyClientResponse; @@ -75,8 +76,8 @@ export function BirthTimeLegacyRectification(props: LegacyRectificationProps) { /> )} {action.kind === "score_pending" &&

正在使用已确认的关键经历评分…

} - {action.kind === "retry_scoring" && } - {action.kind === "paused" && } + {action.kind === "retry_scoring" && } + {action.kind === "paused" && } {showsCandidate ? ( )} {(action.kind === "generate_dynamic_question" || action.kind === "retry_question_generation") && error && ( - + )} {action.kind === "score_pending" && (
正在缩小候选范围… - {props.controller.pollRecoverable && } + {props.controller.pollRecoverable && }
)} - {action.kind === "retry_scoring" && } - {action.kind === "paused" && } + {action.kind === "retry_scoring" && } + {action.kind === "paused" && } {showsCandidate ? ( {emptyCopy.clock} : null} {compact && ( - + )} {stage ?

{engineMeaningToDisplayCopy(stage)}

: null} @@ -338,9 +340,10 @@ export function RectificationBoardPeek({ }>) { const peek = rectificationBoardPeekCopy(result, declaredTime, birthTimeSource); return ( - + ); } diff --git a/frontend/src/components/rectification-choice-card.tsx b/frontend/src/components/rectification-choice-card.tsx index 32a540b5..60dc7da3 100644 --- a/frontend/src/components/rectification-choice-card.tsx +++ b/frontend/src/components/rectification-choice-card.tsx @@ -2,6 +2,8 @@ import { useState } from "react"; import { InlineSpinner } from "@/components/inline-spinner"; +import { RectificationFade } from "@/components/rectification-fade"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { CHOICE_STOP_LABEL, type RectificationChoiceCard as ChoiceCard, @@ -40,6 +42,7 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) { const hidePrompt = embedded || props.hidePrompt === true; return ( +
{props.card.why_user}

) : null} -
- {props.card.options.map((option) => ( - - ))} - {props.card.stop_label !== CHOICE_STOP_LABEL ? ( - - ) : null} -
+ { + const key = next[0]; + if (!key || key === selectedKey) return; + if (key === "stop") { + stop(); + return; + } + if (props.card.options.some((option) => option.key === key)) select(key as ChoiceKey); + }} + > +
+ {props.card.options.map((option) => ( + + {option.key}. {option.label} + + ))} + {props.card.stop_label !== CHOICE_STOP_LABEL ? ( + + {props.card.stop_label} + + ) : null} +
+
+
); } diff --git a/frontend/src/components/rectification-composer-reopen.tsx b/frontend/src/components/rectification-composer-reopen.tsx index 247dacdb..81a0d383 100644 --- a/frontend/src/components/rectification-composer-reopen.tsx +++ b/frontend/src/components/rectification-composer-reopen.tsx @@ -1,3 +1,4 @@ +import { Button } from "@/components/ui/button"; import { RECTIFICATION_REOPEN_LABEL } from "@/lib/rectification-session-composer-guard"; export function RectificationComposerReopen({ @@ -10,9 +11,9 @@ export function RectificationComposerReopen({ return (

{message}

- +
); } diff --git a/frontend/src/components/rectification-fade.tsx b/frontend/src/components/rectification-fade.tsx new file mode 100644 index 00000000..59b2f1c1 --- /dev/null +++ b/frontend/src/components/rectification-fade.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useEffect, useState, type ReactNode } from "react"; + +import { loadDomAnimation, loadMotionReact, peekMotionReact, type MotionReact } from "@/lib/motion/load-react"; +import { motionPresets } from "@/lib/motion/presets"; + +function useMotionReact(): MotionReact | null { + const [mod, setMod] = useState(() => peekMotionReact()); + useEffect(() => { + if (mod) return; + let live = true; + void loadMotionReact().then((next) => { + if (live) setMod(next); + }, () => undefined); + return () => { + live = false; + }; + }, [mod]); + return mod; +} + +/** + * Fade once, and only when Motion was already loaded before this mount. + * Otherwise return the children unchanged so a late chunk does not wrap + * content that is already on screen. Reduced motion stays a short fade. + */ +export function RectificationFade({ children }: { readonly children: ReactNode }) { + const [readyAtMount] = useState(() => peekMotionReact() !== null); + const motion = useMotionReact(); + if (!readyAtMount || motion === null) return children; + return {children}; +} + +function FadeIn({ motion, children }: { readonly motion: MotionReact; readonly children: ReactNode }) { + const reduced = motion.useReducedMotion(); + const preset = motionPresets.disclosure; + const duration = reduced ? preset.reduced.duration : preset.duration; + return ( + + + {children} + + + ); +} diff --git a/frontend/src/components/rectification-range-delivery.tsx b/frontend/src/components/rectification-range-delivery.tsx index 4c872a53..bdace93a 100644 --- a/frontend/src/components/rectification-range-delivery.tsx +++ b/frontend/src/components/rectification-range-delivery.tsx @@ -17,6 +17,7 @@ import { import { RectificationVerificationReport } from "@/components/rectification-verification-report"; import { RectificationSegmentDelivery } from "@/components/rectification-segment-delivery"; import { sessionOutcomeAllowsAdopt } from "@/lib/rectification-agentic/core/rectification-decision"; +import { Button } from "@/components/ui/button"; import { deliveryWindowFromReceipt, protectVargaReport } from "@/lib/rectification-agentic/v9/delivery-window"; @@ -159,8 +160,9 @@ export function RectificationRangeDelivery({ )} {adoptAllowed && !adoptLocked ? ( <> - {recordConflict && !adopted ? (

{switchToRectifiedConfirmCopy(recordConflict.reported_time, column.time)} diff --git a/frontend/src/components/rectification-segment-delivery.tsx b/frontend/src/components/rectification-segment-delivery.tsx index e5423b20..42be37a7 100644 --- a/frontend/src/components/rectification-segment-delivery.tsx +++ b/frontend/src/components/rectification-segment-delivery.tsx @@ -7,6 +7,7 @@ import { SWITCH_TO_RECTIFIED_LABEL } from "@/lib/rectification-agentic/birth-tim import { RectificationVerificationReport } from "@/components/rectification-verification-report"; import { protectVargaReport } from "@/lib/rectification-agentic/v9/delivery-window"; import { unknownProductDeliveryCopy } from "@/lib/rectification-agentic/v9/product-delivery"; +import { Button } from "@/components/ui/button"; export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, selection, resultId, busy, accepting, onAccept, adoptLocked, markdown, candidateDate, canAdopt = false }: Readonly<{ summary: SegmentSummary | null; @@ -57,12 +58,12 @@ export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, se {showAdopt && adoption && candidateId ? ( <>

采用后按 {adoption.date ? `${adoption.date} ` : ""}{adoption.time} 排盘;原填报资料保留。

- + {readopt && priorAdoption && !adopted ? (

现在按 {priorAdoption.time} 排盘;改用后按 {readopt} 排盘,原填报资料保留。

) : null} diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index 315a73b9..d063a65f 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -786,7 +786,10 @@ test("time-selection cards use server adoption state and stay mutually exclusive })); assert.match(persistedHtml, /data-testid="persisted-question"/); assert.match(persistedHtml, /class="rectification-choice-card is-embedded"/); - assert.equal(persistedHtml.match(/class="birth-time-choice-option is-primary"/g)?.length, 4); + // 原值: class="birth-time-choice-option is-primary" 整段 class 恰好出现 4 次。 + // 新值: 同一串作为 class 的一部分出现 4 次。 + // 原因: K6 选项卡片改用 ToggleGroup,组件库会合并上自己的类。选项文案和读屏名称不变。 + assert.equal(persistedHtml.match(/birth-time-choice-option is-primary/g)?.length, 4); assert.doesNotMatch(messageLoop, /rectification-question-slot/); assert.doesNotMatch(persistedHtml, /rectification-question-slot/); assert.doesNotMatch(messageEntry, /rectification-question-slot/);