fix(web): keep onboarding and mobile natal board on one aligned overlay
Put the compact board trigger in the chat header, keep the sheet above the composer on iOS, and align onboarding messages with the intake card on the session column. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1763,8 +1763,24 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.onboarding-card-heading b { font-size: clamp(22px, 2.6vw, 30px); letter-spacing: -.03em; }
|
||||
.onboarding-card-heading small { max-width: 260px; color: var(--color-ink-secondary); line-height: 1.55; font-size: var(--type-caption); text-wrap: pretty; }
|
||||
.birth-time-intake { gap: var(--space-6); }
|
||||
.conversation.is-onboarding .welcome {
|
||||
box-sizing: border-box;
|
||||
width: min(calc(var(--session-column-width) + (var(--session-column-gutter) * 2)), 100%);
|
||||
margin-inline: auto;
|
||||
}
|
||||
.conversation.is-onboarding.is-empty .welcome {
|
||||
width: min(var(--session-column-width), 100%);
|
||||
}
|
||||
.conversation.is-onboarding .onboarding-card,
|
||||
.conversation.is-onboarding .onboarding-card.birth-time-transition-card {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
}
|
||||
.conversation.is-onboarding-form { align-content: start; padding-bottom: var(--space-8); }
|
||||
.conversation.is-onboarding-form .welcome { padding-top: var(--space-6); padding-bottom: var(--space-8); }
|
||||
.conversation.is-onboarding-form .welcome {
|
||||
padding: var(--space-6) var(--session-column-gutter) var(--space-8);
|
||||
}
|
||||
.conversation.is-onboarding-form .welcome > .onboarding-message:first-child { padding-bottom: var(--space-3); }
|
||||
.conversation.is-onboarding-form .welcome > .onboarding-message:first-child .message-bubble p {
|
||||
max-width: 680px;
|
||||
@@ -1876,6 +1892,8 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
|
||||
.rectification-workspace {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
isolation: isolate;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
@@ -1908,6 +1926,12 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.rectification-workspace.is-compact.is-board-open .rectification-workspace__chat .composer-wrap {
|
||||
z-index: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.rectification-board {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
@@ -1920,10 +1944,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.rectification-workspace.is-compact .rectification-board-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
z-index: 50;
|
||||
display: grid;
|
||||
align-items: end;
|
||||
isolation: isolate;
|
||||
}
|
||||
.rectification-board-scrim {
|
||||
position: absolute;
|
||||
@@ -1937,7 +1960,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 92%;
|
||||
height: 72%;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
@@ -2102,6 +2125,33 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
color: var(--color-ink);
|
||||
font-size: var(--type-caption);
|
||||
}
|
||||
.chat-header-rectification:empty {
|
||||
display: none;
|
||||
}
|
||||
.chat-header-rectification .rectification-board-peek {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: auto;
|
||||
max-width: min(11.5rem, 42vw);
|
||||
min-height: 44px;
|
||||
margin: 0;
|
||||
padding: 0 var(--space-3);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.chat-header-rectification .rectification-board-peek strong,
|
||||
.chat-header-rectification .rectification-board-peek span {
|
||||
display: inline;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.chat-header-rectification .rectification-board-peek span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.rectification-board-peek {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3559,6 +3559,7 @@ export default function Home() {
|
||||
<strong>{activeSession?.title || "新对话"}</strong>
|
||||
</div>
|
||||
<div className="chat-header-actions">
|
||||
<div className="chat-header-rectification" data-rectification-header-slot="" />
|
||||
<button className="credit-button" type="button" onClick={() => router.push(membershipHref("credits"))} aria-label={account ? `余额 ${account.credits} 点,会员与充值` : accountError || "读取余额中"}>
|
||||
<Sparkles className="credit-icon" aria-hidden="true" />
|
||||
<span>{account ? account.credits : "—"}</span>
|
||||
@@ -3567,7 +3568,7 @@ export default function Home() {
|
||||
</header>
|
||||
|
||||
{!rectificationSurfaceOpen && (
|
||||
<div ref={conversation} className={`conversation ${!activeSession?.messages.length && !onboardingFormActive ? "is-empty" : ""} ${onboardingFormActive ? "is-onboarding-form" : ""}`}>
|
||||
<div ref={conversation} className={`conversation${!activeSession?.messages.length && !onboardingFormActive ? " is-empty" : ""}${!profileComplete ? " is-onboarding" : ""}${onboardingFormActive ? " is-onboarding-form" : ""}`}>
|
||||
{!activeSession?.messages.length ? (
|
||||
<div className="welcome">
|
||||
{!profileComplete ? (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ArrowUp, Square } from "lucide-react";
|
||||
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { parseAgentReply } from "@/lib/agent-reply";
|
||||
import type { ChatMessage, ChatMessageView } from "@/lib/chat-message-view";
|
||||
import {
|
||||
@@ -262,9 +263,14 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const [compactBoard, setCompactBoard] = useState(false);
|
||||
const [boardOpen, setBoardOpen] = useState(false);
|
||||
const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null));
|
||||
const [headerSlot, setHeaderSlot] = useState<HTMLElement | null>(null);
|
||||
const boardId = useId();
|
||||
const boardTitleId = useId();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setHeaderSlot(document.querySelector("[data-rectification-header-slot]"));
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const query = window.matchMedia(`(max-width: ${RECTIFICATION_BOARD_SPLIT_MIN_PX - 1}px)`);
|
||||
const update = () => {
|
||||
@@ -682,11 +688,21 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
: undefined;
|
||||
const canSend = !busy && !readonly && !regeneratingMessageKey;
|
||||
|
||||
const boardPeek = compactBoard && !boardOpen ? (
|
||||
<RectificationBoardPeek
|
||||
result={candidateResult}
|
||||
expanded={boardOpen}
|
||||
boardId={boardId}
|
||||
onOpen={toggleBoard}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={workspace}
|
||||
className={`rectification-workspace${compactBoard ? " is-compact" : ""}${boardOpen ? " is-board-open" : ""}`}
|
||||
>
|
||||
{headerSlot && boardPeek ? createPortal(boardPeek, headerSlot) : null}
|
||||
<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">
|
||||
@@ -777,14 +793,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
</section>
|
||||
|
||||
<div className="composer-wrap">
|
||||
{compactBoard && (
|
||||
<RectificationBoardPeek
|
||||
result={candidateResult}
|
||||
expanded={boardOpen}
|
||||
boardId={boardId}
|
||||
onOpen={toggleBoard}
|
||||
/>
|
||||
)}
|
||||
<form className="composer" onSubmit={submit}>
|
||||
<Textarea
|
||||
ref={composer}
|
||||
|
||||
@@ -271,6 +271,7 @@ export function RectificationBoardPeek({
|
||||
<button
|
||||
className="rectification-board-peek"
|
||||
type="button"
|
||||
aria-label={peek.detail ? `${peek.title},${peek.detail}` : peek.title}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={boardId}
|
||||
onClick={onOpen}
|
||||
|
||||
@@ -392,11 +392,17 @@ test("the natal house table is a live board beside the chat, not a message", ()
|
||||
|
||||
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 \{[\s\S]*isolation: isolate/);
|
||||
assert.match(styles, /\.rectification-workspace \{[\s\S]*z-index: 0/);
|
||||
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\.is-compact \.rectification-board-overlay \{[^}]*z-index: 50/);
|
||||
assert.doesNotMatch(styles, /\.rectification-workspace\.is-compact \.rectification-board-overlay \{[^}]*z-index: 20/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact\.is-board-open \.rectification-workspace__chat \.composer-wrap \{[\s\S]*visibility: hidden/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-compact\.is-board-open \.rectification-workspace__chat \.composer-wrap \{[\s\S]*backdrop-filter: none/);
|
||||
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 \{[^}]*height: 72%/);
|
||||
assert.doesNotMatch(styles, /\.rectification-board\.is-sheet \{[^}]*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%\)/);
|
||||
@@ -404,6 +410,14 @@ test("compact board overlays chat as a bottom sheet above the composer", () => {
|
||||
assert.match(styles, /\.rectification-board__close \{[\s\S]*width: 44px/);
|
||||
assert.match(styles, /\.rectification-board__close \{[\s\S]*min-height: 44px/);
|
||||
assert.match(board, /<X aria-hidden="true"/);
|
||||
assert.match(chat, /compactBoard && !boardOpen \? \(/);
|
||||
assert.match(chat, /createPortal\(/);
|
||||
assert.match(chat, /\[data-rectification-header-slot\]/);
|
||||
assert.match(page, /className="chat-header-rectification"/);
|
||||
assert.match(page, /data-rectification-header-slot=""/);
|
||||
assert.match(styles, /\.chat-header-rectification \.rectification-board-peek \{[\s\S]*width: auto/);
|
||||
assert.doesNotMatch(chat, /rectification-workspace__board-trigger/);
|
||||
assert.doesNotMatch(chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("<form className=\"composer\"")), /RectificationBoardPeek/);
|
||||
assert.match(chat, /className=\{`rectification-workspace\$\{compactBoard \? " is-compact" : ""\}\$\{boardOpen \? " is-board-open" : ""\}`\}/);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,15 @@ test("aligns the session transcript and composer to one readable column", () =>
|
||||
assert.match(globalStyles, /\.conversation:not\(\.is-empty\):not\(\.is-rectification\) \+ \.composer-wrap \.composer-footer[^\{]*\{[^}]*width:\s*min\(var\(--session-column-width\),\s*100%\)/);
|
||||
});
|
||||
|
||||
test("keeps onboarding transcript and intake card on the same session column", () => {
|
||||
const pageSource = readProjectFile("src/app/page.tsx");
|
||||
assert.match(pageSource, /\$\{!profileComplete \? " is-onboarding" : ""\}/);
|
||||
assert.match(globalStyles, /\.conversation\.is-onboarding \.welcome \{[\s\S]*width:\s*min\(calc\(var\(--session-column-width\) \+ \(var\(--session-column-gutter\) \* 2\)\), 100%\)/);
|
||||
assert.match(globalStyles, /\.conversation\.is-onboarding\.is-empty \.welcome \{[\s\S]*width:\s*min\(var\(--session-column-width\), 100%\)/);
|
||||
assert.match(globalStyles, /\.conversation\.is-onboarding \.onboarding-card,\s*\n\.conversation\.is-onboarding \.onboarding-card\.birth-time-transition-card \{[\s\S]*width:\s*100%;[\s\S]*max-width:\s*none/);
|
||||
assert.match(globalStyles, /\.conversation\.is-onboarding-form \.welcome \{[\s\S]*padding:\s*var\(--space-6\) var\(--session-column-gutter\) var\(--space-8\)/);
|
||||
});
|
||||
|
||||
test("keeps message motion restrained and honors reduced-motion preferences", () => {
|
||||
assert.match(messageRowSource, /gsap\.matchMedia\(\)/);
|
||||
assert.match(messageRowSource, /prefers-reduced-motion:\s*no-preference/);
|
||||
|
||||
Reference in New Issue
Block a user