diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 09e1215a..548a7426 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -4962,12 +4962,132 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class .chart-page-western-side { min-width: 0; } -.chart-page-varga-chips { +/* Chart-type strip (TASK-chart-types D2): one line that scrolls sideways and + never wraps, with the 「≡」 button pinned at the end outside the scroller. */ +.chart-page-type-bar { display: flex; - flex-wrap: wrap; + align-items: center; gap: var(--space-1); + min-width: 0; margin-bottom: var(--space-4); } +.chart-page-type-strip { + display: flex; + flex: 1 1 auto; + flex-wrap: nowrap; + gap: var(--space-1); + min-width: 0; + overflow-x: auto; + overscroll-behavior-x: contain; + scrollbar-width: none; +} +.chart-page-type-strip::-webkit-scrollbar { display: none; } +.chart-page-type-strip .chart-page-chip { flex: 0 0 auto; white-space: nowrap; } +.chart-page-type-menu { + display: inline-grid; + flex: 0 0 auto; + place-items: center; + width: 44px; + height: 44px; + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-ink-secondary); + cursor: pointer; +} +.chart-page-type-menu:hover { color: var(--color-ink); background: var(--color-canvas-muted); } +.chart-page-type-menu:focus-visible, +.chart-page-type-cell:focus-visible, +.chart-page-type-sheet-close:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; } +.chart-page-type-menu svg { width: 20px; height: 20px; } +/* The sheet: bottom drawer on phones, centred 720px dialog from 860px — the + same frame as the report's 分块导出 drawer. */ +.chart-page-type-sheet { + position: fixed; + inset: auto 0 0 0; + width: min(760px, 100%); + max-width: none; + max-height: 80vh; + margin: 0 auto; + padding: 0; + border: 1px solid var(--color-border); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + background: var(--color-canvas); + color: var(--color-ink); + box-shadow: var(--shadow-elevated); + overflow: hidden; +} +.chart-page-type-sheet::backdrop { background: var(--color-scrim); } +.chart-page-type-sheet-panel { display: flex; flex-direction: column; max-height: 80vh; } +@supports (height: 1dvh) { + .chart-page-type-sheet, .chart-page-type-sheet-panel { max-height: 80dvh; } +} +.chart-page-type-sheet-header { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--color-border); +} +.chart-page-type-sheet-header h2 { margin: 0; font-size: var(--type-title-lg); } +.chart-page-type-sheet-close { + display: inline-grid; + place-items: center; + width: 44px; + height: 44px; + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-ink-secondary); + cursor: pointer; +} +.chart-page-type-sheet-close svg { width: 20px; height: 20px; } +.chart-page-type-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-2); + padding: var(--space-3) var(--space-4) max(var(--space-4), env(safe-area-inset-bottom)); + overflow-y: auto; + overscroll-behavior: contain; +} +.chart-page-type-cell { + display: flex; + min-width: 0; + min-height: 56px; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + padding: var(--space-2); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-canvas); + color: var(--color-ink); + cursor: pointer; +} +.chart-page-type-cell strong { font-size: var(--type-body-md); font-weight: 500; font-variant-numeric: tabular-nums; } +.chart-page-type-cell small { + max-width: 100%; + overflow: hidden; + color: var(--color-ink-tertiary); + font-size: var(--type-caption); + text-overflow: ellipsis; + white-space: nowrap; +} +.chart-page-type-cell:hover { background: var(--color-canvas-muted); } +.chart-page-type-cell.is-current { border-color: var(--color-ink); background: var(--color-selected); } +@media (min-width: 860px) { + .chart-page-type-sheet { + inset: 0; + width: min(720px, 92vw); + max-height: 80vh; + margin: auto; + border-radius: var(--radius-lg); + } +} +@media print { .chart-page-type-sheet { display: none !important; } } +.chart-page-precision-hint { margin: 0 0 var(--space-2); } .chart-page-waiting-plot { width: min(100%, 400px); min-width: 0; diff --git a/frontend/src/components/chart-page/chart-page-view.tsx b/frontend/src/components/chart-page/chart-page-view.tsx index cfbd5361..b717bba1 100644 --- a/frontend/src/components/chart-page/chart-page-view.tsx +++ b/frontend/src/components/chart-page/chart-page-view.tsx @@ -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
{copy}
; } @@ -63,7 +72,7 @@ export function ChartPageView({ initialTab?: ChartViewTabId; }) { const [tab, setTab] = useState