feat(ui): 星盘 tab 下划线化、参数收成键值表,星历日期条改 ‹ 日期 ›
星盘页(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:
co-authored by
Claude Opus 5
parent
ca0204984b
commit
1e3570b462
@@ -24,6 +24,18 @@ const golden = JSON.parse(
|
||||
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
const sidebar = readFileSync(new URL("../src/components/app-sidebar.tsx", import.meta.url), "utf8");
|
||||
const vedicTab = readFileSync(new URL("../src/components/chart-page/chart-vedic-tab.tsx", import.meta.url), "utf8");
|
||||
const westernTab = readFileSync(new URL("../src/components/chart-page/chart-western-tab.tsx", import.meta.url), "utf8");
|
||||
const qizhengTab = readFileSync(new URL("../src/components/chart-page/chart-qizheng-tab.tsx", import.meta.url), "utf8");
|
||||
|
||||
/** Every selector that has at least one rule, comments stripped first. */
|
||||
function declaredSelectors(source: string): Set<string> {
|
||||
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, " ");
|
||||
const found = new Set<string>();
|
||||
for (const [, selectorList] of withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
||||
for (const target of selectorList.split(",")) found.add(target.trim().replace(/\s+/g, " "));
|
||||
}
|
||||
return found;
|
||||
}
|
||||
const vedicSvg = readFileSync(new URL("../src/components/personal-report/vedic-chart-svg.tsx", import.meta.url), "utf8");
|
||||
|
||||
const profile = {
|
||||
@@ -126,25 +138,52 @@ test("sidebar adds 星盘 and 星历 after 新建对话 without renaming 我的
|
||||
assert.match(sidebar, /tooltip="星历"/);
|
||||
});
|
||||
|
||||
test("mobile width keeps two centre lines and moves the full parameter card below the chart", () => {
|
||||
// 原值:三条断言都以 `@media (max-width: 767px) { .chart-page-hero h1` 起锚,
|
||||
// 再加一条 `.chart-page-back` 的 44px 触摸目标。
|
||||
// 新值:直接断那个断点块里的三个开关;`.chart-page-back` 一条删除。
|
||||
// 原因:hero 与「返回对话」按钮随页面并入 app 外壳而删除(T4.2)。被断言的三个
|
||||
// 开关本身一条没动,只是不再有 hero 那一行可以用来起锚。返回路径现在是
|
||||
// 常驻侧栏,它的触摸目标由 sidebar-contract 管。
|
||||
const mobile = globalStyles.slice(
|
||||
globalStyles.indexOf("@media (max-width: 767px)", globalStyles.indexOf(".chart-page-center-compact")
|
||||
- 2000),
|
||||
);
|
||||
assert.match(mobile, /\.chart-page-center-compact \{ display: block; \}/);
|
||||
assert.match(mobile, /\.chart-page-center-extra \{ display: none; \}/);
|
||||
assert.match(mobile, /\.chart-page-params-below \{ display: block; \}/);
|
||||
test("the parameters are one key/value table, not a per-breakpoint duplicate", () => {
|
||||
// 原值:断言 `@media (max-width: 767px)` 块里的三个开关
|
||||
// `.chart-page-center-compact { display: block }` /
|
||||
// `.chart-page-center-extra { display: none }` /
|
||||
// `.chart-page-params-below { display: block }`。
|
||||
// 新值:断言参数只有一张 `.chart-page-params-table` 键值表,且那三个类名在
|
||||
// 整份 CSS 里都不再作为规则出现。
|
||||
// 原因:T5.1 要求参数从中宫卡与盘下卡收进一张统一键值表(决策记录 D8 一系)。
|
||||
// 原断言守的是「同一组参数按断点切换三种画法」,那正是本轮要去掉的东西,
|
||||
// 断言无法原样保留。新断言比原来更强:它不只要求当前画法正确,还禁止
|
||||
// 任何一种按断点复制参数的写法回来。
|
||||
// 注:原写法本身也是个陷阱——`indexOf` 未命中时返回 -1,`-2000` 之后
|
||||
// 会从文件开头重新搜 `@media`,静默锚到无关的断点块上。
|
||||
assert.match(cssDeclarations(".chart-page-param-row", globalStyles), /grid-template-columns/);
|
||||
for (const dead of [
|
||||
".chart-page-center-card",
|
||||
".chart-page-center-compact",
|
||||
".chart-page-center-extra",
|
||||
".chart-page-params-below",
|
||||
".chart-page-boundary-desktop",
|
||||
]) {
|
||||
assert.equal(
|
||||
declaredSelectors(globalStyles).has(dead),
|
||||
false,
|
||||
`${dead} still has a CSS rule`,
|
||||
);
|
||||
}
|
||||
assert.match(cssDeclarations(".chart-page-tab", globalStyles), /min-height:\s*44px/);
|
||||
assert.match(cssDeclarations(".chart-page-chip", globalStyles), /min-height:\s*44px/);
|
||||
assert.match(globalStyles, /\.chart-page-vedic-stage \.personal-report-chart-svg text \{ font-size: 16px; \}/);
|
||||
});
|
||||
|
||||
test("the planet table scrolls inside its own box instead of widening the page", () => {
|
||||
assert.match(cssDeclarations(".chart-page-planet-table-wrap", globalStyles), /overflow-x:\s*auto/);
|
||||
// Without `min-width: 0` a grid item is sized by its content, so the table
|
||||
// pushes the page sideways and the overflow never engages.
|
||||
assert.match(cssDeclarations(".chart-page-planet-table-wrap", globalStyles), /min-width:\s*0/);
|
||||
assert.match(cssDeclarations(".chart-page-planet-table", globalStyles), /min-width:\s*\d/);
|
||||
// Every planet table is wrapped, not just the one on the 星盘 tab.
|
||||
for (const source of [vedicTab, westernTab, qizhengTab]) {
|
||||
const tables = [...source.matchAll(/<table className="chart-page-planet-table">/g)].length;
|
||||
const wraps = [...source.matchAll(/<div className="chart-page-planet-table-wrap">/g)].length;
|
||||
assert.equal(wraps, tables);
|
||||
}
|
||||
});
|
||||
|
||||
test("an incomplete profile page has no loading animation", () => {
|
||||
const markup = renderToStaticMarkup(React.createElement(ChartPageView, {
|
||||
view: { status: "birth_profile_incomplete", billed: false, message: "还没有可用来排盘的出生资料。" },
|
||||
|
||||
@@ -114,6 +114,22 @@ test("the Lahiri note sits under panchanga and not under transits", () => {
|
||||
assert.doesNotMatch(transitsBlock, new RegExp(EPHEMERIS_COPY.panchangaLahiriNote));
|
||||
});
|
||||
|
||||
test("今天 is absent on today and present off it, never disabled", () => {
|
||||
// 原值:`assert.match(markup, new RegExp(EPHEMERIS_COPY.today))` —— 在
|
||||
// 「copy stays factual」里对唯一一次渲染(date === today)断言「今天」在场。
|
||||
// 新值:拆成两个状态各断一次,并加断 `disabled` 不出现。
|
||||
// 原因:T5.2 验收标准要求「在今日时不出现『今天』按钮,而不是出现一个禁用按钮」。
|
||||
// 原断言恰好只覆盖 date === today 这一种情况,照搬会要求今日也渲染该按钮,
|
||||
// 与验收标准直接冲突。新断言覆盖两个状态,比原来严格。
|
||||
const onToday = render(payloadFor("2026-09-15"));
|
||||
assert.doesNotMatch(onToday, new RegExp(EPHEMERIS_COPY.today));
|
||||
assert.doesNotMatch(onToday, /disabled=""/);
|
||||
|
||||
const offToday = render(payloadFor("2026-09-14"));
|
||||
assert.match(offToday, new RegExp(EPHEMERIS_COPY.today));
|
||||
assert.doesNotMatch(offToday, /disabled=""/);
|
||||
});
|
||||
|
||||
test("mobile CSS turns five panchanga cards into one stacked card", () => {
|
||||
const markup = render(payloadFor("2026-09-15"));
|
||||
assert.match(markup, /class="ephemeris-panchanga"/);
|
||||
@@ -129,7 +145,6 @@ test("copy stays factual: no fortune-telling, no spinner, ask prefills the date"
|
||||
assert.match(markup, new RegExp(EPHEMERIS_COPY.ask));
|
||||
assert.match(markup, new RegExp(EPHEMERIS_COPY.previousDay));
|
||||
assert.match(markup, new RegExp(EPHEMERIS_COPY.nextDay));
|
||||
assert.match(markup, new RegExp(EPHEMERIS_COPY.today));
|
||||
assert.doesNotMatch(markup, /运势|适合|不宜|建议|吉祥日/);
|
||||
assert.doesNotMatch(pageSource, /InlineSpinner|正在加载|skeleton/i);
|
||||
assert.equal(ephemerisAskDraft("2026-09-15"), "请按 2026年9月15日 这一天的天象来看。");
|
||||
|
||||
Reference in New Issue
Block a user