feat(ui): 星盘 tab 下划线化、参数收成键值表,星历日期条改 ‹ 日期 ›
Independent Staging Quality Gate / validate (push) Failing after 9m13s
Independent Staging Quality Gate / publish (push) Skipped

星盘页(T5.1):
- 五个体系 tab 从五枚描边药丸改成顶栏下方一条下划线式 tab 行,
  flex-wrap: nowrap + 容器自身横滚,375px 下仍是一行;底线画在滚动
  容器的 border 上,不随内容滚走。眉标(BUG-717 的成本与速度承诺)
  原文保留,位置从 tab 行上方移到下方,让 tab 行直接接住 46px 顶栏。
- 分盘 chips 保留但降一级:去描边去卡面、caption 字号、三级色,
  当前项只留浅底。它是一个 tab 内部的筛选器,不是第二层导航。
- 参数从「中宫叠卡(手机两行/桌面六行)+ 盘下重复卡 + media query
  三选一」收成一张八行键值表。同一组事实原本画三遍,改一处要改三处。
  .chart-page-center-card / -compact / -extra / .chart-page-params-below
  / .chart-page-boundary-desktop 五个类连同 767/768 开关块一并删除。
  view.vedic.boundary 那句准确性边界句原文未动,紧跟在表下方,且
  DOM 里从出现两次变成一次。
- 三张行星表(印度宿度 / 西洋相位 / 七政十一曜)全部包上横向滚动
  容器,wrap 补 min-width: 0 —— 少了它 grid item 按内容定宽,表格会
  撑宽页面而不是在自己盒子里滚动。原先只有第一张有 wrap。

星历页(T5.2):
- 日期导航从三个并排等宽按钮改成居中的 ‹ 日期 ›:日期是主语,两枚
  44px 箭头是把手。箭头字形 aria-hidden,可访问名仍是「前一天」
  「后一天」,读屏不丢方向。
- 「今天」只在非今日时以文字按钮出现在日期下方;在今日时整个不渲染,
  而不是渲染成禁用按钮——灰掉的按钮仍要被读、仍要被 Tab 跳过。
- 三段统一成同一套卡片:框从内容上移到段落,.ephemeris-panchanga 的
  dl 网格原样保留。收尾句原文不变,但从底部第四张卡降为页尾一行小字。

两条既有断言改动,测试文件内就地写了原值/新值/原因,进度记录成表:
参数断点开关的三条断言换成「参数只有一张键值表且那五个类不再有规则」
(更强,且避开了原写法 indexOf 未命中后静默锚错断点块的陷阱);
「今天」在场的单状态断言拆成今日/非今日两状态并加断不出现 disabled。
测试总数 3350 → 3352,失败清单与基线逐条一致(31 条无 Docker 缺口)。

tsc 0 错;lint 0 error / 118 warning(与基线同数);/ /chart /ephemeris
渲染标记均保持 ○ Static,/reports 仍 ƒ;主 CSS gzip 36,745 → 36,788
(+0.12%)。行星表 375px 横滚、tab 行横滚、读屏播报无真机验收,已在
进度记录里如实写成环境缺口。

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 06:47:50 +00:00
co-authored by Claude Opus 5
parent ca0204984b
commit 1e3570b462
12 changed files with 538 additions and 213 deletions
+138 -92
View File
@@ -1995,7 +1995,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
}
.chart-page-eyebrow {
width: min(1100px, 100%);
margin: var(--space-4) auto 0;
margin: 0 auto var(--space-6);
color: var(--color-ink-tertiary);
font-size: var(--type-caption);
}
@@ -2073,8 +2073,7 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
.report-center-body,
.chart-page-message,
.ephemeris-message,
.report-center-message,
.chart-page-eyebrow { padding-inline: var(--space-4); }
.report-center-message { padding-inline: var(--space-4); }
}
/* Starter home: greeting, composer, entry pills */
@@ -4500,30 +4499,62 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* The five systems are one underline tab row directly under the app header,
not five bordered pills floating on the page. Five pills of equal weight read
as five buttons you might press; a tab row reads as one control with a
current position. The row scrolls sideways rather than wrapping, so the
header hand-off stays a single line at 375px — the border sits on the
scroll container itself and therefore does not scroll away. */
.chart-page-tabs {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-6);
flex-wrap: nowrap;
gap: var(--space-4);
margin-bottom: var(--space-4);
padding-top: var(--space-2);
border-bottom: 1px solid var(--color-border);
overflow-x: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.chart-page-tab,
.chart-page-chip {
.chart-page-tabs::-webkit-scrollbar { display: none; }
.chart-page-tab {
flex: 0 0 auto;
min-height: 44px;
min-width: 44px;
padding: 0 var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-canvas);
padding: 0 var(--space-1);
border: 0;
border-bottom: 2px solid transparent;
border-radius: 0;
background: transparent;
color: var(--color-ink-secondary);
cursor: pointer;
font-size: var(--type-body-sm);
font-weight: 500;
white-space: nowrap;
}
.chart-page-tab.is-current,
.chart-page-tab:hover { color: var(--color-ink); }
.chart-page-tab.is-current {
border-bottom-color: var(--color-focus);
color: var(--color-ink);
}
/* Vargas are a filter inside one tab, not a second navigation level, so the
chips sit a rung below the tab row: no frame, no card face, caption type. */
.chart-page-chip {
min-height: 44px;
min-width: 36px;
padding: 0 var(--space-2);
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-ink-tertiary);
cursor: pointer;
font-size: var(--type-caption);
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.chart-page-chip:hover { color: var(--color-ink-secondary); }
.chart-page-chip.is-current {
background: var(--color-selected);
border-color: var(--color-border-strong);
box-shadow: inset 2px 0 0 var(--color-focus);
color: var(--color-ink);
}
.chart-page-vedic,
@@ -4531,14 +4562,13 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
.chart-page-dasha,
.chart-page-western,
.chart-page-qizheng,
.chart-page-western-side,
.chart-page-center-compact {
.chart-page-western-side {
min-width: 0;
}
.chart-page-varga-chips {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
gap: var(--space-1);
margin-bottom: var(--space-4);
}
.chart-page-vedic-stage {
@@ -4550,40 +4580,52 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
}
.chart-page-vedic-stage .personal-report-chart-svg { margin-top: 0; }
.chart-page-vedic-stage .personal-report-chart-svg text { font-size: 16px; }
.chart-page-center-card {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: min(42%, 168px);
padding: var(--space-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--color-canvas) 92%, transparent);
color: var(--color-ink-secondary);
font-size: var(--type-overline);
font-weight: 500;
letter-spacing: 0;
line-height: 1.35;
text-align: center;
}
.chart-page-center-card p { margin: 0; }
.chart-page-center-extra { display: none; }
.chart-page-params-below {
display: none;
margin-top: var(--space-4);
/* One key/value table replaces the `.chart-page-center-card` overlay (two lines
on mobile, six on desktop, sat on the chart's geometric centre) and the
`.chart-page-params-below` card that repeated the same six lines for mobile.
The same facts were laid out three ways and switched by media query; naming
each field once removes the per-breakpoint duplication entirely. */
.chart-page-params {
min-width: 0;
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas);
color: var(--color-ink-secondary);
font-size: var(--type-body-sm);
line-height: 1.55;
}
.chart-page-params-below p { margin: 0 0 var(--space-2); }
.chart-page-planet-table-wrap { overflow-x: auto; }
.chart-page-params-table { margin: 0; }
.chart-page-param-row {
display: grid;
grid-template-columns: 5.5rem minmax(0, 1fr);
gap: var(--space-3);
align-items: baseline;
padding: var(--space-2) 0;
border-top: 1px solid var(--color-border);
}
.chart-page-param-row:first-child { border-top: 0; padding-top: 0; }
.chart-page-param-row dt {
margin: 0;
color: var(--color-ink-tertiary);
font-size: var(--type-caption);
font-weight: 500;
}
.chart-page-param-row dd {
margin: 0;
color: var(--color-ink);
font-size: var(--type-body-sm);
line-height: 1.5;
overflow-wrap: anywhere;
}
/* `min-width: 0` matters as much as the overflow: without it a grid item is
sized by its content and the table pushes the page sideways instead of
scrolling inside this box. */
.chart-page-planet-table-wrap {
min-width: 0;
overflow-x: auto;
overscroll-behavior-x: contain;
}
.chart-page-planet-table {
width: 100%;
min-width: 26rem;
border-collapse: collapse;
background: var(--color-canvas);
font-size: var(--type-body-sm);
@@ -4710,17 +4752,7 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
}
.chart-page-unavailable > p { margin: 0 0 var(--space-3); font-size: var(--type-body-md); }
@media (max-width: 767px) {
.chart-page-center-compact { display: block; }
.chart-page-center-extra { display: none; }
.chart-page-params-below { display: block; }
.chart-page-boundary-desktop { display: none; }
}
@media (min-width: 768px) {
.chart-page-center-compact { display: none; }
.chart-page-center-extra { display: block; }
.chart-page-params-below { display: none; }
.chart-page-basics-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@@ -4732,6 +4764,9 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
gap: var(--space-6);
align-items: start;
}
/* Chart and parameters sit side by side; the planet table is a full-width
row under both, because its columns need the whole measure. */
.chart-page-planet-table-wrap { grid-column: 1 / -1; }
.chart-page-basics-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@@ -4749,34 +4784,55 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
Ephemeris page — facts only, own scroll (html/body overflow hidden)
============================================================ */.ephemeris-hero,
.ephemeris-section,
.ephemeris-footer{
.ephemeris-outro{
width: min(900px, 100%);
margin-inline: auto;
}
.ephemeris-hero {
display: grid;
gap: var(--space-4);
justify-items: center;
gap: var(--space-2);
padding: var(--space-6) 0 var(--space-8);
}
.ephemeris-date-label {
margin: 0;
color: var(--color-ink-secondary);
font-size: var(--type-title-sm);
min-width: 12ch;
color: var(--color-ink);
font-family: var(--font-display);
font-size: var(--type-title-md);
font-weight: 500;
text-align: center;
}
.ephemeris-date-bar {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
justify-content: center;
gap: var(--space-2);
}
.ephemeris-date-button {
min-width: 44px;
min-height: 44px;
font-size: var(--type-title-md);
line-height: 1;
}
/* Only rendered off today, so it never needs a disabled face. */
.ephemeris-today {
min-height: 44px;
color: var(--color-ink-secondary);
font-size: var(--type-body-sm);
}
/* One card per section. The three bodies used to draw their own frames while
the section around them drew none, so 五要素 / 行运 / 未来九十天 each looked
like a different kind of object. The frame belongs to the section; what is
inside it is rows. */
.ephemeris-section {
display: grid;
gap: var(--space-3);
margin-bottom: var(--space-10);
margin-bottom: var(--space-6);
padding: var(--space-5);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas);
}
.ephemeris-section h2 {
margin: 0;
@@ -4806,10 +4862,10 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
display: grid;
gap: var(--space-2);
margin: 0;
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas);
padding: var(--space-3);
border: 0;
border-radius: var(--radius-sm);
background: var(--color-canvas-muted);
}
.ephemeris-panchanga-item dt {
margin: 0;
@@ -4834,9 +4890,7 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
}
.ephemeris-transits {
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas);
border-radius: var(--radius-sm);
}
.ephemeris-transits-head,
.ephemeris-transits-row {
@@ -4844,11 +4898,11 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
grid-template-columns: 4.5rem minmax(0, 1fr) 5rem 6.5rem 3rem;
gap: var(--space-3);
align-items: center;
padding: var(--space-3) var(--space-4);
padding: var(--space-3) 0;
min-height: 44px;
}
.ephemeris-transits-head {
background: var(--color-canvas-muted);
border-bottom: 1px solid var(--color-border);
color: var(--color-ink-tertiary);
font-size: var(--type-caption);
font-weight: 500;
@@ -4865,9 +4919,7 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
padding: 0;
list-style: none;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas);
border-radius: var(--radius-sm);
}
.ephemeris-events li {
display: grid;
@@ -4875,7 +4927,7 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
gap: var(--space-4);
align-items: center;
min-height: 44px;
padding: var(--space-3) var(--space-4);
padding: var(--space-3) 0;
border-top: 1px solid var(--color-border);
font-size: var(--type-body-sm);
}
@@ -4884,17 +4936,10 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
color: var(--color-ink-tertiary);
font-size: var(--type-caption);
}
.ephemeris-footer {
display: grid;
gap: var(--space-4);
padding: var(--space-6);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas);
}
.ephemeris-footer p {
.ephemeris-outro {
margin: 0;
color: var(--color-ink-secondary);
padding-bottom: var(--space-4);
color: var(--color-ink-tertiary);
font-size: var(--type-body-sm);
}
.ephemeris-ask { min-height: 44px; width: fit-content; }
@@ -4915,21 +4960,21 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
.ephemeris-message p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-body-sm); }
@media (max-width: 767px) {
.ephemeris-date-bar { display: grid; grid-template-columns: 1fr 1fr 1fr; }
.ephemeris-date-button { width: 100%; }
/* The arrows stay arrow-sized and the date keeps the middle: stretching two
glyph buttons to a third of the width each is what made the old bar read as
three equal actions. */
.ephemeris-date-bar { gap: var(--space-1); }
.ephemeris-panchanga {
grid-template-columns: 1fr;
gap: 0;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-canvas);
border-radius: var(--radius-sm);
}
.ephemeris-panchanga-item {
grid-template-columns: 5.5rem minmax(0, 1fr);
align-items: center;
gap: var(--space-4);
padding: var(--space-4);
padding: var(--space-3) 0;
border: 0;
border-top: 1px solid var(--color-border);
border-radius: 0;
@@ -4941,7 +4986,7 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
.ephemeris-transits-row {
grid-template-columns: 1fr 1fr;
gap: var(--space-2) var(--space-4);
padding: var(--space-4);
padding: var(--space-3) 0;
}
.ephemeris-transits-row span::before {
content: attr(data-label);
@@ -4951,6 +4996,7 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
font-weight: 500;
}
.ephemeris-events li { grid-template-columns: 1fr; gap: var(--space-1); }
.ephemeris-section { padding: var(--space-4); }
.ephemeris-ask { width: 100%; }
}
@@ -74,10 +74,6 @@ export function ChartPageView({
</section>
) : (
<div className="chart-page-body">
{/* BUG-717 copy: a cost and speed promise that also prints on the
failure page. It used to sit in the hero as an eyebrow; the hero is
gone, the promise is not. */}
<p className="chart-page-eyebrow">{CHART_VIEW_COPY.eyebrow}</p>
<div className="chart-page-tabs" role="tablist" aria-label="星盘体系">
{CHART_VIEW_TABS.map((item) => (
<button
@@ -92,6 +88,11 @@ export function ChartPageView({
</button>
))}
</div>
{/* BUG-717 copy: a cost and speed promise that also prints on the
failure page. It used to sit in the hero as an eyebrow; the hero is
gone, the promise is not. It reads under the tab row now, because
the tab row is what the header hands off to. */}
<p className="chart-page-eyebrow">{CHART_VIEW_COPY.eyebrow}</p>
<section className="chart-page-panel" role="tabpanel">
{tab === "vedic" ? (
<ChartVedicTab
@@ -1,31 +0,0 @@
import type { ChartViewOk } from "@/lib/chart-view-contract";
import { formatCoord } from "./chart-format";
export function ChartParamLines({
view,
compact,
}: {
view: ChartViewOk;
compact?: boolean;
}) {
const { profile } = view;
const place = `${profile.placeLabel} · ${formatCoord(profile.latitude, profile.longitude)}`;
if (compact) {
return (
<>
<p> {profile.ayanamsaDisplay}</p>
<p>{profile.nodeModeLabel}</p>
</>
);
}
return (
<>
<p>{profile.time} · {profile.birthTimeStatusLabel}</p>
<p>{place}</p>
<p> {profile.ayanamsaDisplay}</p>
<p>{profile.nodeModeLabel}</p>
<p> {profile.ascendantSignLabel} · 宿 {profile.moonNakshatra} {profile.moonPada}</p>
<p>{profile.currentDashaLabel} · {profile.engineName}</p>
</>
);
}
@@ -0,0 +1,40 @@
import type { ChartViewOk } from "@/lib/chart-view-contract";
import { formatCoord } from "./chart-format";
/**
* The chart parameters as one key/value table.
*
* They used to be rendered three times over: a `.chart-page-center-card`
* floating on the geometric centre of the North Indian chart carried two lines
* on mobile and six on desktop, and a `.chart-page-params-below` card repeated
* the same six lines for mobile only. One set of facts, three renders, switched
* by media queries — and every line was a bare sentence with its label fused to
* its value (「岁差 Raman」). A table names each field once and scans down one
* column, so nothing needs duplicating per breakpoint.
*
* Every value here is passed through from the server projection; nothing on
* this page is computed in the browser.
*/
export function ChartParamTable({ view }: { view: ChartViewOk }) {
const { profile } = view;
const rows: ReadonlyArray<readonly [string, string]> = [
["出生时间", `${profile.time} · ${profile.birthTimeStatusLabel}`],
["出生地", `${profile.placeLabel} · ${formatCoord(profile.latitude, profile.longitude)}`],
["岁差", profile.ayanamsaDisplay],
["交点", profile.nodeModeLabel],
["上升", profile.ascendantSignLabel],
["月宿", `${profile.moonNakshatra}${profile.moonPada}`],
["当前大运", profile.currentDashaLabel],
["引擎", profile.engineName],
];
return (
<dl className="chart-page-params-table">
{rows.map(([label, value]) => (
<div className="chart-page-param-row" key={label}>
<dt>{label}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
);
}
@@ -27,29 +27,31 @@ export function ChartQizhengTab({ view }: { view: ChartViewOk }) {
<p className="chart-page-note">{qizheng.ketuMode} · {qizheng.coordinateSystem}</p>
<Rules />
<QizhengPalaceGrid qizheng={qizheng} />
<table className="chart-page-planet-table">
<caption>宿</caption>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">宿</th>
<th scope="col">宿</th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{qizheng.bodies.map((body) => (
<tr key={body.name}>
<th scope="row">{body.name}</th>
<td>{body.mansion}</td>
<td>{body.mansionDegree.toFixed(2)}</td>
<td>{body.palace}</td>
<td>{body.dignityClosed ? body.dignity : `${body.dignity}(未闭合)`}</td>
<div className="chart-page-planet-table-wrap">
<table className="chart-page-planet-table">
<caption>宿</caption>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">宿</th>
<th scope="col">宿</th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{qizheng.bodies.map((body) => (
<tr key={body.name}>
<th scope="row">{body.name}</th>
<td>{body.mansion}</td>
<td>{body.mansionDegree.toFixed(2)}</td>
<td>{body.palace}</td>
<td>{body.dignityClosed ? body.dignity : `${body.dignity}(未闭合)`}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
@@ -4,7 +4,7 @@ import { VedicChartSvg } from "@/components/personal-report/vedic-chart-svg";
import type { ChartViewOk } from "@/lib/chart-view-contract";
import { CHART_VIEW_COPY, VARGA_CHIP_ORDER } from "@/lib/chart-view-labels";
import { formatDegree } from "./chart-format";
import { ChartParamLines } from "./chart-param-lines";
import { ChartParamTable } from "./chart-param-table";
export function ChartVedicTab({
view,
@@ -45,17 +45,12 @@ export function ChartVedicTab({
<div className="chart-page-vedic-layout">
<div className="chart-page-vedic-stage">
<VedicChartSvg chart={varga.chart} ariaLabel={`${varga.id} 北印度盘 ${varga.meaning}`} />
<div className="chart-page-center-card">
<div className="chart-page-center-compact">
<ChartParamLines view={view} compact />
</div>
<div className="chart-page-center-extra">
<ChartParamLines view={view} />
</div>
</div>
</div>
<div className="chart-page-params-below">
<ChartParamLines view={view} />
<div className="chart-page-params">
<ChartParamTable view={view} />
{/* The accuracy boundary sits directly under the parameters it
qualifies. Server copy, reproduced verbatim — it says what this
chart can and cannot be read for. */}
<p className="chart-page-boundary">{view.vedic.boundary}</p>
</div>
<div className="chart-page-planet-table-wrap">
@@ -83,7 +78,6 @@ export function ChartVedicTab({
</div>
</div>
)}
{varga ? <p className="chart-page-boundary chart-page-boundary-desktop">{view.vedic.boundary}</p> : null}
</div>
);
}
@@ -26,24 +26,26 @@ export function ChartWesternTab({ view }: { view: ChartViewOk }) {
</section>
<section>
<h3></h3>
<table className="chart-page-planet-table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{western.aspects.slice(0, 12).map((aspect) => (
<tr key={`${aspect.left}-${aspect.right}-${aspect.aspect}`}>
<td>{aspect.left} · {aspect.right}</td>
<td>{aspect.aspectLabel}</td>
<td>{aspect.orb.toFixed(1)}°</td>
<div className="chart-page-planet-table-wrap">
<table className="chart-page-planet-table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{western.aspects.slice(0, 12).map((aspect) => (
<tr key={`${aspect.left}-${aspect.right}-${aspect.aspect}`}>
<td>{aspect.left} · {aspect.right}</td>
<td>{aspect.aspectLabel}</td>
<td>{aspect.orb.toFixed(1)}°</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<p> {western.houseSystem === "P" ? "Placidus" : western.houseSystem}</p>
</div>
@@ -126,18 +126,42 @@ export function EphemerisView(props: {
>
<div className="ephemeris-body">
<section className="ephemeris-hero">
<p className="ephemeris-date-label">{props.date ? formatEphemerisDate(props.date) : ""}</p>
{/* ` 日期 `: the date is the subject, the two arrows are its handles.
Three equal side-by-side buttons made 前一天 / 今天 / 后一天 read as
three peers and pushed the date itself out of the group entirely.
The arrow glyphs are decorative — the direction words stay as the
accessible names, so nothing is lost to a screen reader. */}
<div className="ephemeris-date-bar" role="group" aria-label="切换日期">
<Button type="button" variant="outline" className="ephemeris-date-button" onClick={props.onPrevious}>
{EPHEMERIS_COPY.previousDay}
<Button
type="button"
variant="outline"
size="icon"
className="ephemeris-date-button"
aria-label={EPHEMERIS_COPY.previousDay}
onClick={props.onPrevious}
>
<span aria-hidden="true"></span>
</Button>
<Button type="button" variant="outline" className="ephemeris-date-button" onClick={props.onToday} disabled={isToday}>
{EPHEMERIS_COPY.today}
</Button>
<Button type="button" variant="outline" className="ephemeris-date-button" onClick={props.onNext}>
{EPHEMERIS_COPY.nextDay}
<p className="ephemeris-date-label">{props.date ? formatEphemerisDate(props.date) : ""}</p>
<Button
type="button"
variant="outline"
size="icon"
className="ephemeris-date-button"
aria-label={EPHEMERIS_COPY.nextDay}
onClick={props.onNext}
>
<span aria-hidden="true"></span>
</Button>
</div>
{/* On today there is nowhere to go, so the control is absent rather than
present-but-disabled: a greyed button still asks to be read and
still has to be tabbed past. */}
{isToday ? null : (
<Button type="button" variant="ghost" className="ephemeris-today" onClick={props.onToday}>
{EPHEMERIS_COPY.today}
</Button>
)}
</section>
<section className="ephemeris-section" aria-labelledby="ephemeris-panchanga-heading">
@@ -203,9 +227,10 @@ export function EphemerisView(props: {
)}
</section>
<section className="ephemeris-footer">
<p>{EPHEMERIS_COPY.footerNote}</p>
</section>
{/* The closing note used to be a fourth boxed card competing with the
three that carry data. The sentence is unchanged; it is a quiet line
under the page now, and the 「带这天去提问」 action lives in the header. */}
<p className="ephemeris-outro">{EPHEMERIS_COPY.footerNote}</p>
</div>
</SecondaryShell>
);