diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 77643789d..07df4305f 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -446,9 +446,9 @@ export function FileTabContent(props: { tab: string }) { ) return ( - + { scroll = el restoreScroll() diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index f0c7573f6..e93ca11a3 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -347,6 +347,7 @@ export function MessageTimeline(props: { placeholderTitle={placeholderTitle} /> { const root = e.currentTarget diff --git a/packages/ui/src/components/scroll-view.css b/packages/ui/src/components/scroll-view.css index a01298f77..a8574cc9f 100644 --- a/packages/ui/src/components/scroll-view.css +++ b/packages/ui/src/components/scroll-view.css @@ -9,9 +9,13 @@ overflow-y: auto; scrollbar-width: none; outline: none; + display: block; + overflow-anchor: none; +} + +.scroll-view__viewport[data-reverse="true"] { display: flex; flex-direction: column-reverse; - overflow-anchor: none; } .scroll-view__viewport::-webkit-scrollbar { diff --git a/packages/ui/src/components/scroll-view.tsx b/packages/ui/src/components/scroll-view.tsx index 16af3d933..a8d3cf0f8 100644 --- a/packages/ui/src/components/scroll-view.tsx +++ b/packages/ui/src/components/scroll-view.tsx @@ -5,13 +5,14 @@ import { FAST_SPRING } from "./motion" export interface ScrollViewProps extends ComponentProps<"div"> { viewportRef?: (el: HTMLDivElement) => void + reverse?: boolean } export function ScrollView(props: ScrollViewProps) { const i18n = useI18n() const [local, events, rest] = splitProps( props, - ["class", "children", "viewportRef", "style"], + ["class", "children", "viewportRef", "style", "reverse"], [ "onScroll", "onWheel", @@ -36,6 +37,8 @@ export function ScrollView(props: ScrollViewProps) { const [thumbTop, setThumbTop] = createSignal(0) const [showThumb, setShowThumb] = createSignal(false) + const reverse = () => local.reverse === true + const updateThumb = () => { if (!viewportRef) return const { scrollTop, scrollHeight, clientHeight } = viewportRef @@ -57,10 +60,11 @@ export function ScrollView(props: ScrollViewProps) { const maxScrollTop = scrollHeight - clientHeight const maxThumbTop = trackHeight - height - // With column-reverse: scrollTop=0 is at bottom, negative = scrolled up - // Normalize so 0 = at top, maxScrollTop = at bottom - const normalizedScrollTop = maxScrollTop + scrollTop - const top = maxScrollTop > 0 ? (normalizedScrollTop / maxScrollTop) * maxThumbTop : 0 + const top = (() => { + if (maxScrollTop <= 0) return 0 + if (!reverse()) return (scrollTop / maxScrollTop) * maxThumbTop + return ((maxScrollTop + scrollTop) / maxScrollTop) * maxThumbTop + })() // Ensure thumb stays within bounds const boundedTop = trackPadding + Math.max(0, Math.min(top, maxThumbTop)) @@ -135,7 +139,8 @@ export function ScrollView(props: ScrollViewProps) { const limit = (top: number) => { const max = viewportRef.scrollHeight - viewportRef.clientHeight - return Math.max(-max, Math.min(0, top)) + if (reverse()) return Math.max(-max, Math.min(0, top)) + return Math.max(0, Math.min(max, top)) } const glide = (top: number) => { @@ -175,13 +180,11 @@ export function ScrollView(props: ScrollViewProps) { break case "Home": e.preventDefault() - // With column-reverse, top of content = -(scrollHeight - clientHeight) - glide(-(viewportRef.scrollHeight - viewportRef.clientHeight)) + glide(reverse() ? -(viewportRef.scrollHeight - viewportRef.clientHeight) : 0) break case "End": e.preventDefault() - // With column-reverse, bottom of content = 0 - glide(0) + glide(reverse() ? 0 : viewportRef.scrollHeight - viewportRef.clientHeight) break case "ArrowUp": e.preventDefault() @@ -206,6 +209,7 @@ export function ScrollView(props: ScrollViewProps) {
{ updateThumb() if (typeof events.onScroll === "function") events.onScroll(e as any)