multica/apps/web/hooks/use-auto-scroll.ts
Naiyuan Qing 66e99136c2 refactor(web): self-contained shadcn UI with base-nova style and design tokens
Migrate all shadcn components into apps/web/components/ui/ so the web app
is fully independent from packages/ui for its UI layer. Update to the
latest shadcn base-nova style. Add missing semantic color variables
(success, warning, info, canvas), font-mono mapping, scrollbar styling,
and wrap Select items in SelectGroup for proper padding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:19:26 +08:00

73 lines
2 KiB
TypeScript

import { type RefObject, useEffect, useRef, useCallback } from "react"
/**
* Auto-scrolls a scroll container to the bottom when its inner content grows,
* as long as the user hasn't scrolled up to read older content.
*
* Returns a `lockRef` that can be set to `true` to temporarily suppress
* auto-scroll (e.g. during history prepend operations).
*/
export function useAutoScroll(ref: RefObject<HTMLElement | null>) {
const stickRef = useRef(true)
const lockRef = useRef(false)
useEffect(() => {
const el = ref.current
if (!el) return
const scrollToBottom = () => {
el.scrollTo({ top: el.scrollHeight })
}
const onScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = el
stickRef.current = scrollHeight - scrollTop - clientHeight < 50
}
const onContentChange = () => {
if (lockRef.current) return
if (stickRef.current) {
scrollToBottom()
}
}
// Watch child element resizes (content growth, image loads, streaming)
const ro = new ResizeObserver(onContentChange)
for (const child of el.children) {
ro.observe(child)
}
// Watch for added/removed child nodes (new messages rendered)
const mo = new MutationObserver((mutations) => {
// Also observe newly added elements
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
ro.observe(node)
}
}
}
onContentChange()
})
mo.observe(el, { childList: true, subtree: true })
el.addEventListener("scroll", onScroll, { passive: true })
// Initial scroll to bottom
scrollToBottom()
return () => {
el.removeEventListener("scroll", onScroll)
ro.disconnect()
mo.disconnect()
}
}, [ref])
/** Temporarily suppress auto-scroll during prepend operations */
const suppressAutoScroll = useCallback(() => {
lockRef.current = true
return () => { lockRef.current = false }
}, [])
return { suppressAutoScroll }
}