feat(chart): chart-type strip + 星盘类型 sheet, D40/D45/D60, precision hint (T2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f5149d10ca
commit
2071dc7c18
@@ -11,7 +11,7 @@ import {
|
||||
type ChartViewTabId,
|
||||
} from "@/lib/chart-view-contract";
|
||||
import type { ChartViewLayer } from "@/lib/chart-view-engine";
|
||||
import { CHART_VIEW_COPY } from "@/lib/chart-view-labels";
|
||||
import { CHART_VIEW_COPY, type ChartTypeId } from "@/lib/chart-view-labels";
|
||||
import { CHART_SETTINGS_HREF } from "@/lib/settings-url";
|
||||
import { chartLayerFailureCopy } from "@/lib/chart-view-failure";
|
||||
import { ChartPageSkeleton } from "./chart-page-skeleton";
|
||||
@@ -32,6 +32,15 @@ export function ChartPageRoute() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The lazy layer a chart type needs. D1 and the Moon chart are projected from
|
||||
* the natal packet that is already on the page, so they request nothing.
|
||||
*/
|
||||
function chartTypeLayer(id: ChartTypeId): ChartViewLayer | null {
|
||||
if (id === "D1" || id === "MOON" || id === "BHAVA" || id === "TRANSIT") return null;
|
||||
return "varga";
|
||||
}
|
||||
|
||||
function WaitingNote({ copy }: { copy: string }) {
|
||||
return <p className="chart-page-note">{copy}</p>;
|
||||
}
|
||||
@@ -63,7 +72,7 @@ export function ChartPageView({
|
||||
initialTab?: ChartViewTabId;
|
||||
}) {
|
||||
const [tab, setTab] = useState<ChartViewTabId>(initialTab);
|
||||
const [vargaId, setVargaId] = useState("D1");
|
||||
const [vargaId, setVargaId] = useState<ChartTypeId>("D1");
|
||||
|
||||
function selectTab(id: ChartViewTabId) {
|
||||
setTab(id);
|
||||
@@ -71,9 +80,10 @@ export function ChartPageView({
|
||||
if (id === "western") onNeedLayer?.("western");
|
||||
}
|
||||
|
||||
function selectVarga(id: string) {
|
||||
function selectVarga(id: ChartTypeId) {
|
||||
setVargaId(id);
|
||||
if (id !== "D1") onNeedLayer?.("varga");
|
||||
const layer = chartTypeLayer(id);
|
||||
if (layer) onNeedLayer?.(layer);
|
||||
}
|
||||
|
||||
const birthline = view != null && view.status === "ok"
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
CHART_TYPE_ORDER,
|
||||
CHART_TYPE_SHEET_COPY,
|
||||
CHART_TYPE_STRIP_LABEL,
|
||||
CHART_VIEW_COPY,
|
||||
type ChartTypeId,
|
||||
} from "@/lib/chart-view-labels";
|
||||
import { vargaMeaning } from "@/lib/chart-view-varga";
|
||||
|
||||
function sheetCopy(id: ChartTypeId): { label: string; use: string | null } {
|
||||
const fixed = CHART_TYPE_SHEET_COPY[id];
|
||||
if (fixed) return fixed;
|
||||
return { label: id, use: vargaMeaning(Number(id.slice(1))) };
|
||||
}
|
||||
|
||||
/**
|
||||
* The 「星盘类型」 sheet: every chart type in a three-column grid. A native
|
||||
* `<dialog>` so Esc, the backdrop and the focus trap come from the platform;
|
||||
* focus returns to the 「≡」 button that opened it.
|
||||
*/
|
||||
function ChartTypeSheet({ current, onSelect, onClose }: {
|
||||
current: string;
|
||||
onSelect: (id: ChartTypeId) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const dialog = useRef<HTMLDialogElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const element = dialog.current;
|
||||
const previous = document.activeElement;
|
||||
element?.showModal();
|
||||
return () => {
|
||||
element?.close();
|
||||
if (previous instanceof HTMLElement && previous.isConnected) previous.focus({ preventScroll: true });
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref={dialog}
|
||||
className="chart-page-type-sheet"
|
||||
aria-labelledby="chart-page-type-sheet-title"
|
||||
onCancel={(event) => { event.preventDefault(); onClose(); }}
|
||||
onClick={(event) => { if (event.target === event.currentTarget) onClose(); }}
|
||||
>
|
||||
<div className="chart-page-type-sheet-panel">
|
||||
<header className="chart-page-type-sheet-header">
|
||||
<h2 id="chart-page-type-sheet-title">{CHART_VIEW_COPY.chartTypeSheetTitle}</h2>
|
||||
<button type="button" className="chart-page-type-sheet-close" aria-label={CHART_VIEW_COPY.chartTypeSheetClose} onClick={onClose}>
|
||||
<X aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
<div className="chart-page-type-grid">
|
||||
{CHART_TYPE_ORDER.map((id) => {
|
||||
const copy = sheetCopy(id);
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
aria-pressed={id === current}
|
||||
className={id === current ? "chart-page-type-cell is-current" : "chart-page-type-cell"}
|
||||
onClick={() => onSelect(id)}
|
||||
>
|
||||
<strong>{copy.label}</strong>
|
||||
{copy.use ? <small>{copy.use}</small> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chart-type strip + 「≡」. The strip scrolls sideways and never wraps; the
|
||||
* button stays pinned at the row's end and opens the full sheet.
|
||||
*/
|
||||
export function ChartTypePicker({ current, onSelect }: {
|
||||
current: string;
|
||||
onSelect: (id: ChartTypeId) => void;
|
||||
}) {
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const buttons = useRef(new Map<ChartTypeId, HTMLButtonElement>());
|
||||
|
||||
function chooseFromSheet(id: ChartTypeId) {
|
||||
setSheetOpen(false);
|
||||
onSelect(id);
|
||||
// A choice made in the sheet may sit far along the strip: bring it into view.
|
||||
buttons.current.get(id)?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chart-page-type-bar">
|
||||
<div className="chart-page-type-strip" role="tablist" aria-label="盘型">
|
||||
{CHART_TYPE_ORDER.map((id) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
role="tab"
|
||||
ref={(node) => {
|
||||
if (node) buttons.current.set(id, node);
|
||||
else buttons.current.delete(id);
|
||||
}}
|
||||
aria-selected={id === current}
|
||||
className={id === current ? "chart-page-chip is-current" : "chart-page-chip"}
|
||||
onClick={() => onSelect(id)}
|
||||
>
|
||||
{CHART_TYPE_STRIP_LABEL[id] ?? id}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="chart-page-type-menu"
|
||||
aria-label={CHART_VIEW_COPY.chartTypeMenu}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={sheetOpen}
|
||||
onClick={() => setSheetOpen(true)}
|
||||
>
|
||||
<Menu aria-hidden="true" />
|
||||
</button>
|
||||
{sheetOpen ? (
|
||||
<ChartTypeSheet
|
||||
current={current}
|
||||
onClose={() => setSheetOpen(false)}
|
||||
onSelect={chooseFromSheet}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import type {
|
||||
ChartViewVargaAscendantRow,
|
||||
ChartViewVargaRow,
|
||||
} from "@/lib/chart-view-contract";
|
||||
import { CHART_VIEW_COPY, VARGA_CHIP_ORDER } from "@/lib/chart-view-labels";
|
||||
import { BIRTH_TIME_SENSITIVE_VARGAS, CHART_VIEW_COPY, type ChartTypeId } from "@/lib/chart-view-labels";
|
||||
import { ChartTypePicker } from "./chart-type-picker";
|
||||
import {
|
||||
ASCENDANT_COLOR_CLASS,
|
||||
ASCENDANT_MARK,
|
||||
@@ -183,6 +184,11 @@ export function VargaTable({ varga }: { varga: ChartViewVarga }) {
|
||||
);
|
||||
}
|
||||
|
||||
function PrecisionHint({ view, id }: { view: ChartViewOk; id: string }) {
|
||||
if (!BIRTH_TIME_SENSITIVE_VARGAS.has(id) || view.profile.birthTimeStatus === "confirmed") return null;
|
||||
return <p className="chart-page-note chart-page-precision-hint">{CHART_VIEW_COPY.precisionHint}</p>;
|
||||
}
|
||||
|
||||
export function ChartVedicTab({
|
||||
view,
|
||||
vargaId,
|
||||
@@ -191,10 +197,11 @@ export function ChartVedicTab({
|
||||
failure,
|
||||
}: {
|
||||
view: ChartViewOk;
|
||||
/** The selected chart type (`CHART_TYPE_ORDER`): a D-number, `MOON`, `BHAVA` or `TRANSIT`. */
|
||||
vargaId: string;
|
||||
pending?: boolean;
|
||||
failure?: ChartViewFailureStatus;
|
||||
onSelectVarga: (id: string) => void;
|
||||
onSelectVarga: (id: ChartTypeId) => void;
|
||||
}) {
|
||||
const selected = view.vedic.vargas.find((item) => item.id === vargaId);
|
||||
const natal = view.vedic.vargas.find((item) => item.id === "D1") ?? view.vedic.vargas[0];
|
||||
@@ -203,23 +210,7 @@ export function ChartVedicTab({
|
||||
|
||||
return (
|
||||
<div className="chart-page-vedic">
|
||||
<div className="chart-page-varga-chips" role="tablist" aria-label="分盘">
|
||||
{VARGA_CHIP_ORDER.map((division) => {
|
||||
const id = `D${division}`;
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={id === vargaId}
|
||||
className={id === vargaId ? "chart-page-chip is-current" : "chart-page-chip"}
|
||||
onClick={() => onSelectVarga(id)}
|
||||
>
|
||||
{id}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ChartTypePicker current={vargaId} onSelect={onSelectVarga} />
|
||||
{waiting || !varga ? (
|
||||
!pending && failure ? <p className="chart-page-note">{chartLayerFailureCopy(failure)}</p>
|
||||
: <ChartPageSkeleton copy={CHART_VIEW_COPY.waitingVarga} />
|
||||
@@ -238,7 +229,10 @@ export function ChartVedicTab({
|
||||
{varga.id === "D1" || !varga.rows ? (
|
||||
<NatalTable view={view} caption={`${varga.id} · ${varga.meaning}`} />
|
||||
) : (
|
||||
<VargaTable varga={varga} />
|
||||
<div className="chart-page-varga-table-block">
|
||||
<PrecisionHint view={view} id={varga.id} />
|
||||
<VargaTable varga={varga} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user