diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index ac468509..2230b8c1 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -3273,3 +3273,19 @@ - 防复发:任何 revision state 快路径都必须验证可读性,并保留容器镜像 revision 的只读回退,不得仅以路径存在作为可消费条件。 - 相关记录:BUG-083、BUG-192 - 修复版本:本次 staging 集成提交(精确 SHA 以远端 staging 与部署结果为准) + +## BUG-194 | 移动端出生资料空状态无法滚动且主题问题被压成窄列 + +- 状态:resolved(待 staging 精确 SHA 验收) +- 首次发现:2026-08-15 +- 最近更新:2026-08-15 +- 影响面:首页空会话出生资料填写、移动端内部滚动容器、首页主题问题入口。 +- 用户现象:手机上填写出生资料时无法继续向下滚动,底部字段与“继续”按钮不能进入视口;首页十个主题问题在桌面横向挤成窄条,中文几乎逐字换行。 +- 触发条件:宽度不超过 767px 的空会话首页内容高于可视区域;或桌面显示完整主题问题集合。 +- 根因:早期移动端 `.conversation.is-empty { display: block; }` 声明位于后续桌面 `.conversation.is-empty { display: grid; place-items: center; }` 之前,被级联顺序覆盖;外层页面又固定为隐藏溢出,只能依赖该内部容器滚动。主题区同时使用单排 flex 并让所有问题共享整体边框,十项内容被压缩到不可读宽度。 +- 修复:把移动端空状态覆盖移动到桌面 Grid 规则之后,恢复独立纵向滚动、iOS 惯性滚动,并为动态视口高度增加 `100vh` 回退和 `100dvh`;主题入口改为带独立边框和圆角的响应式 Grid,桌面、平板、手机分别为 3、2、1 列,同时沿用按压态底色与仅桌面 hover 箭头反馈。 +- 验证:移动端滚动与主题布局契约测试、首页入口回归、目标 ESLint 和 `git diff --check` 通过;真实浏览器在 1440、820、390 宽度下无水平或文字溢出,390×844 与模拟键盘高度 390×500 均可滚动到底部并显示“继续”。 +- 防复发:移动端覆盖必须位于桌面空状态 Grid 之后;测试锁定滚动容器、`vh`/`dvh` 顺序、iOS 惯性滚动和主题区 3/2/1 列断点,并禁止恢复为单排 flex 压缩布局。 +- 相关记录:BUG-042、BUG-048、BUG-140 +- 复发自:无 +- 修复版本:本次 staging 集成提交(精确 SHA 以远端 `staging` 核对结果为准) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index be7eb4cf..1ef6c0be 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -272,7 +272,6 @@ button:disabled { cursor: default; opacity: .45; } } @media (max-width: 767px) { - .conversation.is-empty { display: block; } .brand-row { padding: 0 4px; } .brand-mark { width: 26px; height: 26px; } .session-nav { overflow: hidden; } @@ -327,7 +326,7 @@ button:disabled { cursor: default; opacity: .45; } .app-loading-error { background: var(--color-canvas-soft); } .app-loading-error .app-loading-content { padding: 28px; border: 1px solid var(--color-border); background: var(--color-canvas); border-color: var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-elevated); } -.group\/sidebar-provider[data-viewport] { height: 100dvh; min-height: 0; overflow: hidden; } +.group\/sidebar-provider[data-viewport] { height: 100vh; height: 100dvh; min-height: 0; overflow: hidden; } .chat-app { width: 100%; height: 100%; min-height: 0; overflow: hidden; display: grid; background: var(--color-canvas); grid-template-columns: minmax(0, 1fr); } .sidebar { position: relative; width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden; display: flex; flex-direction: column; border-right: 1px solid var(--sidebar-border); color: var(--sidebar-foreground); padding: var(--space-5) var(--space-3) 0; background: var(--sidebar-background); backdrop-filter: saturate(130%) blur(20px); } [data-sidebar="header"] { flex: 0 0 auto; } @@ -910,7 +909,13 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled [data-sidebar="rail"] { display: none; } .chat-panel { grid-template-rows: calc(64px + env(safe-area-inset-top)) minmax(0, 1fr) auto; } .chat-header { gap: 10px; padding: max(0px, env(safe-area-inset-top)) var(--space-3) 0; } - .conversation.is-empty { padding: var(--space-5) var(--space-4); } + .conversation.is-empty { + display: block; + place-items: initial; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + padding: var(--space-5) var(--space-4); + } .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; } @@ -1218,43 +1223,36 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled } .starter-theme-accordion { - display: flex; min-width: 0; - overflow: hidden; - border: 1px solid var(--color-border); - border-radius: var(--radius-xl); - background: var(--color-canvas-soft); + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-3); } .starter-theme-card { position: relative; min-width: 0; - min-height: 170px; - flex: 1 1 0; + min-height: 156px; display: grid; grid-template-columns: minmax(0, 1fr) 20px; align-items: end; gap: var(--space-3); padding: var(--space-5); - border: 0; - border-right: 1px solid var(--color-border); - background: transparent; + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: var(--color-canvas-soft); cursor: pointer; text-align: left; - transition: background-color 150ms ease-out; -} - -.starter-theme-card:last-child { - border-right: 0; + transition: border-color 180ms ease-out, background-color 180ms ease-out, box-shadow 180ms ease-out, transform 180ms var(--ease-out); } .starter-theme-card:not(:disabled):active { background: color-mix(in srgb, var(--color-action-soft) 56%, var(--color-canvas)); } -.starter-theme-card:not(:disabled):active .starter-arrow { - color: var(--color-action); - transform: translate(2px, -2px); +.starter-theme-card:focus-visible { + border-color: color-mix(in srgb, var(--color-action) 38%, var(--color-border)); + box-shadow: var(--shadow-soft); } @media (hover: hover) { @@ -1278,7 +1276,7 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled } .starter-content span { - max-width: 245px; + max-width: none; overflow: visible; color: var(--color-ink); font-family: var(--font-display); @@ -1334,21 +1332,11 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled } .starter-theme-accordion { - display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } .starter-theme-card { - min-height: 136px; - border-bottom: 1px solid var(--color-border); - } - - .starter-theme-card:nth-child(2n) { - border-right: 0; - } - - .starter-theme-card:nth-last-child(-n + 2) { - border-bottom: 0; + min-height: 142px; } } @@ -1394,17 +1382,12 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled } .starter-theme-accordion { - display: grid; + grid-template-columns: 1fr; + gap: var(--space-3); } .starter-theme-card { - min-height: 116px; - border-right: 0; - border-bottom: 1px solid var(--color-border); - } - - .starter-theme-card:last-child { - border-bottom: 0; + min-height: 112px; } .starter-content span { diff --git a/frontend/tests/birth-time-mobile-scroll-contract.test.ts b/frontend/tests/birth-time-mobile-scroll-contract.test.ts index a60483fb..1b90e629 100644 --- a/frontend/tests/birth-time-mobile-scroll-contract.test.ts +++ b/frontend/tests/birth-time-mobile-scroll-contract.test.ts @@ -5,6 +5,12 @@ import test from "node:test"; const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); test("mobile rectification welcome content starts at the scroll origin", () => { - assert.match(css, /@media \(max-width: 767px\)[\s\S]*?\.conversation\.is-empty\s*\{[^}]*display:\s*block/); + const centeredGrid = css.indexOf(".conversation.is-empty { display: grid"); + const mobileOverride = css.indexOf(".conversation.is-empty {\n display: block", centeredGrid); + + assert.ok(centeredGrid >= 0, "desktop empty state should remain centered"); + assert.ok(mobileOverride > centeredGrid, "mobile display:block must be declared after the desktop grid rule"); assert.match(css, /\.conversation\s*\{[^}]*min-height:\s*0[^}]*overflow-y:\s*auto/); + assert.match(css.slice(mobileOverride), /\.conversation\.is-empty\s*\{[^}]*-webkit-overflow-scrolling:\s*touch/); + assert.match(css, /\.group\\\/sidebar-provider\[data-viewport\]\s*\{[^}]*height:\s*100vh;[^}]*height:\s*100dvh/); }); diff --git a/frontend/tests/starter-theme-layout-contract.test.ts b/frontend/tests/starter-theme-layout-contract.test.ts new file mode 100644 index 00000000..7a6a295d --- /dev/null +++ b/frontend/tests/starter-theme-layout-contract.test.ts @@ -0,0 +1,13 @@ +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"); + +test("starter questions use readable responsive cards instead of a compressed flex row", () => { + assert.match(css, /\.starter-theme-accordion \{[^}]*display: grid;[^}]*grid-template-columns: repeat\(3, minmax\(0, 1fr\)\);/); + assert.match(css, /@media \(min-width: 768px\) and \(max-width: 900px\)[\s\S]*?\.starter-theme-accordion \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);/); + assert.match(css, /@media \(max-width: 767px\)[\s\S]*?\.starter-theme-accordion \{[^}]*grid-template-columns: 1fr;/); + assert.match(css, /\.starter-theme-card \{[^}]*min-height: 156px;[^}]*border: 1px solid var\(--color-border\);[^}]*border-radius: var\(--radius-lg\);/); + assert.doesNotMatch(css, /\.starter-theme-accordion \{[^}]*display: flex;/); +});