diff --git a/frontend/src/components/chart-board/chart-board.css b/frontend/src/components/chart-board/chart-board.css index 76329248..1f06e27b 100644 --- a/frontend/src/components/chart-board/chart-board.css +++ b/frontend/src/components/chart-board/chart-board.css @@ -494,7 +494,8 @@ .chart-deep-toggle svg { transition: transform .15s ease-out; } .chart-deep-toggle.is-open svg { transform: rotate(180deg); } .chart-deep-more-panel { display: grid; gap: var(--space-4); min-width: 0; } -.chart-deep-detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-6); min-width: 0; } +.chart-deep-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-6); min-width: 0; } +.chart-deep .chart-graha-table.is-pickable tbody tr.is-selected.is-now > * { background: var(--deep-now); color: var(--deep-now-ink); } .chart-deep-detail { display: grid; gap: var(--space-2); min-width: 0; align-content: start; } .chart-deep-detail h5 { margin: 0; font-size: 13px; font-weight: 600; color: var(--color-ink-secondary); } .chart-deep-actions { display: flex; justify-content: flex-end; } @@ -594,6 +595,8 @@ border: 1px solid var(--color-border); border-radius: 12px; color: var(--color-ink-strong); font-size: 14px; } .chart-deep-detail-grid { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); } + .chart-deep .chart-graha-table.is-pickable tbody th, + .chart-deep .chart-graha-table.is-pickable tbody td { height: 44px; padding-top: 0; padding-bottom: 0; } .chart-deep-shadbala-row[data-slot="button"] { grid-template-columns: 22px minmax(0, 1fr) 48px; gap: 10px; } .chart-deep-glyph { font-size: 17px; } .chart-deep-meter { height: 10px; border-radius: 5px; } diff --git a/frontend/src/components/chart-board/chart-deep-reading.tsx b/frontend/src/components/chart-board/chart-deep-reading.tsx index c8cbd0e8..330fa762 100644 --- a/frontend/src/components/chart-board/chart-deep-reading.tsx +++ b/frontend/src/components/chart-board/chart-deep-reading.tsx @@ -302,58 +302,85 @@ function LifeRuler({ view, day, dayWord }: { view: ChartViewOk; day: string; day ); } +/** One level of 运期起止: name, start, end; the current row is marked, a pickable row opens the next level. */ +function PeriodTable({ title, head, spans, day, picked, onPick }: { + title: string; + head: string; + spans: readonly DashaSpan[]; + day: string; + picked?: string | null; + onPick?: (start: string) => void; +}) { + return ( +
+
{title}
+ + + + + {spans.map((span) => { + const on = onPick ? picked === span.start : false; + const isNow = span.start <= day && day < span.end; + const tone = [on ? "is-selected" : "", isNow ? "is-now" : ""].filter(Boolean).join(" "); + return ( + onPick(span.start) : undefined} + > + + + + + ); + })} + +
{head}起止
{span.label}{span.start}{span.end}
+
+
+ ); +} + +/** + * 运期起止: pick any 大运 → its 分运 → pick any 分运 → its 子运. Both picks + * start on the period containing the observation day. + */ function PeriodTables({ view, day }: { view: ChartViewOk; day: string }) { const periods = view.dasha.vimshottari.periods; const now = vimshottariAt(view, day); - const [picked, setPicked] = useState(null); - const maha = periods.find((period) => period.start === picked) ?? now?.maha ?? periods[0]; + const [pickedMaha, setPickedMaha] = useState(null); + const [pickedAntar, setPickedAntar] = useState(null); + const maha = periods.find((period) => period.start === pickedMaha) ?? now?.maha ?? periods[0]; + const antars: DashaSpan[] = maha ? maha.antardashas.map((item) => ({ lord: item.lord, label: item.label, start: item.start, end: item.end })) : []; + const antar = antars.find((item) => item.start === pickedAntar) + ?? antars.find((item) => item.start <= day && day < item.end) + ?? antars[0] + ?? null; + const pratyantars = antar ? vimshottariSplit(antar.lord, antar.start, antar.end) : []; + const mahaSpans: DashaSpan[] = periods.map((period) => ({ lord: period.lord, label: period.label, start: period.start, end: period.end })); return (
-
-
大运
- - - - - {periods.map((period) => { - const on = maha?.start === period.start; - const isNow = now?.maha.start === period.start; - return ( - setPicked(period.start)} - > - - - - - ); - })} - -
大运起止
{period.label}{period.start}{period.end}
-
-
+ { setPickedMaha(start); setPickedAntar(null); }} + /> {maha ? ( -
-
{maha.label}大运的分运
- - - - - {maha.antardashas.map((item) => ( - - - - - - ))} - -
分运起止
{item.label}{item.start}{item.end}
-
-
+ ) : null} + {antar ? : null}
); } diff --git a/frontend/tests/chart-deep-reading-20261102.test.tsx b/frontend/tests/chart-deep-reading-20261102.test.tsx index 23137a50..8432daa3 100644 --- a/frontend/tests/chart-deep-reading-20261102.test.tsx +++ b/frontend/tests/chart-deep-reading-20261102.test.tsx @@ -9,6 +9,7 @@ import type { ChartBoardState } from "../src/hooks/use-chart-board.ts"; import { natalBoard } from "../src/lib/chart-board-view.ts"; import type { BoardSelection } from "../src/lib/chart-board-model.ts"; import { assembleChartView } from "../src/lib/chart-view-load.ts"; +import { vimshottariSplit } from "../src/lib/dasha-span.ts"; import type { ChartViewOk } from "../src/lib/chart-view-contract.ts"; import { CHART_VIEW_LAYERS, type ChartViewLayer } from "../src/lib/chart-view-engine.ts"; import { cssDeclarations } from "./css-contract-test-support.ts"; @@ -249,3 +250,52 @@ test("深读 CSS: page flow, theme tokens only, 44px phone targets", () => { assert.match(block, /@media \(max-width: 640px\) \{[\s\S]*\.chart-deep-ruler-age\.is-minor \{ display: none; \}/, "ticks every 20 years on a phone"); assert.match(block, /@media \(prefers-reduced-motion: reduce\) \{[\s\S]*animation: none/); }); + +test("运期起止 drills from any 大运 to any 分运 to its 子运, current rows marked", async () => { + const view = await okView(); + const day = view.dasha.asOf; + const periods = view.dasha.vimshottari.periods; + const currentMaha = periods.find((period) => period.start <= day && day < period.end)!; + const currentAntar = currentMaha.antardashas.find((item) => item.start <= day && day < item.end)!; + const otherMaha = periods.find((period) => period.start !== currentMaha.start && period.antardashas.length > 1)!; + const otherAntar = otherMaha.antardashas[1]!; + const h = createClientLifecycleHarness(); + const rowsOf = (title: string) => { + const heading = h.elements().find((node) => node.tagName === "H5" && node.text === title); + assert.ok(heading, `missing table ${title}`); + return heading!.parentNode as unknown as { text: string }; + }; + const row = (startIso: string, label: string) => h.elements().find((node) => node.tagName === "TR" && node.text.startsWith(`${label}${startIso}`)); + try { + await h.render(React.createElement(ChartDeepReading, { + view, + board: fakeBoard(view), + observe: day, + onObserve: () => {}, + transit: { transit: null, pending: false, failed: false }, + })); + const toggle = h.elements().find((node) => node.tagName === "BUTTON" && node.text === "看运期起止和第二条运法")!; + await h.event(toggle); + // Default: the current 大运 and 分运, and the current 分运's 子运 with start and end. + const sub = vimshottariSplit(currentAntar.lord, currentAntar.start, currentAntar.end); + let box = rowsOf(`${currentAntar.label}分运的子运`); + for (const span of sub) assert.ok(box.text.includes(`${span.label}${span.start}${span.end}`), span.label); + const nowRows = h.elements().filter((node) => node.tagName === "TR" && node.getAttribute("aria-current") === "date"); + assert.equal(nowRows.length, 3, "one current row per level"); + // Any 大运 → any 分运 → its 子运. + await h.event(row(otherMaha.start, otherMaha.label)!); + rowsOf(`${otherMaha.label}大运的分运`); + await h.event(row(otherAntar.start, otherAntar.label)!); + box = rowsOf(`${otherAntar.label}分运的子运`); + for (const span of vimshottariSplit(otherAntar.lord, otherAntar.start, otherAntar.end)) { + assert.ok(box.text.includes(`${span.label}${span.start}${span.end}`), span.label); + } + const picked = h.elements().filter((node) => node.tagName === "TR" && node.getAttribute("aria-selected") === "true"); + assert.equal(picked.length, 2, "the picked 大运 and 分运"); + // The 逐级放大 bars still show the current periods. + assert.match(h.container.text, new RegExp(`${currentMaha.label}大运`)); + assert.match(h.container.text, /今天 · 已走 \d+%/); + } finally { + await h.close(); + } +}); diff --git a/frontend/tests/chart-page-view.test.tsx b/frontend/tests/chart-page-view.test.tsx index f29f06ff..60817c58 100644 --- a/frontend/tests/chart-page-view.test.tsx +++ b/frontend/tests/chart-page-view.test.tsx @@ -535,7 +535,7 @@ test("varga pending shows the empty shared chart and failure replaces it", async // 原因: 2026-11-02 深读改版,第二条运法收在展开按钮后面 test("western and chara failures are terminal copy rather than waiting", async () => { const view = await okView(); - const western = withSidebarProvider(React.createElement(ChartPageView, { view, initialTab: "western", layerFailures: new Map([["western", "engine_busy" as const]]) })); + const western = withSidebarProvider(React.createElement(ChartPageView, { view, initialTab: "western", layerFailures: new Map([["western", "engine_busy"]]) })); assert.ok(western.includes(CHART_VIEW_COPY.busy)); assert.doesNotMatch(western, /data-skeleton|这一栏还没拿到/); const h = createClientLifecycleHarness(); try {