Skip to content

Commit 5fce594

Browse files
committed
docs(knowledge): capture reader pane scrollbar and placeholder layout rules
1 parent 57c47a2 commit 5fce594

2 files changed

Lines changed: 262 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
title: Reader pane scrollbars need pane-owned scroll roots and full-height placeholders
3+
date: 2026-04-19
4+
category: ui-bugs
5+
module: article reader
6+
problem_type: ui_bug
7+
component: react_component
8+
symptoms:
9+
- the desktop reader could fall back toward page-level scrolling instead of keeping the article list and detail panes independently scrollable
10+
- the left pane depended on a fragile `sm:h-full` height chain, while the right pane still used native `overflow-auto`, so scrollbar behavior was asymmetric and hard to discover
11+
- after the detail pane moved into the shared scroll wrapper, empty and unavailable states could lose vertical centering unless the placeholder path also kept a full-height flex chain
12+
root_cause: scope_issue
13+
resolution_type: code_fix
14+
severity: medium
15+
related_components:
16+
- next_page
17+
- testing_framework
18+
tags:
19+
[
20+
article-reader,
21+
dual-pane,
22+
scroll-area,
23+
scrollbar,
24+
overflow,
25+
placeholder-state,
26+
flex-height-chain,
27+
]
28+
---
29+
30+
# Reader pane scrollbars need pane-owned scroll roots and full-height placeholders
31+
32+
## Problem
33+
34+
The desktop reader is designed as a fixed-height dual-pane surface, but the original height and overflow chain let scrolling drift away from the panes themselves. The list pane depended on an ambient height assumption, the detail pane used native `overflow-auto`, and the later detail refactor showed that placeholder states also needed the same full-height contract as real article content.
35+
36+
## Symptoms
37+
38+
- On desktop, the page could become the effective scroll container instead of the list pane and detail pane owning scroll independently.
39+
- The two panes did not share the same scrollbar contract, so the list pane and detail pane behaved differently under overflow.
40+
- Empty or unavailable detail states risked sitting inside a scroll wrapper without enough height context to stay vertically centered.
41+
42+
## What Didn't Work
43+
44+
- Leaving the detail pane on native `overflow-auto` while the list pane used the shared `ScrollArea` kept behavior asymmetric and made scrollbar visibility depend on OS/browser defaults.
45+
- Relying on `sm:h-full` inside the list pane did not create a reliable height chain in a fixed shell; it only worked when ancestor sizing happened to line up.
46+
- Moving placeholder content under the new scroll wrapper without also restoring a `min-h-full` + `flex-1` chain would keep the scroll root but break the previous centered empty-state layout.
47+
48+
## Solution
49+
50+
Make the reader shell and both panes explicit about scroll ownership, then preserve the same height contract for placeholder states.
51+
52+
The shell now owns a fixed viewport boundary, and the shared primitive is a flex-owned scroll root with stable structural hooks:
53+
54+
```tsx
55+
<main className="h-dvh min-h-dvh overflow-hidden ...">
56+
<section className="... h-[calc(100dvh-1rem-2px)] ... overflow-hidden ...">
57+
<ArticleList ... />
58+
<ArticleDetailPane ... />
59+
</section>
60+
</main>
61+
```
62+
63+
```tsx
64+
const ScrollArea = React.forwardRef(
65+
...({ className, children, type = "always", ...props }, ref) => (
66+
<ScrollAreaPrimitive.Root
67+
data-slot="scroll-area"
68+
type={type}
69+
className={cn(
70+
"group/scroll-area relative flex min-h-0 flex-col overflow-hidden",
71+
className,
72+
)}
73+
{...props}
74+
>
75+
<ScrollAreaPrimitive.Viewport
76+
data-slot="scroll-area-viewport"
77+
className="min-h-0 w-full flex-1 rounded-[inherit]"
78+
>
79+
{children}
80+
</ScrollAreaPrimitive.Viewport>
81+
<ScrollBar />
82+
</ScrollAreaPrimitive.Root>
83+
),
84+
);
85+
```
86+
87+
The detail pane now uses the same shared scroll root as the list pane, and placeholder states keep a full-height flex chain so centering survives the refactor:
88+
89+
```tsx
90+
<ScrollArea data-testid="article-detail-scroll-area" className="min-h-0 flex-1">
91+
<div
92+
className={cn(
93+
"px-5 py-5 lg:px-7 lg:py-7 xl:px-8 xl:py-8",
94+
isPlaceholderState && "flex min-h-full flex-col",
95+
)}
96+
>
97+
<div
98+
className={cn(
99+
"mx-auto w-full max-w-5xl",
100+
isPlaceholderState && "flex min-h-full flex-1 flex-col",
101+
)}
102+
>
103+
{content}
104+
</div>
105+
</div>
106+
</ScrollArea>
107+
```
108+
109+
Verification was split across SSR and browser tests:
110+
111+
- `apps/web/app/page.spec.tsx` asserts the fixed shell classes, the two scroll roots, and the placeholder-state full-height wrappers.
112+
- `apps/web/e2e/home.spec.ts` proves `scrollHeight > clientHeight`, independent `scrollTop` changes for each pane, stable headers during pane scrolling, and preserved detail scroll roots for pending and unavailable states.
113+
114+
## Why This Works
115+
116+
The bug was not a single missing class; it was a broken ownership boundary. The shell is supposed to be a clipped reading surface, so the panes must own overflow explicitly. Once both panes use the same `ScrollArea` contract and their bodies opt into `min-h-0 flex-1`, the browser no longer needs to guess where scrolling belongs. The follow-up placeholder fix works for the same reason: empty and unavailable states now inherit the same full-height flex chain as the normal detail body, so layout does not collapse when content is replaced by a centered fallback.
117+
118+
## Prevention
119+
120+
- In fixed-height reader or dashboard layouts, keep headers outside the scroll body and make the pane body the explicit scroll root.
121+
- Do not rely on `h-full` alone inside nested flex shells; preserve the full `overflow-hidden` -> `min-h-0` -> `flex-1` chain from shell to viewport to content wrapper.
122+
- When moving empty, pending, or unavailable states under a shared scroll wrapper, preserve their full-height layout path and add assertions for the wrapper classes that centering depends on.
123+
- Keep stable scroll-root selectors such as `data-testid="article-list-scroll-area"` and `data-slot="scroll-area-viewport"` so browser tests can verify real overflow behavior instead of only markup.
124+
125+
## Related Issues
126+
127+
- Paired implementation plans:
128+
- `docs/en/plans/2026-04-19-001-fix-reader-pane-scrollbars-plan.md`
129+
- `docs/zh-Hans/plans/2026-04-19-001-fix-reader-pane-scrollbars-plan.md`
130+
- Related learning: `docs/en/solutions/logic-errors/article-summary-retryable-failures-must-not-clear-existing-summary-2026-04-18.md` — the reader should keep a stable visible structure even when summary content is pending or unavailable.
131+
- GitHub issue search via `gh issue list --search "reader scrollbar article detail scroll" --state all --limit 5` returned no related issues.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
title: Reader 栏内滚动条修复需要栏内 scroll root 与占位态全高链路
3+
date: 2026-04-19
4+
category: ui-bugs
5+
module: article reader
6+
problem_type: ui_bug
7+
component: react_component
8+
symptoms:
9+
- desktop reader 可能退回到更像页面级滚动,而不是让文章列表栏与详情栏各自独立滚动
10+
- 左栏依赖脆弱的 `sm:h-full` 高度链路,右栏仍使用原生 `overflow-auto`,导致滚动条行为不对称且不易被发现
11+
- detail pane 接入共享 scroll wrapper 之后,如果占位态路径没有继续保持全高 flex 链路,empty 或 unavailable 状态就可能失去垂直居中
12+
root_cause: scope_issue
13+
resolution_type: code_fix
14+
severity: medium
15+
related_components:
16+
- next_page
17+
- testing_framework
18+
tags:
19+
[
20+
article-reader,
21+
dual-pane,
22+
scroll-area,
23+
scrollbar,
24+
overflow,
25+
placeholder-state,
26+
flex-height-chain,
27+
]
28+
---
29+
30+
# Reader 栏内滚动条修复需要栏内 scroll root 与占位态全高链路
31+
32+
## Problem
33+
34+
desktop reader 的设计本来是固定高度的双栏阅读面,但最初的高度/overflow 链路让滚动所有权从栏内漂走了。左栏依赖环境高度碰巧成立,右栏继续走原生 `overflow-auto`,后续 detail refactor 又暴露出:占位态也必须继承和正文一样的全高布局契约,否则 reader 结构虽然还在,布局却会塌。
35+
36+
## Symptoms
37+
38+
- 在 desktop 上,页面本身可能重新变成主要滚动容器,而不是左右两栏各自拥有独立滚动。
39+
- 左右两栏没有共享同一套滚动条契约,overflow 时行为不一致。
40+
- 空态或 unavailable 的 detail 状态进入 scroll wrapper 后,如果没有足够的高度上下文,就可能失去原本的垂直居中。
41+
42+
## What Didn't Work
43+
44+
- 让右栏继续保留原生 `overflow-auto`,而左栏单独使用共享 `ScrollArea`,会让两栏行为持续不对称,也让滚动条可见性受 OS / 浏览器默认策略摆布。
45+
- 在左栏里只保留 `sm:h-full`,并不能在 fixed shell 内建立稳定高度链路;它只是在祖先尺寸刚好对齐时“看起来能工作”。
46+
- 把占位态内容搬进新的 scroll wrapper 却不补回 `min-h-full` + `flex-1` 链路,虽然 scroll root 还在,但之前的居中 empty-state 布局会悄悄丢失。
47+
48+
## Solution
49+
50+
把 reader shell 和两个 pane 的滚动所有权都显式化,再让占位态沿用同样的全高契约。
51+
52+
现在 shell 明确拥有固定视口边界,共享 primitive 也被收紧成 flex-owned scroll root,并暴露稳定的结构 hook:
53+
54+
```tsx
55+
<main className="h-dvh min-h-dvh overflow-hidden ...">
56+
<section className="... h-[calc(100dvh-1rem-2px)] ... overflow-hidden ...">
57+
<ArticleList ... />
58+
<ArticleDetailPane ... />
59+
</section>
60+
</main>
61+
```
62+
63+
```tsx
64+
const ScrollArea = React.forwardRef(
65+
...({ className, children, type = "always", ...props }, ref) => (
66+
<ScrollAreaPrimitive.Root
67+
data-slot="scroll-area"
68+
type={type}
69+
className={cn(
70+
"group/scroll-area relative flex min-h-0 flex-col overflow-hidden",
71+
className,
72+
)}
73+
{...props}
74+
>
75+
<ScrollAreaPrimitive.Viewport
76+
data-slot="scroll-area-viewport"
77+
className="min-h-0 w-full flex-1 rounded-[inherit]"
78+
>
79+
{children}
80+
</ScrollAreaPrimitive.Viewport>
81+
<ScrollBar />
82+
</ScrollAreaPrimitive.Root>
83+
),
84+
);
85+
```
86+
87+
detail pane 现在和左栏共用同一套 scroll root;同时,占位态继续保留全高 flex 链路,这样 refactor 后居中不会丢:
88+
89+
```tsx
90+
<ScrollArea data-testid="article-detail-scroll-area" className="min-h-0 flex-1">
91+
<div
92+
className={cn(
93+
"px-5 py-5 lg:px-7 lg:py-7 xl:px-8 xl:py-8",
94+
isPlaceholderState && "flex min-h-full flex-col",
95+
)}
96+
>
97+
<div
98+
className={cn(
99+
"mx-auto w-full max-w-5xl",
100+
isPlaceholderState && "flex min-h-full flex-1 flex-col",
101+
)}
102+
>
103+
{content}
104+
</div>
105+
</div>
106+
</ScrollArea>
107+
```
108+
109+
验证被拆成 SSR 与浏览器两层:
110+
111+
- `apps/web/app/page.spec.tsx` 断言 fixed shell class、两个 scroll root,以及占位态所依赖的全高 wrapper 结构。
112+
- `apps/web/e2e/home.spec.ts` 证明 `scrollHeight > clientHeight`、左右两栏的 `scrollTop` 可以独立变化、pane 内滚动时 header 保持稳定,以及 pending / unavailable 状态下 detail scroll root 仍然存在。
113+
114+
## Why This Works
115+
116+
这个问题不是少了某一个 class,而是滚动所有权边界错了。shell 的职责是裁剪阅读面,所以 overflow 必须明确落在 pane 内部。一旦左右两栏都收敛到同一套 `ScrollArea` 契约,并让 body 显式进入 `min-h-0 flex-1` 链路,浏览器就不需要再“猜”到底该谁滚。后续的占位态修复也是同一原理:empty / unavailable 状态重新继承了和正常 detail body 一样的全高 flex 链路,所以内容被 fallback 替换时,布局不会塌。
117+
118+
## Prevention
119+
120+
- 在固定高度的 reader 或 dashboard 布局里,把 header 放在 scroll body 外面,并让 pane body 成为显式 scroll root。
121+
- 不要在嵌套 flex shell 里只靠 `h-full`;要从 shell 到 viewport 到 content wrapper 一直保留 `overflow-hidden` -> `min-h-0` -> `flex-1` 这条高度链。
122+
- 当 empty、pending、unavailable 状态被搬进共享 scroll wrapper 时,保留它们的全高布局路径,并为这些居中依赖的 wrapper class 加断言。
123+
- 保留稳定的 scroll-root selector,比如 `data-testid="article-list-scroll-area"``data-slot="scroll-area-viewport"`,让浏览器测试可以验证真实 overflow 行为,而不只是看 markup。
124+
125+
## Related Issues
126+
127+
- 配对实现计划:
128+
- `docs/en/plans/2026-04-19-001-fix-reader-pane-scrollbars-plan.md`
129+
- `docs/zh-Hans/plans/2026-04-19-001-fix-reader-pane-scrollbars-plan.md`
130+
- 相关 learning:`docs/zh-Hans/solutions/logic-errors/article-summary-retryable-failures-must-not-clear-existing-summary-2026-04-18.md` —— 即使 summary 处于 pending 或 unavailable,reader 也应该保持稳定、可见的结构。
131+
- 通过 `gh issue list --search "reader scrollbar article detail scroll" --state all --limit 5` 检索后,没有找到相关 GitHub issue。

0 commit comments

Comments
 (0)