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
+94 -303
View File
@@ -356,7 +356,7 @@ button:disabled { cursor: default; opacity: .45; }
.birth-time-assessment-progress strong { margin-top: var(--space-2); color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 500; }
.birth-time-assessment-progress > span { font-size: var(--type-body-sm); line-height: 1.55; text-wrap: pretty; word-break: auto-phrase; }
.onboarding-inline-error { margin-left: 0; }
.welcome > .starter-list { margin-left: 0; }
.message-assistant { align-items: flex-start; justify-content: flex-start; gap: var(--space-3); }
.message-user { justify-content: flex-end; }
.message-user .message-content { display: flex; flex-direction: column; align-items: flex-end; }
@@ -558,7 +558,7 @@ button:disabled { cursor: default; opacity: .45; }
.status-已过期, .status-已兑换 { color: var(--color-ink-secondary); }
.empty-cell { color: var(--color-ink-secondary); text-align: center !important; }
.new-chat:not(:disabled):active, .session-main:not(:disabled):active, .session-menu-trigger:not(:disabled):active, .session-action-item:not([data-disabled]):active, .profile-trigger:not(:disabled):active, .credit-button:not(:disabled):active, .account-menu-item:not([data-disabled]):active, .starter-list > button:not(:disabled):active, .composer button:not(:disabled):active, .button-primary:not(:disabled):active, .button-secondary:not(:disabled):active, .dialog-close:not(:disabled):active, .generated-list button:not(:disabled):active, .inline-actions button:not(:disabled):active { transform: scale(.96); }
.new-chat:not(:disabled):active, .session-main:not(:disabled):active, .session-menu-trigger:not(:disabled):active, .session-action-item:not([data-disabled]):active, .profile-trigger:not(:disabled):active, .credit-button:not(:disabled):active, .account-menu-item:not([data-disabled]):active, .composer button:not(:disabled):active, .button-primary:not(:disabled):active, .button-secondary:not(:disabled):active, .dialog-close:not(:disabled):active, .generated-list button:not(:disabled):active, .inline-actions button:not(:disabled):active{ transform: scale(.96); }
@keyframes app-loading-orbit { to { transform: rotate(360deg); } }
@keyframes inline-spin { to { transform: rotate(360deg); } }
@@ -881,7 +881,11 @@ button:disabled { cursor: default; opacity: .45; }
.credit-button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); padding: 0 var(--space-3); cursor: pointer; font-size: var(--type-caption); font-variant-numeric: tabular-nums; transition: background-color 120ms ease-out, transform 120ms ease-out; min-width: 64px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink-secondary); font-weight: 500; }
.conversation { min-width: 0; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding-bottom: var(--composer-reserve); background: var(--color-canvas); }
.conversation.is-empty { display: grid; place-items: center; padding: var(--space-8); }
/* Empty state. The greeting is bottom-aligned inside the conversation row and
the entry pills live under the composer, so the greeting / composer / pills
stack reads as one centred group while the composer keeps the single DOM
position it has in a live session. */
.conversation.is-empty { display: grid; align-content: end; justify-items: center; padding: var(--space-8) var(--space-8) 0; }
.welcome { width: min(820px, 100%); padding: var(--space-10) 0 var(--space-16); }
.welcome > .onboarding-message:first-child { padding-bottom: var(--space-8); }
.welcome > .onboarding-message:first-child .message-bubble p {
@@ -1046,37 +1050,7 @@ button:disabled { cursor: default; opacity: .45; }
.conversational-original-question { background: var(--color-success-muted); }
.phrase-nowrap { white-space: nowrap; }
.starter-list { border-top: 1px solid var(--color-border); display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); border: 0; }
.starter-arrow { width: 17px; height: 17px; color: var(--color-ink-secondary); }
.product-entrypoints { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1.12fr) minmax(0, .88fr); gap: var(--space-3); }
.product-entrypoints small { color: var(--color-ink-secondary); line-height: 1.45; font-size: var(--type-caption); }
.daily-starlanguage-card, .birth-rectification-card { min-height: 132px; display: grid; gap: var(--space-3); padding: var(--space-5); border: 1px solid color-mix(in srgb, var(--color-action) 16%, var(--color-border)); border-radius: var(--radius-lg); background: var(--color-canvas); }
.product-entrypoint-card {
position: relative;
overflow: hidden;
transition: background-color 180ms ease-out;
}
.product-entrypoint-hitarea { position: absolute; z-index: 2; inset: 0; width: 100%; min-height: 0; padding: 0; border: 0; border-radius: inherit; background: transparent; cursor: pointer; }
.product-entrypoint-card > :not(.product-entrypoint-hitarea) { position: relative; z-index: 1; pointer-events: none; }
/* The card is opening a Case: static copy and a progress cursor, no spinner (unified-loading ruling). */
.product-entrypoint-card[data-opening="true"],
.product-entrypoint-card[data-opening="true"] .product-entrypoint-hitarea { cursor: progress; }
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):active) { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); background: var(--color-action-soft); transform: translateY(0); }
.product-entrypoint-card:has(.product-entrypoint-hitarea:focus-visible) { outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent); outline-offset: 2px; }
@media (hover: hover) {
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); transform: translateY(-1px); }
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:disabled) { opacity: 1; }
.product-entrypoint-hitarea:disabled { cursor: not-allowed; }
.daily-starlanguage-card dl, .birth-rectification-card dl { display: grid; gap: var(--space-2); margin: 0; }
.daily-starlanguage-card dl > div, .birth-rectification-card dl > div { display: grid; gap: 4px; }
.daily-starlanguage-card dt, .birth-rectification-card dt { color: var(--color-ink-secondary); font-size: var(--type-caption); }
.daily-starlanguage-card dd, .birth-rectification-card dd { margin: 0; color: var(--color-ink); line-height: 1.45; }
.product-entrypoint-footer { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-3); }
.product-entrypoint-footer small { min-width: 0; flex: 1; }
.product-entrypoint-action { display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: flex-end; gap: var(--space-1); color: var(--color-action); font-size: var(--type-caption); font-weight: 600; white-space: nowrap; }
.product-entrypoint-action .starter-arrow { width: 15px; height: 15px; color: currentColor; }
.rectification-entry-error { grid-column: 1 / -1; margin: 0; }
.message-list {
--assistant-content-inset: calc(32px + var(--space-3));
@@ -1927,7 +1901,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
}
@media (min-width: 768px) and (max-width: 1023px) {
.product-entrypoints { grid-template-columns: 1fr; }
.auth-shell { grid-template-columns: 1.2fr .8fr; }
.auth-story, .auth-panel { padding: var(--space-8); }
.auth-story h2 { font-size: var(--type-display-sm); }
@@ -1955,8 +1929,8 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
}
.welcome { padding: var(--space-6) 0 var(--space-10); }
.welcome > .onboarding-message:first-child .message-bubble p { font-size: var(--type-display-sm); }
.starter-list { grid-template-columns: 1fr; grid-template-rows: auto; }
.product-entrypoints { grid-template-columns: 1fr; }
.message-list { width: 100%; padding: var(--space-5) var(--space-4) var(--space-12); }
.message-content { max-width: 88%; }
.composer-wrap { padding: var(--space-2) var(--space-3) max(var(--space-3), env(safe-area-inset-bottom)); }
@@ -2022,268 +1996,136 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.conversational-composer-footer button, .conversational-confirmation button, .conversational-original-question button { width: 100%; }
}
/* Starter workbench */
/* Starter home: greeting, composer, entry pills */
.welcome {
width: min(1040px, 100%);
width: min(var(--session-column-width), 100%);
margin-inline: auto;
padding: var(--space-8) 0 var(--space-12);
padding: var(--space-8) 0 var(--space-5);
}
.starter-list {
width: min(1040px, 100%);
margin-inline: auto;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--space-6);
border: 0;
}
.starter-hero,
.product-entrypoints {
/* The greeting: one line of salutation, one question. It used to be an h1 at
clamp(34px, 5vw, 52px) inside a bordered hero card, with two 132px product
cards under it — together about 800px sitting on top of the composer. */
.starter-greeting-block {
width: 100%;
min-width: 0;
text-align: center;
}
.starter-hero {
min-height: 0;
display: block;
padding: var(--space-10);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas-soft);
}
.starter-hero-copy { max-width: 720px; }
.starter-greeting {
margin: 0 0 var(--space-3);
color: var(--color-action);
.starter-salutation {
margin: 0 0 var(--space-2);
color: var(--color-ink-tertiary);
font-size: var(--type-body-sm);
font-weight: 600;
}
.starter-hero h1 {
max-width: 760px;
.starter-greeting-block h1 {
margin: 0;
color: var(--color-ink);
font-family: var(--font-display);
font-size: clamp(34px, 5vw, 52px);
font-size: clamp(26px, 3.2vw, 34px);
font-weight: 500;
letter-spacing: -.04em;
line-height: 1.08;
letter-spacing: -.02em;
line-height: 1.3;
text-wrap: balance;
}
.product-entrypoints {
/* Entry pills, under the composer. The two product entrances keep their place
in the product but not their weight: they were 132px cards competing with the
input for the first look. */
.starter-entries {
width: min(var(--session-column-width), 100%);
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
gap: var(--space-2);
margin: var(--space-4) auto 0;
justify-items: center;
}
.starter-entry-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--space-2);
}
.starter-entry {
min-height: 36px;
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: 0 var(--space-4);
border: 1px solid var(--color-border-strong);
border-radius: 999px;
background: var(--color-canvas);
}
.product-entrypoints small {
color: var(--color-ink-secondary);
line-height: 1.5;
font-size: var(--type-caption);
}
.daily-starlanguage-card,
.birth-rectification-card {
min-height: 228px;
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
gap: var(--space-6);
padding: var(--space-8);
border: 0;
border-radius: 0;
background: transparent;
}
.birth-rectification-card {
border-left: 1px solid var(--color-border);
}
.product-entrypoint-card {
position: relative;
overflow: hidden;
transition: background-color 180ms ease-out;
}
.product-entrypoint-card::after {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
background: linear-gradient(112deg, transparent 22%, var(--sheen) 46%, transparent 68%);
content: "";
transform: translateX(-48%);
transition: opacity 220ms ease-out, transform 520ms var(--ease-out);
}
.product-entrypoint-hitarea {
position: absolute;
z-index: 3;
inset: 0;
width: 100%;
min-height: 0;
padding: 0;
border: 0;
border-radius: inherit;
background: transparent;
color: var(--color-ink);
cursor: pointer;
font-size: var(--type-body-sm);
transition: border-color 140ms ease-out;
}
.product-entrypoint-card > :not(.product-entrypoint-hitarea) {
position: relative;
z-index: 1;
pointer-events: none;
.starter-entry > svg {
width: 15px;
height: 15px;
flex: 0 0 15px;
color: var(--color-action);
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):active) {
border-color: transparent;
background: color-mix(in srgb, var(--color-action-soft) 58%, var(--color-canvas));
.starter-entry[data-opening="true"] { cursor: progress; }
.starter-entry:disabled { cursor: not-allowed; }
/* Pressed feedback is transient; hover only moves the hairline. A hover that
shades the surface reads as "selected" and sticks on touch devices. */
.starter-entry:not(:disabled):active {
border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border-strong));
background: var(--color-action-soft);
}
@media (hover: hover) {
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) {
border-color: transparent;
transform: none;
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover)::after {
opacity: .65;
transform: translateX(48%);
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) .product-entrypoint-action {
transform: translateX(3px);
}
.starter-entry:not(:disabled):hover { border-color: var(--color-action); }
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:focus-visible) {
.starter-entry:focus-visible {
outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent);
outline-offset: -3px;
outline-offset: 2px;
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:disabled) {
opacity: 1;
}
.product-entrypoint-hitarea:disabled {
cursor: not-allowed;
}
.product-entrypoint-copy {
max-width: 520px;
}
.product-entrypoint-copy h2 {
.starter-entry-hint,
.starter-entry-boundary {
max-width: 560px;
margin: 0;
color: var(--color-ink);
font-family: var(--font-display);
font-size: clamp(24px, 2.4vw, 31px);
font-weight: 500;
letter-spacing: -.03em;
line-height: 1.12;
}
.product-entrypoint-copy p {
max-width: 460px;
margin: var(--space-3) 0 0;
color: var(--color-ink-secondary);
font-size: var(--type-body-sm);
line-height: 1.65;
color: var(--color-ink-tertiary);
font-size: var(--type-caption);
line-height: 1.55;
text-align: center;
text-wrap: pretty;
}
.product-entrypoint-copy p[aria-busy="true"] {
color: var(--color-ink-tertiary);
.starter-entry-boundary { color: var(--color-ink-tertiary); }
@media (hover: hover) {
}
.product-entrypoint-footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: var(--space-5);
padding-top: var(--space-5);
border-top: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
}
.product-entrypoint-footer small {
min-width: 0;
flex: 1;
}
.product-entrypoint-footer small:empty {
display: none;
}
.product-entrypoint-action {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
margin-left: auto;
gap: var(--space-1);
color: var(--color-action);
font-size: var(--type-caption);
font-weight: 600;
white-space: nowrap;
transition: transform 180ms var(--ease-out);
}
.product-entrypoint-action .starter-arrow {
width: 15px;
height: 15px;
color: currentColor;
}
.starter-arrow {
width: 17px;
height: 17px;
color: var(--color-ink-secondary);
transition: color 180ms ease-out, transform 180ms var(--ease-out);
}
/* One column width across both states: the empty-state composer used to be
1040px while the session composer was 760px, so the input visibly jumped
width the moment the first message landed. */
.composer-wrap-starter .composer {
width: min(1040px, 100%);
width: min(var(--session-column-width), 100%);
margin-inline: auto;
}
.composer-wrap-starter {
border-top-color: transparent;
padding-bottom: var(--space-16);
background: transparent;
backdrop-filter: none;
}
@media (min-width: 768px) and (max-width: 1023px) {
.starter-hero {
padding: var(--space-8);
}
.product-entrypoints {
grid-template-columns: 1fr;
}
.daily-starlanguage-card,
.birth-rectification-card {
min-height: 0;
grid-template-rows: auto auto;
}
.birth-rectification-card {
border-top: 1px solid var(--color-border);
border-left: 0;
}
.product-entrypoint-footer {
flex-direction: column;
align-items: flex-start;
gap: var(--space-3);
}
.product-entrypoint-action {
margin-left: 0;
justify-content: flex-start;
}
}
@media (max-width: 767px) {
@@ -2291,62 +2133,11 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
padding: var(--space-4) 0 var(--space-8);
}
.starter-list {
grid-template-columns: 1fr;
grid-template-rows: auto;
gap: var(--space-5);
}
.starter-hero {
min-height: 0;
grid-template-columns: 1fr;
padding: var(--space-8) var(--space-6);
}
.starter-hero h1 {
font-size: clamp(38px, 11vw, 54px);
}
.product-entrypoints {
grid-template-columns: 1fr;
}
.daily-starlanguage-card,
.birth-rectification-card {
min-height: 0;
grid-template-rows: auto auto;
gap: var(--space-4);
padding: var(--space-5);
}
.birth-rectification-card {
border-top: 1px solid var(--color-border);
border-left: 0;
}
.product-entrypoint-copy h2 {
font-size: var(--type-title-md);
}
.product-entrypoint-footer {
flex-direction: column;
align-items: flex-start;
gap: var(--space-3);
padding-top: var(--space-4);
}
.product-entrypoint-action {
margin-left: 0;
justify-content: flex-start;
font-size: var(--type-body-sm);
font-weight: 500;
}
}
@media (max-width: 480px) {
.starter-hero {
padding: var(--space-6) var(--space-5);
}
}
/* Session conversation */
+21 -21
View File
@@ -42,7 +42,7 @@ import { clearStaleClientReload } from "@/lib/stale-client-recovery";
import { BirthLocationFields } from "@/components/birth-location-fields";
import { OnboardingChatMessage } from "@/components/onboarding-chat-message";
import { ChartLibraryPanel } from "@/components/chart-library-panel";
import { StarterHome } from "@/components/starter-home";
import { StarterEntries, StarterGreeting } from "@/components/starter-home";
import {
GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION,
isGeneralDailyFortuneQuestion,
@@ -645,7 +645,6 @@ export default function Home() {
const dailyStarlanguageTrend = dailyStarlanguage.kind === "ready"
? dailyStarlanguage.card.trend
: "今天的星语还没写出来。";
const dailyStarlanguageAction = dailyStarlanguage.kind === "ready" ? dailyStarlanguage.card.action : "";
const bootstrapPrepareReady = bootstrapPrepareSettled({
dailyStarlanguageApplicable: Boolean(accountId) && profileComplete && natalMinuteAvailable,
dailyStarlanguageSettled: dailyStarlanguage.kind !== "pending",
@@ -1714,25 +1713,7 @@ export default function Home() {
{!profileComplete && onboardingStep === "name" && accountError && <p className="form-error onboarding-inline-error" role="alert">{accountError}</p>}
{profileComplete && presetMessageFinished && !rectificationSurfaceOpen && (
<StarterHome
starterGreeting={starterGreeting}
natalMinuteAvailable={natalMinuteAvailable}
dailyStarlanguage={dailyStarlanguage}
dailyStarlanguageQuestion={dailyStarlanguageQuestion}
dailyStarlanguageTrend={dailyStarlanguageTrend}
dailyStarlanguageAction={dailyStarlanguageAction}
productEntrypointsDisabled={productEntrypointsDisabled}
startDailyStarlanguageConsultation={startDailyStarlanguageConsultation}
rectificationCardLabel={rectificationCardLabel}
rectificationCardAction={rectificationCardAction}
rectificationLoading={rectificationLoading}
rectificationMutationPending={rectificationMutationPending}
openRectificationFromHomepage={openRectificationFromHomepage}
rectificationEntrySummary={rectificationEntrySummary}
rectificationError={rectificationErrorSessionId ? "" : rectificationError}
rectificationSurfaceOpen={rectificationSurfaceOpen}
rectificationErrorMessage={rectificationErrorSessionId ? "" : rectificationErrorMessage}
/>
<StarterGreeting starterGreeting={starterGreeting} />
)}
</div>
) : sessionMessagesLoading ? (
@@ -1819,6 +1800,25 @@ export default function Home() {
)}
onStop={() => void stopResponse()}
/>
{starterHomeVisible && (
<StarterEntries
natalMinuteAvailable={natalMinuteAvailable}
dailyStarlanguage={dailyStarlanguage}
dailyStarlanguageQuestion={dailyStarlanguageQuestion}
dailyStarlanguageTrend={dailyStarlanguageTrend}
productEntrypointsDisabled={productEntrypointsDisabled}
startDailyStarlanguageConsultation={startDailyStarlanguageConsultation}
rectificationCardLabel={rectificationCardLabel}
rectificationCardAction={rectificationCardAction}
rectificationLoading={rectificationLoading}
rectificationMutationPending={rectificationMutationPending}
openRectificationFromHomepage={openRectificationFromHomepage}
rectificationEntrySummary={rectificationEntrySummary}
rectificationError={rectificationErrorSessionId ? "" : rectificationError}
rectificationSurfaceOpen={rectificationSurfaceOpen}
rectificationErrorMessage={rectificationErrorSessionId ? "" : rectificationErrorMessage}
/>
)}
</div>}
</SidebarInset>
+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>
);
}