Scroll-down disc rides the dock: anchored above the composer, never over it

The scroll-to-bottom disc (ConversationScrollButton) was the scroller's
last child — DOM order painted it ABOVE the dock overlay, so it floated
at the scroller's bottom edge, partly covering the composer (owner
refinement round 3, 2026-09-24: it must ALWAYS sit above the composer,
never over any element, and stay positioned with respect to the
composer — which grows a lot taller when a question owns the band).

The fix is a DOM move, not a coordinate: the button now renders INSIDE
the DockOverlay, as a sibling of the DOCK container within the dock
block — `absolute bottom-full mb-2`, anchored to the BLOCK's top edge.
Because the anchor is the block itself (not the scroller's bottom):

- it sits above the composer shell AND the stats row, in the
  gradient's transparent band;
- it rides every height change LIVE: typing growth, a pending
  question expanding the band (verified: the disc moves with the
  393px question form, 9px clear), the 180ms fold morph to the slim
  bar (8px clear of the folded bar — no overlap mid-morph either,
  height animations reflow per frame);
- pointer-events-auto (the overlay is none by default); the
  isAtBottom/scrollToBottom context still reaches it — DockOverlay
  is a Conversation child.

The use-stick-to-bottom pattern comment updated; the shared
ConversationScrollButton component itself is untouched (positioning
arrives via className, twMerge swaps bottom-4 → bottom-full).

Live-verified on :3001 with a tall transcript, all three states:
chat mode (15px clear of the composer textarea), expanded question
(9px clear of the form top), folded slim bar (8px clear). 902/902.
This commit is contained in:
amosblomqvist
2026-09-24 19:13:22 +03:00
parent 401a21e83f
commit 4936b776ab
+17 -4
View File
@@ -1070,9 +1070,11 @@ export function Chat({
<div aria-hidden data-testid="dock-spacer" style={{ height: dockReserve }} />
</ConversationContent>
{/* The dock: an overlay sibling of the scroller (the
use-stick-to-bottom pattern — like the scroll button). Before
the button in DOM order so the button keeps painting above
the dock, as it always has. */}
use-stick-to-bottom pattern). The scroll-down button lives
INSIDE the overlay now (owner refinement round, 2026-09-24):
anchored to the dock block's top edge, so it rides every
height morph (typing, a question growing the band, the
fold) and NEVER overlaps the composer. */}
<DockOverlay
pending={pending}
chatId={chatId}
@@ -1128,7 +1130,6 @@ export function Chat({
});
}}
/>
<ConversationScrollButton />
</Conversation>
{/* The session-scoped focuses (chrome-melt wave, 2026-09-19): mounted
@@ -1504,6 +1505,18 @@ function DockOverlay({
turns={turns}
dictationSlot={dictationSlot}
/>
{/* The scroll-down disc (moved here from the Conversation,
owner refinement round 2026-09-24): anchored to the BLOCK's
top edge — above the composer shell AND the stats row, in
the gradient's transparent band — so it never overlaps
any dock element. Because the anchor is the block itself
(not the scroller's bottom), it rides every height change
LIVE: typing growth, a pending question expanding the
band, the 180ms fold morph. pointer-events-auto: the
overlay is none by default. The isAtBottom/scrollToBottom
context still reaches it — DockOverlay is a Conversation
child. */}
<ConversationScrollButton className="pointer-events-auto bottom-full mb-2" />
</div>
{/* The plan marginalia (owner, plan-placement round): the bare
diagram in the band's right flank — boundaless, meet-fitted, part