fix(ui): 44px coarse hits and align CSS breakpoints
Independent Staging Quality Gate / validate (push) Failing after 8m53s
Independent Staging Quality Gate / publish (push) Skipped

Touch-screen message actions get a 44x44 hit once gap and bottom margin open to 20px; fine-pointer geometry stays 27x34. Tablet CSS cuts at 1023 to match sidebarViewportForWidth. Report centre/reader join the 767 mobile cut; TOC keeps 860. Occupies BUG-695/696/697.
This commit is contained in:
jesse-ux
2026-09-15 12:46:59 +08:00
parent 8144fca27d
commit 6c748d86b0
10 changed files with 259 additions and 25 deletions
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-09-15 — 手机上回答按钮更好点;平板宽度不再串台
手机上复制 / 点赞 / 点踩 / 重新生成的可点区域是 44×44,图标看起来还是原来那么大。平板按 1024 切,不再在 901–1023 之间侧栏窄、内容却按桌面排。报告中心和阅读页跟全局移动断点对齐。Skill 版本不变。
## 2026-09-15 — 首页不再给主题卡;回答默认只展开口语层
首页「从一个主题开始」整段下线,不再为六张卡调大模型、也不再等它揭幕。聊天里本命类回答默认只看到口语层(一句结论、2–3 条短要点、一句下一步);从「统一参数与原始结构」起的骨架折在「完整分析」里,内容一字不删,复制和导出仍是全文。手机上「完整分析」里的四列表可以横着滑,三列技法表仍是卡片。登录和资料输入框改成 16px,避免 iPhone 一点就整页放大。Skill 版本不变。
+48
View File
@@ -10805,3 +10805,51 @@
- 相关记录:BUG-693、BUG-576
- 复发自:无
- 修复版本:待发布
## BUG-695 | 回答操作触屏热区 27×34,点赞容易点成点踩
- 状态:resolved
- 首次发现:2026-09-15
- 最近更新:2026-09-15
- 影响面:聊天回答下方复制 / 点赞 / 点踩 / 重新生成,以及代码块复制按钮
- 用户现象:四个图标并排,手机上点赞很容易点成点踩。
- 触发条件:触屏设备上点助手回答下方的操作行。
- 根因:复发自 BUG-446 的有意让步。当时中心距只有 27px、下方 follow-up 只有 8px,44×44 会互相吃点击,热区停在 27×34。触屏推荐仍是 44×44。
- 修复:视觉图标仍是 26×26。`(pointer: coarse)` 下 gap 和底边距收到 20px,伪元素放大到 44×44。细指针几何不变。代码块复制按钮在粗指针下 `min-height: 44px`
- 验证:代码级契约测试锁细指针 27×34、粗指针 44×44 且 gap/底边 20px。浏览器级验收见 `docs/testing/mobile-touch-and-breakpoints-20260915.md`
- 防复发:`frontend/tests/touch-target-contract.test.ts` 同时锁两套几何。粗指针热区不得与相邻按钮或 follow-up 药丸重叠。
- 相关记录:BUG-446
- 复发自:BUG-446(当时为避免重叠把热区停在 27×34)
- 修复版本:待发布
## BUG-696 | CSS 平板上限 900px 与侧栏 1024 不一致,9011023 是混合态
- 状态:resolved
- 首次发现:2026-09-15
- 最近更新:2026-09-15
- 影响面:兑换码表单、模型选择器、首页产品卡在平板宽度下的栅格
- 用户现象:窗口拖到大约 iPad 横屏附近时,侧栏已经是窄版平板,内容区却按桌面排。
- 触发条件:视口宽度 901–1023px。
- 根因:`sidebarViewportForWidth` 在 1024 切桌面,CSS 另有 `max-width: 900px` 当平板上限。
- 修复:这些 CSS 切点改为 `max-width: 1023px`。主题卡留下的 `.starter-list > button` 孤儿规则删除,不改断点再留着。
- 验证:`viewport-breakpoint-contract.test.ts` 断言 `@media` 里不再出现 `max-width: 900px`,且 JS 阈值仍是 768 / 1024。浏览器级验收见 `docs/testing/mobile-touch-and-breakpoints-20260915.md`
- 防复发:宽度断点白名单契约测试;新切点必须先改白名单。
- 相关记录:BUG-697
- 复发自:无
- 修复版本:待发布
## BUG-697 | 报告域 720 / 760 / 860 三个断点互不对齐
- 状态:resolved
- 首次发现:2026-09-15
- 最近更新:2026-09-15
- 影响面:报告中心、专业报告阅读页、报告目录
- 用户现象:761–860px 目录已经塌成抽屉,正文还是桌面内边距;721–760 阅读区已窄、报告中心还是桌面。
- 触发条件:视口落在那两个混合区间。
- 根因:报告中心用 720、阅读区用 760、目录用 860,彼此和全局 767 都不对齐。
- 修复:720 与 760 并入 767。860 保留并就地注释:目录是 1120 阅读页的第三栏,必须比正文更早塌。会员 639/640 与校正 640/641 统一为 640/641。录入卡 620 并入 640。toast / 校正面 430 并入 480。
- 验证:白名单契约测试抽出的 `@media` 宽度集合等于 480 / 640 / 641 / 767 / 768 / 860 / 1023 / 1024。浏览器级验收见 `docs/testing/mobile-touch-and-breakpoints-20260915.md`
- 防复发:`viewport-breakpoint-contract.test.ts``globals.css` 顶部白名单注释。
- 相关记录:BUG-696
- 复发自:无
- 修复版本:待发布
@@ -0,0 +1,48 @@
# PROGRESS · 消息操作触控目标 + 断点错位(2026-09-15
- 执行分支:`codex/mobile-touch-and-breakpoints-20260915`
- 工作树:`.worktrees/mobile-touch-and-breakpoints-20260915`
- 基线:任务书写 `41473261`;实际按串行依赖用合入后的 `origin/staging` @ `8144fca2`
## 做了什么
BUG-695:细指针几何不变(26×26 视觉,27×34 热区,gap 1px,底边 8px)。`(pointer: coarse)` 下 gap 与底边距改为 `--space-5`(20px),热区 44×44。代码块复制按钮粗指针 `min-height: 44px`。注释改为说明为什么现在 44×44 不会抢 follow-up。
BUG-696`max-width: 900px``@media` 全部改为 `1023px`。主题卡留下的 `.starter-list > button` 孤儿规则删除。`width: min(900px, 100%)` 是栏宽不是切点,未动。JS `sidebarViewportForWidth` 未动。
BUG-697:报告中心 720、阅读区 760 并入 767。目录 860 保留,就地注释第三栏内容宽度。会员 639/640 改为 640/641。录入卡 620 并入 640。toast / 校正面 430 并入 480。
## 测试数字
| | `test(` 声明(`git grep -E '^test\(' -- frontend/tests` |
| --- | ---: |
| 基线 `8144fca2` | 3208 |
| 改后 | 3213+4 `viewport-breakpoint-contract`+1 `touch-target-contract` 粗指针) |
定向(本机):
| 套件 | 结果 |
| --- | --- |
| `touch-target-contract` | 3 pass |
| `viewport-breakpoint-contract` | 4 pass |
| `chat-stream-layout` | 6 pass |
| `membership-page` 本轮改的 640/641 栅格 | pass |
| `membership-page`「login redirects…」 | 本机失败:未改的 `page.tsx` 工作区是 CRLF,正则要 `\n`。不是本单回归。 |
| `./node_modules/.bin/tsc --noEmit` | 0 错 |
| `npm run lint` | **0 error** / 120 warning(既有,未动) |
本机 `next build` 仍受 Windows `SKILL.md` symlink `EPERM` 限制,不作为通过证据。`/` Static 与首屏 gzip 留给 Gitea 门禁。浏览器 / 真机项见 `docs/testing/mobile-touch-and-breakpoints-20260915.md`,不得写成通过。
## 既有断言三栏
| 位置 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `touch-target-contract` 热区 | 只锁 27×34 | 细指针仍 27×34,另锁粗指针 44×44 | BUG-695 |
| `membership-page` 栅格 | `min-width: 640` / `max-width: 639` | `641` / `640` | BUG-697 与校正那组对齐 |
## 偏离
- 任务 3.2860 **保留**。目录是 1120 阅读页第三栏,768–860 若并入 1023 会让抽屉和宽正文抢同一段。
- 任务 3.3430 并入 480、620 并入 640,未看到必须保留原值的理由。
- 任务 1 未走让步 3(热区高度仍是 44,不只加宽)。
- 浏览器 / 真机项见 `docs/testing/mobile-touch-and-breakpoints-20260915.md`,不得写成通过。
+1 -1
View File
@@ -118,7 +118,7 @@
| `TASK-unified-loading-20260902.md` | `PROGRESS-unified-loading-20260902.md` | 首页一次等待一次揭幕 | 已验收 | `482796fc`BUG-479 |
| — | — | 初始化后点首页「家庭」报运行合同未完成:本命第一步未强制计算工具、主题卡无入口钉死 | 入口已下线,机制保留供历史会话 | `codex/consultation-family-contract-incomplete-20260909`BUG-630);主题卡发出点已删,`guided_topic` 枚举仍可读回;走查 `docs/testing/consultation-family-contract-incomplete-20260909.md` 不再可执行 |
| `TASK-chat-reading-load-20260915.md` | `PROGRESS-chat-reading-load-20260915.md` | 首页主题卡下线 + 回答两层可见性 + 折叠层宽表可横滑 + 输入框 16px。内容一字不删。⚠️ BUG-630 走查路径失效 | 待验收 | `codex/chat-reading-load-20260915` |
| `TASK-mobile-touch-and-breakpoints-20260915.md` | | 消息操作按钮触屏命中区 27×34、相邻仅隔 1px(BUG-695);CSS 平板上限 900px 与 `sidebarViewportForWidth` 的 1024 不一致,9011023 是混合态(BUG-696);报告域 720/760/860 三个断点互不对齐,761–860 目录已塌、正文还是桌面(BUG-697)。含断点白名单契约测试。**串行在 chat-reading-load 之后** | 待领取 | `codex/mobile-touch-and-breakpoints-20260915` |
| `TASK-mobile-touch-and-breakpoints-20260915.md` | `PROGRESS-mobile-touch-and-breakpoints-20260915.md` | 消息操作按钮触屏命中区 27×34、相邻仅隔 1px(BUG-695);CSS 平板上限 900px 与 `sidebarViewportForWidth` 的 1024 不一致,9011023 是混合态(BUG-696);报告域 720/760/860 三个断点互不对齐,761–860 目录已塌、正文还是桌面(BUG-697)。含断点白名单契约测试。**串行在 chat-reading-load 之后** | 待验收 | `codex/mobile-touch-and-breakpoints-20260915` |
### 个人报告
@@ -0,0 +1,46 @@
# 真机 / 浏览器清单 · 触控目标与断点(2026-09-15)
代码级合同在 `frontend/tests/touch-target-contract.test.ts``viewport-breakpoint-contract.test.ts`。下面这些必须用浏览器或真机看,自动化替代不了。
地址:`https://staging.jyotisha.chat`。截图不要带真实姓名、出生资料或会话内容。
## A. BUG-695 回答操作按钮
### 桌面(鼠标,`pointer: fine`
1. 打开一条已有助手回答。
2. **看:** 复制 / 点赞 / 点踩 / 重新生成四个图标看起来仍是原来那么大、那么密,肉眼无变化。
### 手机或开了触屏仿真的 Chrome(`pointer: coarse`
1. 同一条回答。
2. **看:** 四个图标视觉仍是小的,但彼此之间空档更大,和下面的 follow-up 药丸也不贴在一起。
3. **点:** 点赞不要点成点踩;点复制不要点到点赞。四个都能单独点到。
4. 若这条回答有代码块,点右上角复制。**看:** 热区够大,不会点空。
## B. BUG-696 平板宽度 901 / 1023 / 1024
开发者工具设宽度,刷新后再看。
| 宽度 | 打开 | 看什么 |
| ---: | --- | --- |
| 901 | 空首页 | 今日星语和生时校正是单栏上下排,不是并排。侧栏是窄版平板(约 240px),不是桌面 288px。 |
| 901 | 设置 → 账户与点数里的兑换码表单 | 两列表单还在,没有突然变成桌面三列或挤成一团。 |
| 901 | 顶栏模型选择器 | 触发器占满当前格,没有和旁边控件抢位置。 |
| 1023 | 同上三处 | 仍按平板排,与 901 同类。 |
| 1024 | 同上三处 | 侧栏切到桌面宽度;首页两张产品卡可以回到两列。 |
## C. BUG-697 报告页 761 / 800 / 860
需要一条已生成的专业报告。没有就先生成再测。
| 宽度 | 看什么 |
| ---: | --- |
| 761 | 报告中心列表已是移动内边距;阅读页正文也是窄边距。目录若在阅读页,已经是抽屉而不是侧栏。 |
| 800 | 与 761 同类:目录仍是抽屉,正文用窄边距,不再出现「目录抽屉 + 桌面正文边距」的串台。 |
| 860 | 目录仍可以是抽屉(860 是第三栏内容宽度,允许比 767 更早塌)。正文边距与 767 移动规则一致。 |
| 861 | 目录可以回到侧栏。若这一档看起来挤,记下来,不要自己改 860。 |
## 结论
> 待填。测完写日期 + 设备 + 浏览器 + 逐条正常/有问题。
+9 -3
View File
@@ -168,9 +168,9 @@ Radii have two visual steps. Controls use 8px (`--radius-md`; `--radius-xs` and
- Chat reading width: 760px for the welcome/composer and 900px for long answers. Both chat surfaces share the 900px transcript width; the rectification session no longer narrows it to 720px.
- Admin content width: 1200px, centered.
- Desktop shell: 288px sidebar plus flexible reading panel.
- Layout breakpoints: mobile below 768px, tablet 7681023px, desktop 1024px and above. These three decide the shell — sidebar mode, grid columns, drawer versus rail.
- Component breakpoints are separate and narrower, each owned by one surface rather than the shell: 900px and 480px (redemption and profile forms), 760px and 720px (personal report and report centre), 640px (rectification candidates), 767px (settings dialog mobile full-screen), 620px (onboarding cards), 430px (toasts and the conversational rectification panel). Do not repurpose one for another surface, and do not assume testing the three layout tiers covers them.
- All full-height surfaces use `100dvh`. Touch targets are at least 44px. Where adjacent controls sit closer than 44px, the hit expands to the largest size that does not overlap; those exceptions are locked by `frontend/tests/touch-target-contract.test.ts`.
- Layout breakpoints: mobile below 768px, tablet 7681023px, desktop 1024px and above. These three decide the shell — sidebar mode, grid columns, drawer versus rail. CSS `@media` width cuts must follow `sidebarViewportForWidth` in `frontend/src/lib/sidebar-state.ts` (768 / 1024), not the other way around. The allowlist lives as a comment at the top of `globals.css` and is locked by `frontend/tests/viewport-breakpoint-contract.test.ts`.
- Allowed `@media` widths: 480 (small phone), 640/641 (content grids: membership, rectification candidates, intake card), 767/768 (mobile/tablet), 860 (report TOC, content width of the third column), 1023/1024 (tablet/desktop). Do not add a new width without updating that list and the contract test in the same change.
- All full-height surfaces use `100dvh`. Touch targets: on a coarse pointer the hit is 44×44; the visual control may stay smaller. Message-action icons stay 26×26; a fine pointer keeps the 27×34 overlay, a coarse pointer opens gap and bottom margin to 20px so a 44×44 overlay does not eat the next button or the follow-up pills. Locked by `frontend/tests/touch-target-contract.test.ts`.
## 5. Components
@@ -570,3 +570,9 @@ Admin 的 antd `<Spin>` 是独立设计系统,不在此表。
- **窄屏表格:** 三列表(技法审计)继续卡片化。四列及以上(Yoga、统一参数)在 `.conversation` 里横向滚动,右缘用 canvas 渐隐提示还可滑;`.conversation` 自身 `overflow-x: hidden`,页面不得出现横向滚动条。个人报告表格不走这套规则。
- **表单输入 16px** 全局 `input` / `select``--type-body-md`16px)。iOS Safari 对小于 16px 的字段聚焦时会整页放大且不缩回。`.otp-input` 仍是 20px`.composer textarea` 仍是 16px。
## 14. 断点
宽度切点只允许出现在 `globals.css` 顶部注释的白名单里。壳层切点以 `sidebarViewportForWidth` 为准:小于 768 移动,小于 1024 平板,否则桌面。组件级栅格(会员方案卡、校正候选、录入卡)用 640/641。报告目录在 860 塌成抽屉,因为那是第三栏的内容宽度,不是设备宽度。
禁止再引入 900 / 720 / 760 / 639 / 620 / 430 这类与壳层或彼此错位的切点。新切点必须先改白名单和 `viewport-breakpoint-contract.test.ts`
+40 -19
View File
@@ -51,6 +51,15 @@
--radius-lg: 12px;
}
/* Width breakpoints allowed in @media (min-width / max-width).
CSS must follow sidebar-state.ts (768 / 1024), not the other way around.
480 small phone
640/641 content grids (membership cards, rectification candidates, intake card)
767/768 mobile / tablet, same cut as sidebarViewportForWidth
860 report TOC (third column; content width, not a device)
1023/1024 tablet / desktop, same cut as sidebarViewportForWidth
Adding a new width here means updating this list and
frontend/tests/viewport-breakpoint-contract.test.ts in the same change. */
:root {
color-scheme: light;
--color-action: #85432f;
@@ -548,7 +557,7 @@ button:disabled { cursor: default; opacity: .45; }
.account-menu-item:hover { background: var(--color-canvas-muted); }
}
@media (max-width: 900px) {
@media (max-width: 1023px) {
.code-form { grid-template-columns: 1fr 1fr; }
.code-form .note-field { grid-column: 1 / -1; }
.code-form .button-primary { justify-self: start; }
@@ -1272,9 +1281,11 @@ button:disabled { cursor: default; opacity: .45; }
opacity: .7;
transition: background-color 120ms ease-out, color 120ms ease-out, opacity 120ms ease-out, transform 120ms ease-out;
}
/* Visual icon stays 26px. Neighbours sit 27px centre-to-centre (26px + 1px gap)
and follow-ups start after an 8px bottom margin, so a 44×44 overlay would
steal clicks. This is the largest non-overlapping hit: 27×34. */
/* Visual icon stays 26px. Fine pointer: neighbours sit 27px centre-to-centre
(26px + 1px gap) and follow-ups start after an 8px bottom margin, so the
overlay stays 27×34 the largest hit that does not overlap. Coarse pointer:
gap opens to 20px (centre-to-centre 46px) and bottom margin to 20px, so a
44×44 overlay fits without eating the next button or the follow-up pills. */
.message-actions button::after {
position: absolute;
top: 50%;
@@ -1284,6 +1295,19 @@ button:disabled { cursor: default; opacity: .45; }
content: "";
transform: translate(-50%, -50%);
}
@media (pointer: coarse) {
.message-actions {
gap: var(--space-5);
margin-bottom: var(--space-5);
}
.message-actions button::after {
width: 44px;
height: 44px;
}
.markdown-code-copy {
min-height: 44px;
}
}
.message-actions button:hover:not(:disabled) {
background: color-mix(in srgb, var(--color-canvas-muted) 72%, transparent);
color: var(--color-ink-secondary);
@@ -1659,7 +1683,7 @@ button:disabled { cursor: default; opacity: .45; }
.model-selector-check { width: 18px; height: 18px; opacity: 0; color: var(--color-action); }
.model-selector-option[data-selected] .model-selector-check { opacity: 1; }
@media (max-width: 900px) {
@media (max-width: 1023px) {
.model-selector-trigger { max-width: 100%; grid-column: 1; grid-row: 1; }
}
@@ -1856,16 +1880,12 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.composer button:not(:disabled):hover, .button-primary:not(:disabled):hover { background: var(--color-action-hover); }
.model-selector-trigger:not(:disabled):hover { background: var(--color-canvas-muted); color: var(--color-ink); }
.model-selector-option:hover { background: var(--color-canvas-soft); color: var(--color-ink); }
.starter-list > button:not(:disabled):hover { background: var(--color-canvas-strong); }
.starter-list > button:first-of-type:not(:disabled):hover { background: color-mix(in srgb, var(--color-action-soft) 72%, var(--color-canvas)); }
.button-secondary:not(:disabled):hover { background: var(--color-canvas-muted); }
.danger-primary:not(:disabled):hover { background: color-mix(in srgb, var(--color-danger) 88%, var(--color-ink)); }
.birth-time-source-option:hover, .birth-time-answer-list button:not(:disabled):hover { border-color: var(--color-action); }
}
@media (min-width: 768px) and (max-width: 900px) {
.starter-list { grid-template-columns: 1fr; grid-template-rows: auto; }
.starter-list > button { min-height: 112px; grid-row: auto; padding: var(--space-4); }
@media (min-width: 768px) and (max-width: 1023px) {
.product-entrypoints { grid-template-columns: 1fr; }
.auth-shell { grid-template-columns: 1.2fr .8fr; }
.auth-story, .auth-panel { padding: var(--space-8); }
@@ -1895,7 +1915,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.welcome { padding: var(--space-6) 0 var(--space-10); }
.welcome > .onboarding-message:first-child .message-bubble p { font-size: var(--type-display-sm); }
.starter-list { grid-template-columns: 1fr; grid-template-rows: auto; }
.starter-list > button { min-height: 112px; grid-row: auto; padding: var(--space-4); }
.product-entrypoints { grid-template-columns: 1fr; }
.message-list { width: 100%; padding: var(--space-5) var(--space-4) var(--space-12); }
.message-content { max-width: 88%; }
@@ -1954,7 +1973,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.auth-brand { display: flex; margin-bottom: var(--space-6); }
}
@media (max-width: 430px) {
@media (max-width: 480px) {
[data-sonner-toaster] { right: var(--space-4) !important; left: var(--space-4) !important; width: auto !important; }
.conversational-rectification { width: 100%; max-width: 100%; gap: var(--space-4); overflow-x: clip; }
.conversational-narrative-body, .conversational-composer, .conversational-evidence-recap, .conversational-candidate, .conversational-confirmation, .conversational-original-question, .conversational-empty-state { padding: var(--space-3); }
@@ -2196,9 +2215,8 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-inline: auto;
}
@media (min-width: 768px) and (max-width: 900px) {
@media (min-width: 768px) and (max-width: 1023px) {
.starter-hero {
grid-template-columns: minmax(0, 1fr) 180px;
padding: var(--space-8);
}
@@ -2535,7 +2553,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.onboarding-card-actions { align-items: center; justify-content: space-between; gap: var(--space-4); padding-top: var(--space-2); }
.onboarding-card-action-hint { color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.4; }
@media (max-width: 620px) {
@media (max-width: 640px) {
.onboarding-card-heading { align-items: flex-start; flex-direction: column; gap: var(--space-2); }
.birth-time-source-list, .birth-time-detail-grid { grid-template-columns: 1fr; }
.birth-time-window-details .birth-time-skip-button { grid-column: auto; justify-self: stretch; }
@@ -3700,7 +3718,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
}
.report-center-message p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-body-sm); }
@media (max-width: 720px) {
@media (max-width: 767px) {
.report-center-shell { padding-inline: var(--space-4); }
.report-center-hero { grid-template-columns: 1fr; align-items: start; gap: var(--space-6); padding-block: var(--space-8) var(--space-6); }
.report-center-hero h1 { font-size: var(--type-display-md); letter-spacing: -.5px; }
@@ -3824,6 +3842,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
max-width: none;
margin: var(--space-3) 0 0;
}
/* Report TOC is the third column of a 1120px reader. It has to collapse
before the article padding does, or 768860px puts a rail beside a still-wide
body. 860 is the content width of that rail, not a device cut. */
@media (max-width: 860px) {
.personal-report-md-layout { grid-template-columns: 1fr; }
.personal-report-toc { position: static; max-height: none; }
@@ -4299,7 +4320,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.personal-report-avoid-break { break-inside: avoid-page; }
.personal-report-avoid-break-row { break-inside: avoid; }
@media (max-width: 760px) {
@media (max-width: 767px) {
.personal-report-reader-body { padding: 0 var(--space-4) var(--space-12); }
.personal-report-actions > div { min-height: 50px; padding: var(--space-2) var(--space-4); }
.personal-report-action-end > span { display: none; }
@@ -4485,12 +4506,12 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.membership-payment-panel .dialog-actions { grid-column: 1 / -1; }
}
@media (max-width: 639px) {
@media (max-width: 640px) {
.membership-plan-card--recommended { order: -1; }
.membership-payment-error { align-items: flex-start; flex-direction: column; }
}
@media (min-width: 640px) {
@media (min-width: 641px) {
.membership-plan-grid, .membership-credit-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
+5 -2
View File
@@ -80,9 +80,12 @@ test("membership hero stays content-led without decorative orbit artwork", () =>
test("desktop uses three columns, tablet two and mobile one with monthly first", () => {
assert.match(globalStyles, /\.membership-plan-grid,[\s\S]*\.membership-credit-grid\s*\{[^}]*grid-template-columns:\s*1fr/);
assert.match(globalStyles, /@media \(min-width:\s*640px\)[\s\S]*grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\)/);
// 原值: 会员栅格 min-width 640 / max-width 639,与校正的 640/641 差 1px。
// 新值: 统一为 640/641。
// 原因: BUG-697 噪音收敛,白名单只留一组内容级切点。
assert.match(globalStyles, /@media \(min-width:\s*641px\)[\s\S]*\.membership-plan-grid[\s\S]*grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\)/);
assert.match(globalStyles, /@media \(min-width:\s*1024px\)[\s\S]*grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\)/);
assert.match(globalStyles, /@media \(max-width:\s*639px\)[\s\S]*\.membership-plan-card--recommended\s*\{[^}]*order:\s*-1/);
assert.match(globalStyles, /@media \(max-width:\s*640px\)[\s\S]*\.membership-plan-card--recommended\s*\{[^}]*order:\s*-1/);
assert.doesNotMatch(cssBlock(".membership-plan-card--recommended"), /grid-column/);
});
@@ -32,6 +32,9 @@ test("icon and chip controls keep their visual size and expand the hit with a ps
assert.match(actions, /height:\s*26px/);
assert.match(cssDeclarations(".message-actions", globalStyles), /gap:\s*1px/);
const actionHit = hasHitExpandPseudo(".message-actions button");
// 原值: 热区锁 27×3444×44 会与相邻按钮和 follow-up 重叠)。
// 新值: 细指针仍是 27×34;粗指针见下一条,gap 20px + 底边 20px 后才放大到 44×44。
// 原因: BUG-695,触屏点赞容易点成点踩。
assert.match(actionHit, /width:\s*27px/);
assert.match(actionHit, /height:\s*34px/);
@@ -42,3 +45,16 @@ test("icon and chip controls keep their visual size and expand the hit with a ps
assert.match(auth, /min-height:\s*32px/);
assert.match(hasHitExpandPseudo(".auth-links button"), /inset:\s*-4px/);
});
test("coarse pointers get a 44×44 message-action hit that does not overlap neighbours", () => {
const coarseStart = globalStyles.indexOf("@media (pointer: coarse)");
assert.notEqual(coarseStart, -1);
const coarse = globalStyles.slice(coarseStart, globalStyles.indexOf("}", globalStyles.indexOf(".markdown-code-copy", coarseStart)) + 1);
assert.match(coarse, /\.message-actions \{[\s\S]*gap:\s*var\(--space-5\)/);
assert.match(coarse, /margin-bottom:\s*var\(--space-5\)/);
assert.match(coarse, /\.message-actions button::after \{[\s\S]*width:\s*44px;[\s\S]*height:\s*44px;/);
assert.match(coarse, /\.markdown-code-copy \{[\s\S]*min-height:\s*44px;/);
const base = globalStyles.slice(0, coarseStart);
assert.match(base, /\.message-actions \{[\s\S]*gap:\s*1px;/);
assert.match(base, /\.message-actions button::after \{[\s\S]*width:\s*27px;[\s\S]*height:\s*34px;/);
});
@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const sidebar = readFileSync(new URL("../src/lib/sidebar-state.ts", import.meta.url), "utf8");
/** Widths that may appear in @media (min-width / max-width). Keep in sync with the comment at the top of globals.css. */
const ALLOWED = new Set([480, 640, 641, 767, 768, 860, 1023, 1024]);
function mediaWidthBreakpoints(source: string): number[] {
const widths = new Set<number>();
for (const block of source.matchAll(/@media[^{]+/g)) {
for (const hit of block[0].matchAll(/\((?:min|max)-width:\s*(\d+)px\)/g)) {
widths.add(Number(hit[1]));
}
}
return [...widths].sort((a, b) => a - b);
}
test("CSS width breakpoints stay on the published allowlist", () => {
assert.deepEqual(mediaWidthBreakpoints(css), [...ALLOWED].sort((a, b) => a - b));
});
test("tablet CSS cuts at 1023 so it matches sidebarViewportForWidth", () => {
assert.doesNotMatch(css, /@media[^{]*max-width:\s*900px/);
assert.match(css, /@media \(max-width: 1023px\)/);
assert.match(sidebar, /if \(width < 768\) return "mobile"/);
assert.match(sidebar, /if \(width < 1024\) return "tablet"/);
});
test("report centre and reader share the global mobile cut", () => {
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.report-center-shell/);
assert.match(css, /@media \(max-width: 767px\) \{[\s\S]*\.personal-report-reader-body/);
assert.doesNotMatch(css, /@media \(max-width: 720px\)/);
assert.doesNotMatch(css, /@media \(max-width: 760px\)/);
});
test("report TOC keeps a content-width exception at 860px", () => {
assert.match(css, /Report TOC is the third column/);
assert.match(css, /@media \(max-width: 860px\) \{[\s\S]*\.personal-report-md-layout/);
});