feat(loading): chart wait lights houses up in order, people archive uses the orbit, today's line no longer jumps in after reveal (BUG-1125)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9a616107c2
commit
d327ca9c95
@@ -986,6 +986,7 @@ button:disabled:where(:not([data-slot="button"])) { cursor: default; opacity: .4
|
||||
.chat-profile-picker-title { margin: 0; color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-sm); font-weight: 500; }
|
||||
.chat-profile-picker-status { margin: var(--space-2) 0 0; color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.45; }
|
||||
.chat-profile-picker-status p { margin: 0; }
|
||||
.chat-profile-picker-waiting { margin-top: var(--space-2); }
|
||||
.chat-profile-picker-retry { min-height: 44px; margin-top: var(--space-2); padding: 0 var(--space-3); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--color-action); cursor: pointer; font: inherit; font-size: var(--type-caption); }
|
||||
.chat-profile-picker-options { display: grid; gap: var(--space-1); margin: var(--space-2) 0 0; }
|
||||
.chat-profile-picker-option { width: 100%; min-height: 44px; display: grid; grid-template-columns: minmax(0, 1fr) 20px; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border: 1px solid transparent; border-radius: var(--radius-md); background: transparent; color: var(--color-ink-secondary); cursor: pointer; text-align: left; font: inherit; transition: border-color 120ms ease-out, background-color 120ms ease-out, color 120ms ease-out; }
|
||||
@@ -2413,6 +2414,16 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
}
|
||||
/* The line enters softly (BUG-1125): opacity and a 4px rise, 240ms, whenever
|
||||
its text first appears or changes. No placeholder, no spinner. */
|
||||
.starter-greeting-sub { animation: starter-greeting-sub-in 240ms var(--ease-out) both; }
|
||||
@keyframes starter-greeting-sub-in {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.starter-greeting-sub { animation: none; }
|
||||
}
|
||||
|
||||
/* 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
|
||||
@@ -5361,16 +5372,30 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.chart-page-skeleton {
|
||||
animation: chart-skeleton-breathe 2.4s ease-in-out infinite;
|
||||
}
|
||||
/* The waiting chart (2026-09-30, 宫位依次点亮): the empty drawing's lines stay
|
||||
still and faint while the twelve houses (the western wheel's twelve sectors)
|
||||
take turns glowing in house order, 150ms apart, one wave every 2.4s. Only
|
||||
the fill moves; the box is the finished chart's box (BUG-1101). */
|
||||
.chart-page-skeleton svg { display: block; margin-top: 0; }
|
||||
@keyframes chart-skeleton-breathe {
|
||||
0%, 100% { opacity: 0.35; }
|
||||
50% { opacity: 0.6; }
|
||||
.chart-page-skeleton .personal-report-chart-cell,
|
||||
.chart-page-skeleton .chart-page-western-ring,
|
||||
.chart-page-skeleton .chart-page-western-spoke { stroke-opacity: 0.55; }
|
||||
.chart-page-skeleton .personal-report-chart-cell,
|
||||
.chart-page-skeleton .chart-skeleton-sector {
|
||||
fill: var(--color-action-strong);
|
||||
fill-opacity: 0;
|
||||
stroke-linejoin: round;
|
||||
animation: chart-skeleton-house 2.4s ease-in-out infinite;
|
||||
animation-delay: calc(var(--house-index, 0) * 150ms);
|
||||
}
|
||||
.chart-page-skeleton .chart-skeleton-sector { stroke: none; }
|
||||
@keyframes chart-skeleton-house {
|
||||
0%, 40%, 100% { fill-opacity: 0; }
|
||||
14% { fill-opacity: 0.18; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.chart-page-skeleton { animation: none; opacity: 0.5; }
|
||||
.chart-page-skeleton .personal-report-chart-cell,
|
||||
.chart-page-skeleton .chart-skeleton-sector { animation: none; fill-opacity: 0; }
|
||||
}
|
||||
/* 「那一刻的天空」: the chart header's one extra action, tinted with the accent and
|
||||
a sparkle so it reads as an invitation rather than a thin outline (2026-09-29).
|
||||
@@ -5424,14 +5449,8 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
width: max-content;
|
||||
}
|
||||
.chart-symbol-bubble.is-below { transform: translate(-50%, 8px); }
|
||||
/* Waiting copy sits over the breathing empty chart, centred, so the waiting
|
||||
stage and the finished one are the same height. */
|
||||
/* The waiting stage carries only an sr-only sentence next to the drawing. */
|
||||
.chart-page-waiting-plot { position: relative; }
|
||||
.chart-page-skeleton-copy {
|
||||
position: absolute;
|
||||
inset: 50% var(--space-4) auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.chart-page-symbol-hit { fill: transparent; stroke: none; }
|
||||
.chart-page-symbol-target { cursor: help; }
|
||||
.chart-page-symbol-target:focus-visible {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { EMPTY_NORTH_INDIAN_CHART, VedicChartSvg } from "@/components/personal-report/vedic-chart-svg";
|
||||
import { EMPTY_WESTERN_WHEEL, WesternWheelSvg } from "./western-wheel-svg";
|
||||
import { OrbitWaiting } from "@/components/orbit-waiting";
|
||||
|
||||
/** Only chart positions opt in; the shared page shell remains static. */
|
||||
export function ChartPageSkeleton({ copy, western = false }: { copy: string; western?: boolean }) {
|
||||
return (
|
||||
<div className={western ? "chart-page-western-layout" : "chart-page-vedic-layout"}>
|
||||
<div className={`chart-page-waiting-plot ${western ? "chart-page-western-wheel" : "chart-page-vedic-stage"}`} aria-busy="true" role="status">
|
||||
{/* The empty drawing is the whole wait (2026-09-30, product: 「星盘的加载动画
|
||||
太丑了」, chose 宫位依次点亮): faint static lines while the houses light up
|
||||
one after another in house order. No orbit logo over it any more. Same
|
||||
box as the finished chart, so the reveal does not jump (BUG-1101). */}
|
||||
<div className={western ? "chart-page-skeleton" : "chart-page-skeleton chart-page-vedic-plot"} aria-hidden="true">
|
||||
{western ? <WesternWheelSvg western={EMPTY_WESTERN_WHEEL} skeleton />
|
||||
: <VedicChartSvg chart={EMPTY_NORTH_INDIAN_CHART} ariaLabel={copy} skeleton />}
|
||||
</div>
|
||||
{/* Over the empty drawing, not under it, so the waiting stage and the
|
||||
finished one are the same height (BUG-1101). An orbit, not a sentence
|
||||
(2026-09-30); the sentence is for screen readers. */}
|
||||
<OrbitWaiting className="chart-page-skeleton-copy" label={copy} />
|
||||
<span className="sr-only">{copy}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MouseEvent, PointerEvent } from "react";
|
||||
import type { CSSProperties, MouseEvent, PointerEvent } from "react";
|
||||
|
||||
import type { ChartViewWesternOk } from "@/lib/chart-view-contract";
|
||||
import { SIGN_ORDER, signZh, type SignName } from "@/lib/chart-view-labels";
|
||||
@@ -183,6 +183,15 @@ export function WesternWheelSvg({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Waiting wheel only: twelve sectors that light up in turn around the circle. */}
|
||||
{skeleton && SIGN_ORDER.map((sign, index) => (
|
||||
<path
|
||||
key={`${sign}-waiting`}
|
||||
d={westernRingSlice(WESTERN_R_ZODIAC, WESTERN_R_OUTER, westernScreenAngle(index * 30 + 30, asc), westernScreenAngle(index * 30, asc))}
|
||||
className="chart-skeleton-sector"
|
||||
style={{ "--house-index": index } as CSSProperties}
|
||||
/>
|
||||
))}
|
||||
<circle cx={WESTERN_CX} cy={WESTERN_CY} r={WESTERN_R_OUTER} className="chart-page-western-ring" />
|
||||
<circle cx={WESTERN_CX} cy={WESTERN_CY} r={WESTERN_R_ZODIAC} className="chart-page-western-ring" />
|
||||
<circle cx={WESTERN_CX} cy={WESTERN_CY} r={WESTERN_R_HOUSE} className="chart-page-western-ring" />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect } from "react";
|
||||
import { useCurrentSubject, useCurrentSubjectProfile, useCurrentSubjectReady } from "@/lib/current-subject";
|
||||
import { resolveBirthTimeConsultationRoute } from "@/lib/birth-time-consultation-consent";
|
||||
import { missingOtherProfileStep } from "@/lib/home-profile";
|
||||
import { SELF_SUBJECT_ID } from "@/lib/subject-birth";
|
||||
import { calendarDateInTimeZone, dailyStarlanguageProfileKey } from "@/lib/daily-starlanguage";
|
||||
import type { BootstrapPhase } from "@/lib/home-bootstrap";
|
||||
import { readHomeWarmDaily, rememberHomeWarmDaily } from "@/lib/home-warm-snapshot";
|
||||
@@ -36,7 +37,18 @@ export function DailyStarlanguageBinder(props: {
|
||||
const natalMinuteAvailable = route?.kind === "consult" && Boolean(route.time);
|
||||
|
||||
useEffect(() => {
|
||||
if (bootstrapPhase === "account" || !accountId || !subjectReady || !profile || !profileComplete || !natalMinuteAvailable) {
|
||||
// Only another person needs the catalog; the owner's profile is already here.
|
||||
// While it loads the card is "pending", not "unavailable": "unavailable" counted
|
||||
// as settled, the home revealed without the line, and the line jumped in under
|
||||
// the greeting a moment later (BUG-1125). Pending holds the reveal (bounded by
|
||||
// the prepare timeout) so the line arrives with the first frame.
|
||||
const waitingForCatalog = !subjectReady && subjectId !== SELF_SUBJECT_ID;
|
||||
if (bootstrapPhase !== "account" && accountId && waitingForCatalog) {
|
||||
let cancelled = false;
|
||||
queueMicrotask(() => { if (!cancelled) setDailyStarlanguage({ kind: "pending" }); });
|
||||
return () => { cancelled = true; };
|
||||
}
|
||||
if (bootstrapPhase === "account" || !accountId || waitingForCatalog || !profile || !profileComplete || !natalMinuteAvailable) {
|
||||
let cancelled = false;
|
||||
queueMicrotask(() => { if (!cancelled) setDailyStarlanguage({ kind: "unavailable" }); });
|
||||
return () => { cancelled = true; };
|
||||
|
||||
@@ -298,8 +298,8 @@ export function PeoplePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SecondaryPageShell title="星盘档案">
|
||||
{status === "loading" ? <p className="secondary-page-waiting">人物列表还在读</p> : null}
|
||||
// The shared waiting orbit, like 星盘 / 星历 / 报告 (2026-09-30, product: 「星盘档案这里也要加一个加载动画」).
|
||||
<SecondaryPageShell title="星盘档案" waiting={status === "loading" ? "人物列表还在读" : null}>
|
||||
{status === "failed" ? (
|
||||
<section className="chart-page-message">
|
||||
<p>人物列表暂时读不出来</p>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* Colour identifies a body. It is not a benefic/malefic signal.
|
||||
*/
|
||||
|
||||
import { useId } from "react";
|
||||
import { useId, type CSSProperties } from "react";
|
||||
|
||||
import type { ChartV1 } from "@/lib/personal-report-contract";
|
||||
import { SIGN_ORDER, signZh } from "@/lib/chart-view-labels";
|
||||
@@ -364,6 +364,8 @@ export function VedicChartSvgDrawing({ chart: suppliedChart, ariaLabel, highligh
|
||||
<polygon
|
||||
points={polygonPoints(polygon)}
|
||||
className={`personal-report-chart-cell${highlightHouseNumbers?.has(houseNumber) ? " is-changed" : ""}`}
|
||||
// The waiting chart lights the houses up in house order (chart-skeleton-house).
|
||||
style={skeleton ? ({ "--house-index": houseNumber - 1 } as CSSProperties) : undefined}
|
||||
/>
|
||||
{ordinal !== null && (
|
||||
<g
|
||||
|
||||
@@ -36,7 +36,9 @@ export function StarterGreeting({ starterGreeting, dailyTrend = null, dailyTrend
|
||||
<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"}>
|
||||
// Keyed on the text: a line that arrives or changes after the first frame
|
||||
// fades in instead of snapping in (BUG-1125).
|
||||
<p key={dailyTrend} className="starter-greeting-sub" role={dailyTrendSettled ? undefined : "status"}>
|
||||
{dailyTrend}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type ChatProfileCatalogStatus,
|
||||
type ChatProfilePickerRow,
|
||||
} from "@/lib/chat-profile-picker-model";
|
||||
import { OrbitWaiting } from "@/components/orbit-waiting";
|
||||
import type { ChartLibraryRecord } from "@/lib/home-types";
|
||||
|
||||
export type SubjectSwitcherProps = {
|
||||
@@ -88,7 +89,7 @@ export function SubjectSwitcher({
|
||||
}}
|
||||
>
|
||||
<Popover.Title className="chat-profile-picker-title">当前星盘</Popover.Title>
|
||||
{status === "loading" ? <p className="chat-profile-picker-status" role="status">人物列表还在读</p> : null}
|
||||
{status === "loading" ? <OrbitWaiting className="chat-profile-picker-waiting" label="人物列表还在读" /> : null}
|
||||
{status === "failed" ? (
|
||||
<div className="chat-profile-picker-status" role="status">
|
||||
<p>人物列表暂时读不出来</p>
|
||||
|
||||
Reference in New Issue
Block a user