fix(rectification): show mobile natal board as a covering bottom sheet
BUG-330: the compact board sat in the same layer as the composer, so peek copy overlapped the sheet title. Overlay only below 768px; desktop stays a side column. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+18
-2
@@ -4950,11 +4950,27 @@
|
||||
- 根因:窄屏用 `showModal()` 对话框加不透明 backdrop,并把聊天设为 `inert`;高度约 86dvh / 100dvh,聊天被遮死。
|
||||
- 修复:窄屏改为工作区分栏下半屏 sheet(约 46% 高),聊天留在上半屏可继续阅读;关闭改为 44px 图标按钮,Escape 仍可关闭。
|
||||
- 验证:`frontend/tests/rectification-agentic-entry.test.ts`。
|
||||
- 防复发:窄屏盘面不得再用 modal dialog / backdrop 盖住聊天;关闭控件必须是独立 44px 目标并带 `aria-label="关闭盘面"`。
|
||||
- 相关记录:BUG-322、BUG-326
|
||||
- 防复发:关闭控件必须是独立 44px 目标并带 `aria-label="关闭盘面"`。窄屏分层策略以 BUG-330 为准:必须用高于输入框的覆盖层,不得再与输入框做同层网格分栏。
|
||||
- 相关记录:BUG-322、BUG-326、BUG-330
|
||||
- 复发自:BUG-322(分栏后窄屏改成对话框,聊天被挡住)
|
||||
- 修复版本:994f0583
|
||||
|
||||
## BUG-330 | 移动端盘面与输入框同层叠字,无法作为弹出层使用
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-20
|
||||
- 最近更新:2026-08-20
|
||||
- 影响面:生时纠正窄屏盘面、`rectification-board`、输入框
|
||||
- 用户现象:点开“当前盘面”后,盘面标题与输入区“当前盘面”文案叠在一起;宫位表像嵌在输入框同一层,而不是从下往上盖住聊天列表和输入框。
|
||||
- 触发条件:窄于分栏阈值的视口打开宫位表。
|
||||
- 根因:BUG-328 把窄屏盘面改成工作区第二行(约 46% 高)且未设叠层;输入框 `.composer-wrap` 仍是 `position: relative; z-index: 2`。盘面与输入框在同一工作区网格里重叠时,输入框画在盘面标题之上。
|
||||
- 修复:仅在视口宽度小于 768px 的移动端,把盘面改成工作区内 `z-index: 20` 的底部 sheet 覆盖层;桌面网页仍是右侧分栏,不出现底部弹出。遮罩点击、Escape、44px 关闭按钮可关;聊天列在打开时 `inert`。
|
||||
- 验证:`frontend/tests/rectification-agentic-entry.test.ts`。
|
||||
- 防复发:底部 sheet 只允许 `matchMedia(max-width: 767px)` 触发,不得用聊天区 `clientWidth` 把桌面侧栏挤窄误判成移动端;桌面必须保持 `grid-template-columns: minmax(0, 1fr) minmax(18rem, 22.5rem)`。窄屏盘面必须是覆盖层(absolute、高于输入框的 z-index、不透明背景)。
|
||||
- 相关记录:BUG-322、BUG-326、BUG-328
|
||||
- 复发自:BUG-328(当时为了露出聊天改成下半屏分栏,叠层低于输入框)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-329 | 生时纠正 Agent 回答在结算后一次性出现,推理中无法停止
|
||||
|
||||
- 状态:resolved
|
||||
|
||||
@@ -322,6 +322,7 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
@keyframes onboarding-caret { 50% { opacity: 0; } }
|
||||
@keyframes account-overlay-enter { from { opacity: 0; } }
|
||||
@keyframes account-dialog-enter { from { opacity: 0; transform: translateY(var(--space-1)) scale(.985); } }
|
||||
@keyframes rectification-sheet-enter { from { opacity: 0; transform: translateY(18%); } }
|
||||
|
||||
@media (hover: hover) {
|
||||
.session-list button:not(:disabled):hover, .profile-trigger:not(:disabled):hover { background: var(--color-light-hover); color: var(--color-ink); }
|
||||
@@ -1874,6 +1875,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.birth-time-clock-menu .select-item { justify-content: flex-start; }
|
||||
|
||||
.rectification-workspace {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
@@ -1883,9 +1885,11 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
}
|
||||
.rectification-workspace.is-compact {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
}
|
||||
.rectification-workspace.is-compact.is-board-open {
|
||||
grid-template-rows: minmax(8rem, 1fr) minmax(13rem, 46%);
|
||||
.rectification-workspace.is-compact .rectification-workspace__chat,
|
||||
.rectification-workspace.is-compact .rectification-board-overlay {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
.rectification-workspace__chat {
|
||||
min-width: 0;
|
||||
@@ -1913,13 +1917,42 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
background: var(--color-canvas-soft);
|
||||
color: var(--color-ink);
|
||||
}
|
||||
.rectification-workspace.is-compact .rectification-board-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
align-items: end;
|
||||
isolation: isolate;
|
||||
}
|
||||
.rectification-board-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 0;
|
||||
background: var(--color-scrim);
|
||||
cursor: pointer;
|
||||
animation: account-overlay-enter 180ms ease-out both;
|
||||
}
|
||||
.rectification-board.is-sheet {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 92%;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-inline-start: 0;
|
||||
border-block-start: 1px solid var(--color-border);
|
||||
border-start-start-radius: var(--radius-lg);
|
||||
border-start-end-radius: var(--radius-lg);
|
||||
box-shadow: 0 -8px 28px color-mix(in srgb, var(--color-ink) 10%, transparent);
|
||||
background: var(--color-canvas);
|
||||
box-shadow: 0 -12px 32px color-mix(in srgb, var(--color-ink) 12%, transparent);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
animation: rectification-sheet-enter 280ms var(--ease-out) both;
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
}
|
||||
.rectification-board.is-sheet .rectification-board__header {
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
.rectification-board__handle {
|
||||
width: 36px;
|
||||
|
||||
@@ -266,17 +266,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const boardTitleId = useId();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const node = workspace.current;
|
||||
if (!node) return;
|
||||
const query = window.matchMedia(`(max-width: ${RECTIFICATION_BOARD_SPLIT_MIN_PX - 1}px)`);
|
||||
const update = () => {
|
||||
const nextCompact = node.clientWidth < RECTIFICATION_BOARD_SPLIT_MIN_PX;
|
||||
const nextCompact = query.matches;
|
||||
setCompactBoard(nextCompact);
|
||||
if (!nextCompact) setBoardOpen(false);
|
||||
};
|
||||
update();
|
||||
const observer = new ResizeObserver(update);
|
||||
observer.observe(node);
|
||||
return () => observer.disconnect();
|
||||
query.addEventListener("change", update);
|
||||
return () => query.removeEventListener("change", update);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -689,7 +687,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
ref={workspace}
|
||||
className={`rectification-workspace${compactBoard ? " is-compact" : ""}${boardOpen ? " is-board-open" : ""}`}
|
||||
>
|
||||
<div className="rectification-workspace__chat">
|
||||
<div className="rectification-workspace__chat" inert={compactBoard && boardOpen ? true : undefined}>
|
||||
<section ref={conversation} className="conversation is-rectification" aria-label="生时校正对话" aria-busy={busy || regeneratingMessageKey !== null}>
|
||||
<div className="message-list">
|
||||
{pendingConsultationQuestion?.trim() && (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
groupWindowTransitions,
|
||||
rectificationBoardPeekCopy,
|
||||
@@ -210,8 +210,11 @@ export function RectificationBoard({
|
||||
titleId: string;
|
||||
onClose: () => void;
|
||||
}>) {
|
||||
const sheetRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!compact || !open) return;
|
||||
sheetRef.current?.focus();
|
||||
const handleKey = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") onClose();
|
||||
};
|
||||
@@ -221,11 +224,15 @@ export function RectificationBoard({
|
||||
|
||||
if (compact && !open) return null;
|
||||
|
||||
return (
|
||||
const sheet = (
|
||||
<aside
|
||||
ref={sheetRef}
|
||||
id={boardId}
|
||||
className={`rectification-board${compact ? " is-sheet" : ""}`}
|
||||
aria-labelledby={titleId}
|
||||
role={compact ? "dialog" : undefined}
|
||||
aria-modal={compact ? true : undefined}
|
||||
tabIndex={compact ? -1 : undefined}
|
||||
>
|
||||
<RectificationBoardBody
|
||||
result={result}
|
||||
@@ -237,6 +244,15 @@ export function RectificationBoard({
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
|
||||
if (!compact) return sheet;
|
||||
|
||||
return (
|
||||
<div className="rectification-board-overlay">
|
||||
<div className="rectification-board-scrim" aria-hidden="true" onClick={onClose} />
|
||||
{sheet}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RectificationBoardPeek({
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type RectificationHouseTable,
|
||||
} from "./rectification-candidate-result";
|
||||
|
||||
export const RECTIFICATION_BOARD_SPLIT_MIN_PX = 832;
|
||||
export const RECTIFICATION_BOARD_SPLIT_MIN_PX = 768;
|
||||
|
||||
export type GroupedWindowMinute = Readonly<{
|
||||
at: string;
|
||||
|
||||
@@ -28,6 +28,10 @@ const board = readFileSync(
|
||||
new URL("../src/components/rectification-board.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const boardModel = readFileSync(
|
||||
new URL("../src/lib/rectification-board-model.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const houseTable = readFileSync(
|
||||
new URL("../src/components/rectification-house-table.tsx", import.meta.url),
|
||||
"utf8",
|
||||
@@ -331,6 +335,11 @@ test("candidate state renders from the snapshot API and never from sentinels", (
|
||||
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/);
|
||||
assert.match(styles, /\.rectification-workspace \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) minmax\(18rem, 22\.5rem\)/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact \{[\s\S]*grid-template-columns: minmax\(0, 1fr\)/);
|
||||
assert.match(chat, /matchMedia\(`\(max-width: \$\{RECTIFICATION_BOARD_SPLIT_MIN_PX - 1\}px\)`\)/);
|
||||
assert.match(chat, /nextCompact = query\.matches/);
|
||||
assert.doesNotMatch(chat, /clientWidth < RECTIFICATION_BOARD_SPLIT_MIN_PX/);
|
||||
assert.match(boardModel, /RECTIFICATION_BOARD_SPLIT_MIN_PX = 768/);
|
||||
assert.match(board, /if \(!compact\) return sheet;/);
|
||||
assert.doesNotMatch(styles, /\.rectification-snapshot \{[^}]*--assistant-content-inset/);
|
||||
assert.doesNotMatch(styles, /\.rectification-house-table \{[^}]*padding-inline-start:\s*var\(--assistant-content-inset\)/);
|
||||
assert.doesNotMatch(styles, /\.rectification-house-table \{[^}]*--assistant-content-inset/);
|
||||
@@ -369,17 +378,28 @@ test("the natal house table is a live board beside the chat, not a message", ()
|
||||
assert.match(board, /event\.key === "Escape"/);
|
||||
assert.match(board, /aria-label="关闭盘面"/);
|
||||
assert.match(board, /className="rectification-board__handle"/);
|
||||
assert.match(board, /className="rectification-board-overlay"/);
|
||||
assert.match(board, /className="rectification-board-scrim"/);
|
||||
assert.match(board, /role=\{compact \? "dialog" : undefined\}/);
|
||||
assert.match(board, /aria-modal=\{compact \? true : undefined\}/);
|
||||
assert.match(chat, /RectificationBoardPeek/);
|
||||
assert.match(chat, /onOpen=\{toggleBoard\}/);
|
||||
assert.doesNotMatch(chat, /inert=\{compactBoard && boardOpen/);
|
||||
assert.match(chat, /inert=\{compactBoard && boardOpen \? true : undefined\}/);
|
||||
assert.match(houseTable, /当前本命宫位/);
|
||||
assert.match(houseTable, /补充经历后会按新线索重算/);
|
||||
assert.doesNotMatch(houseTable, /<ChatMessageRow/);
|
||||
});
|
||||
|
||||
test("compact board shares the viewport with chat and uses an icon close control", () => {
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact\.is-board-open \{[\s\S]*grid-template-rows: minmax\(8rem, 1fr\) minmax\(13rem, 46%\)/);
|
||||
test("compact board overlays chat as a bottom sheet above the composer", () => {
|
||||
assert.doesNotMatch(styles, /\.rectification-workspace\.is-compact\.is-board-open \{[\s\S]*grid-template-rows: minmax\(8rem, 1fr\) minmax\(13rem, 46%\)/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[\s\S]*position: absolute/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[\s\S]*inset: 0/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[\s\S]*z-index: 20/);
|
||||
assert.match(styles, /\.rectification-workspace__chat \.composer-wrap \{[\s\S]*z-index: 2/);
|
||||
assert.match(styles, /\.rectification-board\.is-sheet \{[\s\S]*height: 92%/);
|
||||
assert.match(styles, /\.rectification-board\.is-sheet \{[\s\S]*background: var\(--color-canvas\)/);
|
||||
assert.match(styles, /\.rectification-board\.is-sheet \{[\s\S]*border-block-start: 1px solid var\(--color-border\)/);
|
||||
assert.match(styles, /@keyframes rectification-sheet-enter \{[\s\S]*translateY\(18%\)/);
|
||||
assert.doesNotMatch(styles, /dialog\.rectification-board/);
|
||||
assert.match(styles, /\.rectification-board__close \{[\s\S]*width: 44px/);
|
||||
assert.match(styles, /\.rectification-board__close \{[\s\S]*min-height: 44px/);
|
||||
|
||||
Reference in New Issue
Block a user