Strict sticky-bottom autoscroll for the chat thread: while the viewport is parked at the bottom, the tail follows content growth (streaming tokens, late measurement, Shiki re-highlight) via a useLayoutEffect keyed on the virtualizer's own size signal, pinned in the same pre-paint pass as its scrollToFn so the two never rubber-band. The gate is a single boolean — one upward pixel (scroll/wheel/touch) disarms follow until the user returns to the bottom. Adds a floating jump-to-bottom control that appears once scrolled ~10px away (above the dim threshold so a sub-pixel settle never flashes it), positioned above the composer with respect to the status stack, with a subtle scale + slide in/out animation that honours prefers-reduced-motion. The button bridges to the virtualizer's re-arm + pin path through a small nanostore emitter. Supersedes #43624.
35 lines
1.3 KiB
TypeScript
35 lines
1.3 KiB
TypeScript
import { atom, type WritableAtom } from 'nanostores'
|
|
|
|
// `$threadScrolledUp` flips the instant the viewport leaves the bottom (dims the
|
|
// composer / status stack). `$threadJumpButtonVisible` trips a little further up
|
|
// (~10px) so the floating jump control only shows once meaningfully away.
|
|
export const $threadScrolledUp = atom(false)
|
|
export const $threadJumpButtonVisible = atom(false)
|
|
|
|
// Skip no-op writes so subscribers don't churn on every scroll tick.
|
|
const setter = (target: WritableAtom<boolean>) => (value: boolean) => {
|
|
if (target.get() !== value) {
|
|
target.set(value)
|
|
}
|
|
}
|
|
|
|
export const setThreadScrolledUp = setter($threadScrolledUp)
|
|
export const setThreadJumpButtonVisible = setter($threadJumpButtonVisible)
|
|
|
|
export const resetThreadScroll = () => {
|
|
setThreadScrolledUp(false)
|
|
setThreadJumpButtonVisible(false)
|
|
}
|
|
|
|
// Cross-component bridge: the jump button lives by the composer, the re-arm +
|
|
// pin machinery lives in the virtualizer. The virtualizer registers a handler;
|
|
// the button fires it. Mirrors the composer focus/insert emitter pattern.
|
|
const handlers = new Set<() => void>()
|
|
|
|
export const onScrollToBottomRequest = (handler: () => void) => {
|
|
handlers.add(handler)
|
|
|
|
return () => void handlers.delete(handler)
|
|
}
|
|
|
|
export const requestScrollToBottom = () => handlers.forEach(handler => handler())
|