此前空态是 hero 卡(h1 clamp 到 52px)加两张 min-height 228px 的产品卡, 输入框被压在约 800px 内容之下,主动作不是视觉焦点。 改法:问候拆成 StarterGreeting 留在对话区并底对齐,两个产品入口降级成 StarterEntries 渲染在 composer 之后。composer 本身一行未动——它在空态与 会话态是同一个 DOM 位置,首条消息落地时不重挂载,草稿/焦点/排队卡都不受影响。 顺带修掉:空态 composer 1040px、会话态 760px,发首条消息时输入框会当场 变窄一截。两态统一到 --session-column-width,并新增断言锁住。 校正入口保留三个服务端驱动的文案分支(我第一版收成了两个,是 rectification-agentic-entry 打红才发现)。 DESIGN.md:Product entrypoint card 整节重写为 Starter home;删掉 Suggestion card 死文档(它描述的主题卡样式正是本轮删除的 CSS)。 测试 3350 不变,fail 仍 31 且与基线双向零差异;/ 仍 Static; CSS gzip 41,096→41,044(−0.13%)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
125 lines
5.0 KiB
TypeScript
125 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { Clock3, Sparkles } from "lucide-react";
|
|
import type { DailyStarlanguageState } from "@/lib/home-types";
|
|
import type { RectificationEntrySummary } from "@/lib/rectification-entry";
|
|
|
|
/**
|
|
* The empty state is split across two slots on purpose.
|
|
*
|
|
* `StarterGreeting` sits in the conversation area, bottom-aligned, so it lands
|
|
* directly above the composer. `StarterEntries` sits inside `.composer-wrap`,
|
|
* below the composer. The composer itself never moves: it keeps the one DOM
|
|
* position it has in a live session, so nothing remounts when the first message
|
|
* arrives and an in-flight draft, focus or queued card survives the transition.
|
|
*
|
|
* Before this, the empty state was a bordered hero card plus two 132px product
|
|
* cards, and the composer sat under roughly 800px of content — the primary
|
|
* action was the last thing on the page.
|
|
*/
|
|
|
|
export type StarterGreetingProps = {
|
|
readonly starterGreeting: { readonly salutation: string; readonly question: string };
|
|
};
|
|
|
|
export function StarterGreeting({ starterGreeting }: StarterGreetingProps) {
|
|
return (
|
|
<section className="starter-greeting-block" aria-labelledby="starter-heading">
|
|
<p className="starter-salutation">{starterGreeting.salutation}</p>
|
|
<h1 id="starter-heading">{starterGreeting.question}</h1>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export type StarterEntriesProps = {
|
|
readonly dailyStarlanguage: DailyStarlanguageState;
|
|
readonly natalMinuteAvailable: boolean;
|
|
readonly dailyStarlanguageQuestion: string;
|
|
readonly dailyStarlanguageTrend: string;
|
|
readonly productEntrypointsDisabled: boolean;
|
|
readonly startDailyStarlanguageConsultation: () => void;
|
|
readonly rectificationCardLabel: string;
|
|
readonly rectificationCardAction: string;
|
|
readonly rectificationLoading: boolean;
|
|
readonly rectificationMutationPending: boolean;
|
|
readonly openRectificationFromHomepage: () => void;
|
|
readonly rectificationEntrySummary: RectificationEntrySummary | null;
|
|
readonly rectificationError: string;
|
|
readonly rectificationSurfaceOpen: boolean;
|
|
readonly rectificationErrorMessage: string;
|
|
};
|
|
|
|
export function StarterEntries({
|
|
natalMinuteAvailable,
|
|
dailyStarlanguage,
|
|
dailyStarlanguageQuestion,
|
|
dailyStarlanguageTrend,
|
|
productEntrypointsDisabled,
|
|
startDailyStarlanguageConsultation,
|
|
rectificationCardLabel,
|
|
rectificationCardAction,
|
|
rectificationLoading,
|
|
rectificationMutationPending,
|
|
openRectificationFromHomepage,
|
|
rectificationEntrySummary,
|
|
rectificationError,
|
|
rectificationSurfaceOpen,
|
|
rectificationErrorMessage,
|
|
}: StarterEntriesProps) {
|
|
const dailyLabel = natalMinuteAvailable ? "今日星语" : "每日运势";
|
|
/* The trend line is the only part that arrives late. It stays a live region so
|
|
a screen reader hears it settle, but it is a tooltip-rank hint now, not a
|
|
card body — the pill is actionable from the first paint either way. */
|
|
const dailyHint = natalMinuteAvailable ? dailyStarlanguageTrend : "看看今天的整体节奏";
|
|
/* Three branches, same as the card it replaces: an unfinished case, a finished
|
|
one that can be redone, and a first run. The label itself is server-driven
|
|
from the entry summary (`rectificationCardAction`), not from the session list. */
|
|
const rectificationHint = rectificationEntrySummary?.hasResumableCase
|
|
? "未完成的那次可以从左侧继续"
|
|
: rectificationCardAction === "restart"
|
|
? "上次已经校正完,可以拿最新资料再来一次"
|
|
: "不确定出生时间时,用记得住的经历一步步缩小范围";
|
|
|
|
return (
|
|
<div className="starter-entries">
|
|
<div className="starter-entry-row">
|
|
<button
|
|
className="starter-entry"
|
|
type="button"
|
|
aria-label={natalMinuteAvailable ? dailyStarlanguageQuestion : "查看今日运势"}
|
|
disabled={productEntrypointsDisabled}
|
|
onClick={startDailyStarlanguageConsultation}
|
|
>
|
|
<Sparkles aria-hidden="true" />
|
|
<span>{dailyLabel}</span>
|
|
</button>
|
|
<button
|
|
className="starter-entry"
|
|
type="button"
|
|
aria-label={rectificationCardLabel}
|
|
data-opening={rectificationLoading ? "true" : undefined}
|
|
disabled={productEntrypointsDisabled || rectificationLoading || rectificationMutationPending}
|
|
onClick={() => void openRectificationFromHomepage()}
|
|
>
|
|
<Clock3 aria-hidden="true" />
|
|
<span>生时校正</span>
|
|
</button>
|
|
</div>
|
|
<p
|
|
className="starter-entry-hint"
|
|
role={natalMinuteAvailable && dailyStarlanguage.kind !== "ready" ? "status" : undefined}
|
|
>
|
|
{dailyHint} · {rectificationHint}
|
|
</p>
|
|
{!natalMinuteAvailable && (
|
|
<p className="starter-entry-boundary">出生时间不足以支持的判断我会说明,不会补造具体分钟。</p>
|
|
)}
|
|
{rectificationError && !rectificationSurfaceOpen && (
|
|
<p className="form-error rectification-entry-error" role="alert">
|
|
{rectificationErrorMessage}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|