feat(ui): 空状态改成「问候 + 输入框 + 两枚入口 pill」
Independent Staging Quality Gate / validate (push) Failing after 9m13s
Independent Staging Quality Gate / publish (push) Skipped

此前空态是 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
This commit is contained in:
Jesse_Chen
2026-09-16 05:31:15 +00:00
co-authored by Claude Opus 5
parent 66d9f59d84
commit 62903b7a12
13 changed files with 486 additions and 493 deletions
+83 -69
View File
@@ -1,16 +1,41 @@
"use client";
import { ArrowUpRight } from "lucide-react";
import { Clock3, Sparkles } from "lucide-react";
import type { DailyStarlanguageState } from "@/lib/home-types";
import type { RectificationEntrySummary } from "@/lib/rectification-entry";
export type StarterHomeProps = {
/**
* 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 dailyStarlanguageAction: string;
readonly productEntrypointsDisabled: boolean;
readonly startDailyStarlanguageConsultation: () => void;
readonly rectificationCardLabel: string;
@@ -24,13 +49,11 @@ export type StarterHomeProps = {
readonly rectificationErrorMessage: string;
};
export function StarterHome({
starterGreeting,
export function StarterEntries({
natalMinuteAvailable,
dailyStarlanguage,
dailyStarlanguageQuestion,
dailyStarlanguageTrend,
dailyStarlanguageAction,
productEntrypointsDisabled,
startDailyStarlanguageConsultation,
rectificationCardLabel,
@@ -42,69 +65,60 @@ export function StarterHome({
rectificationError,
rectificationSurfaceOpen,
rectificationErrorMessage,
}: StarterHomeProps) {
}: 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-list starter-workbench" aria-label="Jyotisha 首页">
<section className="starter-hero" aria-labelledby="starter-heading">
<div className="starter-hero-copy">
<p className="starter-greeting">{starterGreeting.salutation}</p>
<h1 id="starter-heading">{starterGreeting.question}</h1>
</div>
</section>
<section className="product-entrypoints" aria-label="常用占星入口">
<article className="daily-starlanguage-card product-entrypoint-card" aria-labelledby="daily-starlanguage-title">
<button
className="product-entrypoint-hitarea"
type="button"
aria-label={natalMinuteAvailable ? dailyStarlanguageQuestion : "查看今日运势"}
disabled={productEntrypointsDisabled}
onClick={startDailyStarlanguageConsultation}
/>
<div className="product-entrypoint-copy">
<h2 id="daily-starlanguage-title">{natalMinuteAvailable ? "今日星语" : "每日运势"}</h2>
<p
role={natalMinuteAvailable && dailyStarlanguage.kind !== "ready" ? "status" : undefined}
>{natalMinuteAvailable ? dailyStarlanguageTrend : "看看今天的整体节奏、适合推进的事和需要留意的地方。"}</p>
</div>
<div className="product-entrypoint-footer">
{natalMinuteAvailable
? (dailyStarlanguageAction ? <small>{dailyStarlanguageAction}</small> : null)
: <small></small>}
<span className="product-entrypoint-action" aria-hidden="true">{natalMinuteAvailable ? dailyStarlanguageQuestion : "查看今日运势"} <ArrowUpRight className="starter-arrow" /></span>
</div>
</article>
<article
className="birth-rectification-card product-entrypoint-card"
aria-labelledby="birth-rectification-title"
data-opening={rectificationLoading ? "true" : undefined}
>
<button
className="product-entrypoint-hitarea"
type="button"
aria-label={rectificationCardLabel}
disabled={productEntrypointsDisabled || rectificationLoading || rectificationMutationPending}
onClick={() => void openRectificationFromHomepage()}
/>
<div className="product-entrypoint-copy">
<h2 id="birth-rectification-title"></h2>
<p>{rectificationEntrySummary?.hasResumableCase
? "新建一段独立校正;未完成的记录仍可从左侧历史会话继续。"
: rectificationCardAction === "restart"
? "上一次校正已经完成,可以基于最新资料再次校正。"
: "不确定准确出生时间时,可通过已经发生的人生事件逐步缩小范围。"}</p>
</div>
<div className="product-entrypoint-footer">
<span className="product-entrypoint-action" aria-hidden="true">{rectificationCardLabel} <ArrowUpRight className="starter-arrow" /></span>
</div>
</article>
</section>
{rectificationError && !rectificationSurfaceOpen && (
<p className="form-error rectification-entry-error" role="alert">
{rectificationErrorMessage}
</p>
)}
</div>
<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>
);
}