fix(home): split starter fine print, show hints only when actionable (BUG-1041)

TASK-starter-home-polish-20260926:
- D1 today's trend moves to a sub-line under the greeting
  (.starter-greeting-sub), read from the same dailyStarlanguage state the
  warm snapshot seeds; static sentence until ready, hidden without a minute.
- D2 the line under the pills appears only for a resumable case or a
  non-self subject; the first-run and redo sentences are removed.
- BUG-1041 entrySummaryFromResponse read snake_case keys while the
  entry-summary route returns camelCase, so every account parsed as a
  first run; the parser now reads the route's shape (snake_case still ok).
- D3 daily entry icon MoonStar, credits icon Coins; D4 boundary line kept.
- Records: CHANGELOG, DESIGN, VOICE, BUG_HISTORY, PROGRESS, testing list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-26 08:47:53 +08:00
co-authored by Claude Opus 5.5
parent 6d371d82a7
commit b5a02ca861
16 changed files with 427 additions and 68 deletions
+32 -31
View File
@@ -1,8 +1,7 @@
"use client";
import { Clock3, Sparkles } from "lucide-react";
import { Clock3, MoonStar } from "lucide-react";
import { useCurrentSubject } from "@/lib/current-subject";
import type { DailyStarlanguageState } from "@/lib/home-types";
import type { RectificationEntrySummary } from "@/lib/rectification-entry";
/**
@@ -21,27 +20,50 @@ import type { RectificationEntrySummary } from "@/lib/rectification-entry";
export type StarterGreetingProps = {
readonly starterGreeting: { readonly text: string };
/* Today's trend, one quiet line under the greeting. `null` hides it (no natal
minute). Before the card is ready the caller passes the static sentence,
never a placeholder or a spinner. */
readonly dailyTrend?: string | null;
readonly dailyTrendSettled?: boolean;
};
/* One line. The salutation/question pair it replaces asked the user's question
for them; the composer placeholder asks now. */
export function StarterGreeting({ starterGreeting }: StarterGreetingProps) {
for them; the composer placeholder asks now. The optional sub-line is today's
trend, moved up from the fine print under the entry pills where it was glued
to an unrelated rectification hint with a middle dot. */
export function StarterGreeting({ starterGreeting, dailyTrend = null, dailyTrendSettled = true }: StarterGreetingProps) {
return (
<section className="starter-greeting-block" aria-labelledby="starter-heading">
<h1 id="starter-heading">{starterGreeting.text}</h1>
{dailyTrend ? (
<p className="starter-greeting-sub" role={dailyTrendSettled ? undefined : "status"}>
{dailyTrend}
</p>
) : null}
</section>
);
}
/* The line under the pills only appears when there is something to do about
it. First runs and finished cases say nothing: the pill already names the
action. The branch reads the entry summary itself, not the derived
start/restart label, and a non-self subject wins because the case summary
belongs to the account owner. */
export function rectificationEntryHint(
rectificationForSelf: boolean,
summary: RectificationEntrySummary | null,
): string | null {
if (!rectificationForSelf) return "生时校正暂时只支持本人。";
if (summary?.hasResumableCase) return "上次那次校正还没完成,可以接着做。";
return null;
}
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;
@@ -53,13 +75,10 @@ export type StarterEntriesProps = {
export function StarterEntries({
natalMinuteAvailable,
dailyStarlanguage,
dailyStarlanguageQuestion,
dailyStarlanguageTrend,
productEntrypointsDisabled,
startDailyStarlanguageConsultation,
rectificationCardLabel,
rectificationCardAction,
rectificationLoading,
rectificationMutationPending,
openRectificationFromHomepage,
@@ -71,20 +90,7 @@ export function StarterEntries({
const subjectId = useCurrentSubject();
const rectificationForSelf = subjectId === "self";
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 = !rectificationForSelf
? "生时校正暂时只支持本人"
: rectificationEntrySummary?.hasResumableCase
? "未完成的那次可以从左侧继续"
: rectificationCardAction === "restart"
? "上次已经校正完,可以拿最新资料再来一次"
: "不确定出生时间时,用记得住的经历一步步缩小范围";
const rectificationHint = rectificationEntryHint(rectificationForSelf, rectificationEntrySummary);
return (
<div className="starter-entries">
@@ -96,7 +102,7 @@ export function StarterEntries({
disabled={productEntrypointsDisabled}
onClick={startDailyStarlanguageConsultation}
>
<Sparkles aria-hidden="true" />
<MoonStar aria-hidden="true" />
<span>{dailyLabel}</span>
</button>
<button
@@ -111,12 +117,7 @@ export function StarterEntries({
<span>生时校正</span>
</button>
</div>
<p
className="starter-entry-hint"
role={natalMinuteAvailable && dailyStarlanguage.kind !== "ready" ? "status" : undefined}
>
{dailyHint} · {rectificationHint}
</p>
{rectificationHint && <p className="starter-entry-hint">{rectificationHint}</p>}
{!natalMinuteAvailable && (
<p className="starter-entry-boundary">出生时间不足以支持的判断我会说明,不会补造具体分钟。</p>
)}