Commit 57495ec
authored
feat: Zed-style overlay scrollbars (#509)
* feat(ui): Zed-style overlay scrollbars for plan mode
Wide, translucent, full-length overlay scrollbars replacing the 6px
WebKit rail that users couldn't reliably grab. Click the track to
page-animate toward the click, drag the thumb, no layout shift, Firefox
parity.
Wraps plan-mode scroll containers (main viewer, annotation panel,
sidebar, settings, export modal) in a new <OverlayScrollArea> component
backed by overlayscrollbars-react. The library handles pointer capture,
click-to-jump, hover reveal, auto-hide, RTL, touch, momentum, and
cross-browser consistency. ClickScrollPlugin registered explicitly so
`clickScroll: true` actually pages (otherwise it silently no-ops).
Plumbing:
- New ScrollViewportContext + useScrollViewport() hook so descendants
(TableOfContents, PinpointOverlay, Viewer sticky observer) can reach
the real scrolling element instead of document.querySelector('main'),
which no longer returns the scroll node after wrapping.
- New useOverlayViewport() hook — canonical ref+state+callback pattern
bridging the library viewport into components that need both
imperative access and effect re-runs when the viewport mounts.
- useActiveSection gains an optional scrollElement arg so its
IntersectionObserver root re-attaches when the viewport becomes
available (ref mutations don't retrigger effects).
Theme:
- New .os-theme-plannotator tokens sourced from existing theme CSS
variables (translucent muted-foreground for handle, transparent track
at rest). 10px at rest, 14px on hover. Color + width transitions
only — deliberately does not reintroduce transform/opacity in global
transitions, preserving the abc952f scroll-jank fix.
- Firefox `scrollbar-width: thin` + `scrollbar-color` fallback for
unwrapped surfaces (micro-scrollers, future dockview).
- print.css hides .os-scrollbar during print.
ResizeHandle (#354 regression guard):
- `side="right"` touch area retuned from `-right-3 left-0` to
`-right-3 left-3` to clear the 14px hover scrollbar. Load-bearing
comment added explicitly warning against simplification because #354
has already regressed twice.
Intentionally left native: max-h-24 inline scrollers
(EditorAnnotationCard, AgentsTab, ThemeTab), dropdown menus — a 14px
overlay scrollbar would dominate those UI elements.
Fixes #354
Follow-up to #359, #465 (both fixed #354, which kept regressing)
Preserves #253 bidirectional annotation scroll
Preserves #452 file-switch reset (plan mode portion)
For provenance purposes, this commit was AI assisted.
* feat(review): overlay scrollbars for file tree, sidebar, and PR panels
Wrap the trivial code-review scroll containers in <OverlayScrollArea>:
FileTree, ReviewSidebar content area, AITab chat history, PRCommentsTab
timeline, and the ReviewPRSummary / ReviewPRChecks dock panels.
None of these components read scrollTop / scrollHeight / scrollLeft
directly — all descendant queries use containerRef.current.querySelector
and all scroll-to-target calls use element.scrollIntoView, which walks
up to the nearest scrollable ancestor (now the library viewport). No
plumbing changes required.
AITab was originally scoped to Commit B but an audit of its scroll
effects (jump-to-question + auto-scroll-to-bottom) confirmed it only
uses descendant scrollIntoView, so it ships here.
DiffViewer and LiveLogViewer follow in a separate commit — they
programmatically read/write scrollTop and need explicit viewport
plumbing via useOverlayViewport.
For provenance purposes, this commit was AI assisted.
* feat(review): overlay scrollbars for diff viewer and live logs
Wrap DiffViewer's main scroll container and LiveLogViewer's log pane in
<OverlayScrollArea>, plumbing containerRef through useOverlayViewport
so imperative scroll reads/writes and IntersectionObserver roots land
on the real library viewport, not the OverlayScrollArea host.
DiffViewer:
- `previousScrollFilePathRef` guard added: the file-switch reset
(#452) now only advances the tracking ref once the scroll actually
executed, closing a race where a file switch landing before the
library viewport attached would leave the ref stale while the
scrollTo silently no-oped.
- `viewport` added to every effect dep that reads containerRef.current
(file-switch reset, annotation scroll, search-highlight apply +
swap, scroll-to-match) so they re-run when the viewport mounts.
Without this they'd silently no-op on first paint.
- Split-view sync via @pierre/diffs unaffected — its ScrollSyncManager
attaches scroll listeners to its own internal codeDeletions /
codeAdditions elements inside the content, not the outer scroller.
LiveLogViewer:
- React onScroll replaced with a native addEventListener('scroll', ...)
inside a useEffect keyed on `viewport` because React's onScroll
doesn't bubble across the library's wrapper layers.
- Follow-tail heuristic (`scrollHeight - scrollTop - clientHeight < 40`)
and the auto-scroll-to-bottom assignment both preserved verbatim —
only the ref target changed from the raw div to the library viewport.
Preserves #452 file-switch reset
Preserves #253 bidirectional annotation scroll (diff side)
For provenance purposes, this commit was AI assisted.
* fix(ui): address PR #509 review — viewport delivery and resize handle
Two P1 bugs from the review, plus four correctness/cleanup items.
P1: OverlayScrollbars viewport was never delivered to consumers.
handleOsRef was reading osInstance() synchronously from the React ref
callback, but `defer: true` queues the library's initialization for a
later frame — at ref-callback time, the internal instance ref is still
null. With a stable useCallback the ref never re-fires, so
onViewportReady was never called with a real viewport. Every consumer
of useOverlayViewport/useScrollViewport stayed permanently null:
useActiveSection, TableOfContents.handleNavigate, Viewer sticky
detection, PinpointOverlay, DiffViewer file-switch reset, LiveLogViewer
follow-tail — all silently no-ops.
Fix: deliver the viewport via the library's own `events.initialized`
and `events.destroyed` callbacks, which fire exactly when elements are
ready and when they're torn down. `getViewport()` prefers the tracked
viewport ref over the imperative osInstance() path so late callers
still work.
P1: right-side ResizeHandle touch area was 0px wide.
Touch area is an absolute-positioned child of a w-0 parent, so actual
width = parent - left - right. With side='right' I'd set `left-3
-right-3`, which evaluates to `0 - 12 - (-12) = 0`. The annotation
panel resize handle in plan mode and both right handles in code review
had no draggable region. The visible 4px track has no event handlers,
so the only affordance was cursor-style feedback — drag did nothing.
Fix: revert to `left-0 -right-3` (12px wide, entirely right of the
boundary, no left encroachment into the adjacent scrollbar zone —
which was the original correct value before this branch). Rewrote the
comment to explain the geometry trap instead of protecting the value
that broke it.
Additional fixes:
- OverlayScrollArea: prefers-reduced-motion now reactive via
useSyncExternalStore — OS toggle mid-session propagates to mounted
instances instead of staying frozen at mount-time snapshot.
- OverlayScrollArea: `ref as never` replaced with a narrow cast to
`React.RefCallback<OverlayScrollbarsComponentRef<'div'>>` so future
signature changes get type feedback.
- PinpointOverlay: window resize listener moved above the scroll
viewport guard so it attaches unconditionally. Scroll listener still
requires the viewport (correct). Old code always registered resize
on window; new code was accidentally skipping it when viewport was
null.
- TableOfContents: `className || default` changed to `className ??
default`. An explicit empty string from a caller (SidebarContainer
passing className="" now that it wraps us in an OverlayScrollArea)
should mean "no container styling", not "use the default". The old
|| operator treated "" as falsy and applied the default, leaving
dead overflow-y-auto and unintended backdrop-blur on the nav.
- useOverlayViewport: removed redundant double cast and `?? null`
no-op in the ref setter.
For provenance purposes, this commit was AI assisted.
* fix(ui): print clipping with overlay scrollbar wrappers
When <main> is wrapped in OverlayScrollArea, the library adds its own
attribute-selector CSS rules: `[data-overlayscrollbars~="host"]` gets
`overflow: hidden !important` and `[data-overlayscrollbars-viewport]`
gets `overflow-x/y: hidden` (or scroll) with fixed viewport heights.
These beat our existing `main { overflow: visible !important }` print
override on specificity — attribute selectors outrank tag selectors
even when both use `!important`. Result: long plans printed only the
currently-visible viewport instead of flowing across pages.
Fix: add a print-scoped override that targets the library's attribute
selectors directly, setting overflow:visible, height:auto,
max-height:none, and display:block to defeat both the overflow clip
and the flex layout the library applies to host/padding wrappers.
Verified by printing a multi-page plan in the dev server.
For provenance purposes, this commit was AI assisted.
* fix(ui): persistent overlay scrollbar, remove dead reduced-motion rule
Switch autoHide from 'leave' to 'never'. The previous behavior faded
the scrollbar 800ms after the pointer left the scroll host, which felt
broken in a common interaction pattern: click a TOC entry (pointer in
the sidebar) → trackpad-scroll (pointer still in sidebar) → 800ms idle
→ scrollbar disappears. User had to hover the right edge to bring it
back on every interaction.
Persistent visibility matches the pattern used by every editor-class
technical app (VS Code, Zed, JetBrains, Xcode, Sublime) where the
scrollbar is both a position indicator and a targeting surface for
click-to-jump. Overlay scrollbars cost zero layout space, so "always
visible" has no downside.
Also removes the dead `.os-theme-plannotator .os-scrollbar` selector
from the reduced-motion block. The library applies the theme class
directly to the .os-scrollbar element itself (verified in the library
runtime source, not just CSS), so a descendant selector with a space
matches nothing. The track and handle selectors in the same block
work correctly (they really are descendants) and are preserved.
The component's own prefers-reduced-motion hook and helpers are now
unused (autoHide is unconditionally 'never') and removed. Reduced
motion is still honored for the hover color + width transitions on
track/handle via the CSS @media (prefers-reduced-motion: reduce)
block in theme.css, which the browser evaluates independently.
For provenance purposes, this commit was AI assisted.
* docs(ui): update OverlayScrollArea jsdoc to match persistent-scrollbar behavior
The component header still described the old autoHide:'leave' behavior
and implied the reduced-motion branch was in the component itself.
Neither is true after cf137bf. Rewrite the jsdoc to describe the
current behavior: always visible, no fade, reduced-motion handled via
a CSS media query in theme.css rather than a React branch.
For provenance purposes, this commit was AI assisted.
* fix(ui): recompute scrollbar on content resize (pierre/diffs expand-lines)
When pierre/diffs expanded context lines on a file whose diff previously
fit inside the viewport, the scrollbar stayed hidden until the user
manually scrolled or dragged the split-ratio handle to force a layout
recalculation.
Root cause: OverlayScrollbars' internal content observer doesn't see
the mutation because pierre/diffs renders inside a shadow DOM, and
MutationObserver does not pierce shadow DOM by default. The library's
own host-level size observer doesn't help either — the host (our
<main> / flex-1 container) has a fixed layout size that doesn't
change when content grows.
Fix: track the OverlayScrollbars instance in state and attach a
ResizeObserver to the viewport's first element child in a useEffect.
When the content's layout box grows — which happens even when the
growth originates inside a shadow tree, because layout size
propagates from shadow content to the shadow host — the observer
fires and calls `instance.update(true)` to recompute scrollbar
visibility. The call is debounced through requestAnimationFrame so
the browser commits the new layout before OverlayScrollbars reads
dimensions.
Verified manually in the compiled binary against PR #509 itself:
opening a small file, clicking pierre's expand-lines control, now
reveals the scrollbar immediately. No regression observed in normal
scroll / click-to-jump / file-switch / annotation-click paths.
For provenance purposes, this commit was AI assisted.
* docs(ui): document ResizeObserver content-resize mechanism
Add the content-resize auto-recompute behavior to OverlayScrollArea's
jsdoc header so the "what does this component do" summary is complete.
The inline comment on the effect already explains the mechanism; this
just surfaces it at the top.
For provenance purposes, this commit was AI assisted.1 parent 1e8a60b commit 57495ec
25 files changed
Lines changed: 535 additions & 73 deletions
File tree
- packages
- editor
- review-editor
- components
- dock/panels
- ui
- components
- sidebar
- hooks
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
40 | 43 | | |
41 | 44 | | |
42 | 45 | | |
| |||
126 | 129 | | |
127 | 130 | | |
128 | 131 | | |
129 | | - | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
130 | 141 | | |
131 | 142 | | |
132 | 143 | | |
| |||
383 | 394 | | |
384 | 395 | | |
385 | 396 | | |
386 | | - | |
| 397 | + | |
387 | 398 | | |
388 | 399 | | |
389 | 400 | | |
| |||
1514 | 1525 | | |
1515 | 1526 | | |
1516 | 1527 | | |
| 1528 | + | |
1517 | 1529 | | |
1518 | 1530 | | |
1519 | 1531 | | |
| |||
1586 | 1598 | | |
1587 | 1599 | | |
1588 | 1600 | | |
1589 | | - | |
| 1601 | + | |
| 1602 | + | |
| 1603 | + | |
| 1604 | + | |
| 1605 | + | |
| 1606 | + | |
1590 | 1607 | | |
1591 | 1608 | | |
1592 | 1609 | | |
| |||
1699 | 1716 | | |
1700 | 1717 | | |
1701 | 1718 | | |
1702 | | - | |
| 1719 | + | |
1703 | 1720 | | |
1704 | 1721 | | |
1705 | 1722 | | |
| |||
1726 | 1743 | | |
1727 | 1744 | | |
1728 | 1745 | | |
| 1746 | + | |
1729 | 1747 | | |
1730 | 1748 | | |
1731 | 1749 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
188 | 189 | | |
189 | 190 | | |
190 | 191 | | |
191 | | - | |
| 192 | + | |
| 193 | + | |
192 | 194 | | |
193 | 195 | | |
194 | 196 | | |
| |||
261 | 263 | | |
262 | 264 | | |
263 | 265 | | |
| 266 | + | |
264 | 267 | | |
265 | 268 | | |
266 | 269 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| |||
198 | 200 | | |
199 | 201 | | |
200 | 202 | | |
201 | | - | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
202 | 208 | | |
203 | 209 | | |
204 | 210 | | |
| |||
298 | 304 | | |
299 | 305 | | |
300 | 306 | | |
301 | | - | |
302 | | - | |
303 | | - | |
304 | | - | |
305 | | - | |
306 | | - | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
307 | 316 | | |
308 | 317 | | |
309 | 318 | | |
| |||
328 | 337 | | |
329 | 338 | | |
330 | 339 | | |
331 | | - | |
| 340 | + | |
332 | 341 | | |
333 | 342 | | |
334 | 343 | | |
| |||
349 | 358 | | |
350 | 359 | | |
351 | 360 | | |
352 | | - | |
| 361 | + | |
353 | 362 | | |
354 | 363 | | |
355 | 364 | | |
356 | 365 | | |
357 | 366 | | |
358 | 367 | | |
359 | | - | |
| 368 | + | |
360 | 369 | | |
361 | 370 | | |
362 | 371 | | |
363 | 372 | | |
364 | 373 | | |
365 | | - | |
| 374 | + | |
366 | 375 | | |
367 | 376 | | |
368 | 377 | | |
| |||
574 | 583 | | |
575 | 584 | | |
576 | 585 | | |
577 | | - | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
578 | 592 | | |
579 | 593 | | |
580 | 594 | | |
| |||
664 | 678 | | |
665 | 679 | | |
666 | 680 | | |
667 | | - | |
| 681 | + | |
668 | 682 | | |
669 | 683 | | |
670 | 684 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
353 | 354 | | |
354 | 355 | | |
355 | 356 | | |
356 | | - | |
| 357 | + | |
| 358 | + | |
357 | 359 | | |
358 | 360 | | |
359 | 361 | | |
| |||
393 | 395 | | |
394 | 396 | | |
395 | 397 | | |
| 398 | + | |
396 | 399 | | |
397 | 400 | | |
398 | 401 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
| |||
24 | 26 | | |
25 | 27 | | |
26 | 28 | | |
27 | | - | |
| 29 | + | |
| 30 | + | |
28 | 31 | | |
29 | 32 | | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
35 | 45 | | |
36 | 46 | | |
37 | 47 | | |
38 | 48 | | |
39 | 49 | | |
40 | 50 | | |
41 | | - | |
| 51 | + | |
42 | 52 | | |
43 | 53 | | |
44 | 54 | | |
| |||
56 | 66 | | |
57 | 67 | | |
58 | 68 | | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
63 | 72 | | |
| 73 | + | |
64 | 74 | | |
65 | 75 | | |
66 | 76 | | |
| |||
73 | 83 | | |
74 | 84 | | |
75 | 85 | | |
76 | | - | |
| 86 | + | |
| 87 | + | |
77 | 88 | | |
78 | 89 | | |
79 | 90 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
309 | 310 | | |
310 | 311 | | |
311 | 312 | | |
312 | | - | |
| 313 | + | |
| 314 | + | |
313 | 315 | | |
314 | 316 | | |
315 | 317 | | |
| |||
396 | 398 | | |
397 | 399 | | |
398 | 400 | | |
| 401 | + | |
399 | 402 | | |
400 | 403 | | |
401 | 404 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
282 | 283 | | |
283 | 284 | | |
284 | 285 | | |
285 | | - | |
| 286 | + | |
286 | 287 | | |
287 | 288 | | |
288 | 289 | | |
| |||
439 | 440 | | |
440 | 441 | | |
441 | 442 | | |
442 | | - | |
| 443 | + | |
443 | 444 | | |
444 | 445 | | |
445 | 446 | | |
| |||
Lines changed: 3 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
42 | | - | |
| 43 | + | |
43 | 44 | | |
44 | | - | |
| 45 | + | |
45 | 46 | | |
46 | 47 | | |
0 commit comments