From fc2283693df92fc59dfdc7a8f907d288c803822c Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Fri, 23 Oct 2026 01:22:46 +0800 Subject: [PATCH] fix(chart): span natal and moon planet tables across the row VedicLayout now owns the third grid item, so a wrapper around the table cannot drop the natal or Moon table into the left column. Desktop grid-column targets that row. BUG-1421. Self-test: tsc exit 0, lint 0 errors, 49 targeted tests, next build with / static. Co-Authored-By: Grok (agent loop) --- CHANGELOG.md | 5 + docs/BUG_HISTORY.md | 16 +++ .../PROGRESS-chart-table-span-20261023.md | 30 ++++++ docs/tasks/README.md | 2 +- docs/testing/chart-table-span-20261023.md | 6 ++ frontend/DESIGN.md | 2 +- frontend/src/app/globals.css | 12 +-- .../components/chart-page/chart-vedic-tab.tsx | 4 +- .../tests/chart-table-span-20261023.test.tsx | 97 +++++++++++++++++++ 9 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 docs/tasks/PROGRESS-chart-table-span-20261023.md create mode 100644 docs/testing/chart-table-span-20261023.md create mode 100644 frontend/tests/chart-table-span-20261023.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a9bd01a..42ce8858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 印度占星 Skill 更新日志 +## 2026-10-23 — 星盘页本命和月亮的行星表重新占满整行 + +- 宽屏下,本命和月亮的行星表回到参数卡旁边那一整行,宿主、pada、状态不用横拖就能看见。分盘、Bhava、行运仍是整行。 +- Skill 版本不变(BUG-1421)。 + ## 2026-10-22 — 星盘大运右栏可以切到 Yogini - 大运页左栏仍是 Vimshottari。右栏默认 Chara,可以切到 Yogini。点开当前大运能看到小运。Yogini 的小运下面写「小运未经外部软件核对。」 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 1b543a01..781f688b 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -19253,3 +19253,19 @@ - 相关记录:无 - 复发自:无 - 修复版本:本提交 + +## BUG-1421 | 星盘页本命和月亮的行星表被挤进左栏 + +- 状态:resolved +- 首次发现:2026-10-22 +- 最近更新:2026-10-23 +- 影响面:星盘页桌面宽度下的本命表和月亮盘表 +- 用户现象:宽屏下选「本命」或「月亮」时,行星表只占左栏。宿主、pada、状态三列要横拖才看得到,右栏在参数卡下面空着。分盘、Bhava、行运的表正常。 +- 触发条件:桌面宽度(≥1024px)打开星盘页的本命或月亮。来自 #87 给这两张表包了一层符号说明。 +- 根因:跨栏按类名而非结构。桌面规则只认表格外层的两个类名。本命和月亮的直接子项变成了符号说明那一层,规则匹配不上,表就排进左栏。 +- 修复:网格自己用固定的整行容器装表格,跨栏只认这个容器。表格内容、列数、列宽不变。 +- 验证:`frontend/tests/chart-table-span-20261023.test.tsx`。五种盘型的网格都是三个直接子项,第三个是整行容器;桌面样式只让这个容器跨栏。 +- 防复发:上面两条断言。再往表外包一层,结构断言会失败;跨栏不再跟着表格类名走。 +- 相关记录:BUG-1083(窄屏仍只能在表自己的盒子里横滚)。issue #102。回归来源 #87。 +- 复发自:无 +- 修复版本:`codex/chart-table-span-20261023` diff --git a/docs/tasks/PROGRESS-chart-table-span-20261023.md b/docs/tasks/PROGRESS-chart-table-span-20261023.md new file mode 100644 index 00000000..893428e1 --- /dev/null +++ b/docs/tasks/PROGRESS-chart-table-span-20261023.md @@ -0,0 +1,30 @@ +# PROGRESS · 星盘页行星表整行(2026-10-23) + +Issue #102。BUG-1421。 + +## 基线 + +- 开工时 `origin/staging` = `d72adb5754eab4d66ac3a47c4edcb12704e29815`(任务书写的 `9049b5e99` 已过期)。 +- 本文件最大编号是 BUG-1420,本单用 BUG-1421。 + +## 做了什么 + +- 星盘页网格的第三格改成固定的整行容器。本命、月亮、分盘、Bhava、行运的表都放在里面。 +- 桌面跨栏只认这个容器。表格自己的类名不再负责跨栏。 +- 容器可以缩到比内容窄,窄屏时表仍在自己的框里横滑。 +- 表格的字、列、列宽没改。符号说明的气泡还在原来的位置。 + +## 自测 + +| 检查 | 命令 | 结果 | +| --- | --- | --- | +| 类型 | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 | +| lint | `frontend` 下 `npm run lint` | 退出码 0。0 error,122 条既有 warning,本轮不改 | +| 定向测试 | `node node_modules/tsx/dist/cli.mjs --test` 跑新结构测试,加上 `chart-page-view.test.tsx`、`chart-varga-table.test.tsx`、`chart-page-mobile-width-20260929.test.ts` | 49 条通过,0 失败 | +| 构建 | `frontend` 下 `npm run build` | 退出码 0。路由表 `/` 为 `○ Static`。预渲染 `index.html` 引用 33 个 JS、3 个 CSS,gzip-9 合计 646,234 字节(JS 575,154,CSS 71,080)。没有用改动前的代码再构建一次,所以不写增减百分比 | + +未跑前端全量 `npm test`(验收时跑)。没有登录,本机浏览器截图没做,留给 `docs/testing/chart-table-span-20261023.md`。 + +## 偏离 + +- 无。编号说明:本分支的 BUG-1421 与尚未合并的 #95 分支各自占用了同一个号。若 #95 先进入 staging,验收时需要给本条改号。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index f0cadaaf..92fe64c4 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -38,7 +38,7 @@ | 任务书 | 进度 | 主题 | 状态 | 落点 | | --- | --- | --- | --- | --- | | `TASK-rectification-explicit-open-20261023.md` | — | **明说要校正只回一句话(issue #103)**:找会话直接读一张对服务角色撤权的表、必然失败被吞(真正进行中的也续不上);`candidate_accepted` 被算作进行中挡住入口和提议卡;兜底只给一句话。产品选 C;只改入口口径、经 RPC 找会话、删兜底,不改共享状态集合、不动表 | 待执行(循环) | 分支 `codex/rectification-explicit-open-20261023` | -| `TASK-chart-table-span-20261023.md` | — | **星盘页本命 / 月亮行星表挤进左栏(issue #102)**:#87 给表包了 `ChartSymbolHelp` 后跨栏规则失配;根因修法是 `VedicLayout` 用固定整行容器承载表格,跨栏只认容器;加结构断言防复发 | 待执行(循环) | 分支 `codex/chart-table-span-20261023` | +| `TASK-chart-table-span-20261023.md` | `PROGRESS-chart-table-span-20261023.md` | **星盘页本命 / 月亮行星表挤进左栏(issue #102)**:#87 给表包了 `ChartSymbolHelp` 后跨栏规则失配;根因修法是 `VedicLayout` 用固定整行容器承载表格,跨栏只认容器;加结构断言防复发。BUG-1421 | 待验收 | 分支 `codex/chart-table-span-20261023` | | `TASK-ui-kit-migration-20261023.md` | — | **界面统一换成组件库**:shadcn base-nova(补 `shadcn/tailwind.css`)· Lucide · Motion · react-resizable-panels · dnd-kit · Zustand;lint 拦手写控件(白名单只减不增);对话旁星盘面板;会话、档案拖动排序(动表);长相不变。产品 2026-10-23 拍板 D1~D8;8 个 issue(K0~K7)串行,取代全站动效 #98 #100 #101 | 待领取(循环) | 原型 https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG | | `TASK-onboarding-journey-20261022.md` | — | **新用户启程**:建档加「先看谁的盘」(自己 / 帮别人看,本人资料可后补)、人物关系字段、首问免费(服务端原子)、起步三问、启程清单(七项从数据推导,收费项标价,老用户可见)、八条一次性提示(含星盘四页签)、启程流程专用动效。产品 2026-10-22 拍板 D1~D6;动表经产品同意进循环,拆四个 issue 串行。BUG-1420 起 | 待领取(循环) | 原型 https://claude.ai/artifact/PcTwMVb9RD611R3qStxv3U | | `TASK-site-motion-20261022.md` | — | **全站动效**:换页按侧栏方向滑入、选中块跟随、页签、盘型切换星体移宫、换人、翻日期、列表补位、弹窗从触发处长出、抽屉、顶部提示、点数滚动、中英交叉淡入;重写 DESIGN §6(推翻 180 ms 上限与 Spatial Instant),不引动画库,先验证 View Transitions。产品 2026-10-22 拍板;三个 issue,排在启程(三)之后、启程(四)之前;**2026-10-23 被 ui-kit-migration 取代** | 已取代 | 原型 https://claude.ai/artifact/7u9goivnmTNSGTNWNeVMQd | diff --git a/docs/testing/chart-table-span-20261023.md b/docs/testing/chart-table-span-20261023.md new file mode 100644 index 00000000..41ce17a3 --- /dev/null +++ b/docs/testing/chart-table-span-20261023.md @@ -0,0 +1,6 @@ +# 星盘页行星表整行(2026-10-23) + +登录后打开星盘页。不要用真实出生资料做记录,用站内已有的示例盘即可。 + +1. 把窗口拉到大约 1280 宽。依次看「本命」「月亮」「D9」「Bhava」「行运」。每张表都占满盘和参数卡下面的整行,宿主、pada、状态不用横拖就能看见。 +2. 把窗口收到大约 390 宽(或手机)。页面本身不左右滚动。表如果放不下,只在表自己的框里横滑。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index b15398ac..7f1fe7a8 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -1136,7 +1136,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句 **不再按断点复制参数。** 原本同一组参数有三种画法:中宫叠一张 `.chart-page-center-card`(手机两行、桌面六行),盘下方再来一张 `.chart-page-params-below` 重复那六行,由 media query 三选一。同样的事实画三遍、靠断点开关,改一处要改三处。键值表让每个字段只出现一次,`.chart-page-center-card` / `-compact` / `-extra` / `.chart-page-params-below` / `.chart-page-boundary-desktop` 五个类连同它们的 767/768 开关块一并删除,由 `chart-page-view.test.tsx` 守着不许回来。 -桌面(≥1024)盘面与参数表左右并排,行星表是跨两列的整行——它的列宽需要完整版心。2026-09-29 起桌面印度盘宽 440px;当晚(BUG-1100,产品「框太丑」)上限从绘图区移到外框 `.chart-page-vedic-stage`(440px + 两侧 `--space-3` 内边距 + 边框),外框贴合星图,不再是一张宽卡片中间放一张窄盘;手机端外框不设上限;西洋桌面绘图区上限 480px。低于此断点的结果盘不收紧,参数区和表格不缩。等待盘与结果盘复用同一桌面列宽与绘图容器,不再使用独立 400px 等待盘上限;印度等待绘图容器以 400/452 比例预留结果图例带,内部空 SVG 仍为 400×400,避免揭幕时外框和说明区纵向跳动,不改共享绘图。只有盘位动(2026-09-30 起为宫位依次点亮,不再整张呼吸),失败后退出,参数表不新增骨架。 +桌面(≥1024)盘面与参数表左右并排,行星表是跨两列的整行——它的列宽需要完整版心。表格区由 `VedicLayout` 的整行容器承载,不靠表格自身类名跨栏。2026-09-29 起桌面印度盘宽 440px;当晚(BUG-1100,产品「框太丑」)上限从绘图区移到外框 `.chart-page-vedic-stage`(440px + 两侧 `--space-3` 内边距 + 边框),外框贴合星图,不再是一张宽卡片中间放一张窄盘;手机端外框不设上限;西洋桌面绘图区上限 480px。低于此断点的结果盘不收紧,参数区和表格不缩。等待盘与结果盘复用同一桌面列宽与绘图容器,不再使用独立 400px 等待盘上限;印度等待绘图容器以 400/452 比例预留结果图例带,内部空 SVG 仍为 400×400,避免揭幕时外框和说明区纵向跳动,不改共享绘图。只有盘位动(2026-09-30 起为宫位依次点亮,不再整张呼吸),失败后退出,参数表不新增骨架。 **符号说明(2026-09-29;当晚改为气泡,BUG-1101)。** `/chart` 开启共享 SVG 的可选交互模式:说明是标记旁的小气泡(`.chart-symbol-bubble`,最宽 220px,`--color-canvas` 底 + 细边 + `--shadow-elevated`),默认在标记上方 8px,离外框顶不足 48px 时翻到下方,左右被夹在外框内;气泡不接收指针,不会挡住旁边的标记。电脑:鼠标悬停出现、移开消失,点击与键盘聚焦同样出现。手机:轻点标记出现,触摸产生的离开事件不关它,点盘外任意处或 3 秒后自动消失。盘下不再有「停在或轻点……」提示行和说明行;读屏器仍从一个隐藏的 `role="status"` 读到同一句话;Esc、离开、失焦可关闭,切盘/人物/数据身份变化清理旧说明。行星与其逆行短线共用一个目标;西洋 caption 和命中圆用一致的行星解析,原点击过滤相位及再次点击取消保留。报告、校正、静态导出默认不启用该模式;纯绘图层不持有 hooks,静态 SVG 白名单不扩。 diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 919936f1..d707bc79 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -5808,8 +5808,9 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); } @media print { .chart-page-type-sheet { display: none !important; } } .chart-page-precision-hint, .chart-page-table-note { margin: 0 0 var(--space-2); } -/* A note + table pair is one grid item, so the table keeps its own scroll box. */ -.chart-page-varga-table-block { min-width: 0; } +/* The table row is one grid item. A note and its table stay in one scroll box. */ +.chart-page-varga-table-block, +.chart-page-vedic-tables { min-width: 0; } .chart-page-varga-table-block > .chart-page-planet-table-wrap + .chart-page-planet-table-wrap { margin-top: var(--space-4); } /* A body the Bhava boundaries move to another house. */ .chart-page-planet-table tr.is-shifted > td, @@ -6341,10 +6342,9 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); } card with empty sides around a 440px chart. 440 + the card's padding. */ .chart-page-vedic-stage { width: 100%; max-width: calc(440px + 2 * var(--space-3) + 2px); } .chart-page-western-wheel { max-width: 480px; } - /* 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, - .chart-page-varga-table-block { grid-column: 1 / -1; } + /* Chart and parameters sit side by side. The third grid item is the table + row, so a wrapper around a table cannot drop it back into the left column. */ + .chart-page-vedic-tables { grid-column: 1 / -1; } .chart-page-basics-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } diff --git a/frontend/src/components/chart-page/chart-vedic-tab.tsx b/frontend/src/components/chart-page/chart-vedic-tab.tsx index feffb016..3eac0bdb 100644 --- a/frontend/src/components/chart-page/chart-vedic-tab.tsx +++ b/frontend/src/components/chart-page/chart-vedic-tab.tsx @@ -350,7 +350,9 @@ function VedicLayout({ view, chart, ariaLabel, ascendantHouse, children }: { chart can and cannot be read for. */}

{view.vedic.boundary}

- {children} +
+ {children} +
); } diff --git a/frontend/tests/chart-table-span-20261023.test.tsx b/frontend/tests/chart-table-span-20261023.test.tsx new file mode 100644 index 00000000..677a6d10 --- /dev/null +++ b/frontend/tests/chart-table-span-20261023.test.tsx @@ -0,0 +1,97 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import test from "node:test"; + +import { ChartVedicTab } from "../src/components/chart-page/chart-vedic-tab.tsx"; +import { assembleChartView } from "../src/lib/chart-view-load.ts"; +import { chartViewOkSchema, type ChartViewOk } from "../src/lib/chart-view-contract.ts"; + +Object.assign(globalThis, { React }); + +const golden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8")); +const bhavaGolden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-bhava-golden.json", import.meta.url), "utf8")); +const transitPacket = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-transit-2026-09-15.golden.json", import.meta.url), "utf8")); +const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); + +const profile = { + name: "示例", + date: "1990-06-15", + time: "12:00", + placeLabel: "北京", + latitude: 39.9042, + longitude: 116.4074, + timezoneOffset: 8, + timezoneId: "Asia/Shanghai", + ayanamsa: "raman", + birthTimeStatus: "confirmed", +}; + +function directDivClasses(html: string, parentClass: string): string[] { + const needle = `
`; + const start = html.indexOf(needle); + assert.ok(start >= 0, parentClass); + let index = start + needle.length; + let depth = 1; + const classes: string[] = []; + while (index < html.length && depth > 0) { + const nextOpen = html.indexOf("", index); + assert.ok(nextClose >= 0); + if (nextOpen >= 0 && nextOpen < nextClose) { + if (depth === 1) { + const tagEnd = html.indexOf(">", nextOpen); + const tag = html.slice(nextOpen, tagEnd + 1); + classes.push(tag.match(/class="([^"]*)"/)?.[1] ?? ""); + } + depth += 1; + index = html.indexOf(">", nextOpen) + 1; + } else { + depth -= 1; + index = nextClose + "
".length; + } + } + return classes; +} + +async function fullView(): Promise { + const result = await assembleChartView({ + userId: "user-1", + profile, + asOf: "2026-09-15", + layers: ["varga", "bhava", "transit"], + postEngine: async (path, _body, cacheScope) => { + if (path === "/api/chart" && cacheScope?.startsWith("transit:")) return { status: "ok", payload: transitPacket }; + if (path === "/api/chart") return { status: "ok", payload: golden.chart }; + if (path === "/api/varga_full") return { status: "ok", payload: golden.varga_full }; + if (path === "/api/bhava_chalit") return { status: "ok", payload: bhavaGolden.bhava_chalit }; + return { status: "http_error", path, elapsedMs: 1, httpStatus: 500 }; + }, + }); + assert.equal(result.body.status, "ok"); + return chartViewOkSchema.parse(result.body); +} + +test("every Vedic chart type puts its table in the layout's full-width row", async () => { + const view = await fullView(); + for (const vargaId of ["D1", "MOON", "D9", "BHAVA", "TRANSIT"]) { + const markup = renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId, onSelectVarga: () => {} })); + const children = directDivClasses(markup, "chart-page-vedic-layout"); + assert.deepEqual(children, [ + "chart-page-vedic-stage", + "chart-page-params", + "chart-page-vedic-tables", + ], vargaId); + } +}); + +test("the desktop chart grid spans the table row, not a table's own class", () => { + const desktopStart = globalStyles.indexOf("@media (min-width: 1024px) {\n .chart-page-vedic-layout,"); + assert.ok(desktopStart >= 0); + const desktopEnd = globalStyles.indexOf("\n}", desktopStart); + const desktop = globalStyles.slice(desktopStart, desktopEnd); + assert.match(desktop, /\.chart-page-vedic-tables \{ grid-column: 1 \/ -1; \}/); + assert.doesNotMatch(desktop, /\.chart-page-planet-table-wrap[\s\S]*grid-column/); + assert.doesNotMatch(desktop, /\.chart-page-varga-table-block[\s\S]*grid-column/); +});