diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index d1127615..008f863c 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -39,6 +39,7 @@ import { IconMagnifyingGlass, IconWrenchScrewdriver, IconDocumentMagnifyingGlass, + IconArrowDown, } from "./icons" import DiffView from "./DiffView" import CodeBlock from "./CodeBlock" @@ -594,12 +595,17 @@ export default function Share(props: { info: Session.Info messages: Record }) { + let lastScrollY = 0 let hasScrolled = false + let scrollTimeout: number | undefined const id = props.id const params = new URLSearchParams(window.location.search) const debug = params.get("debug") === "true" + const [showScrollButton, setShowScrollButton] = createSignal(false) + const [isButtonHovered, setIsButtonHovered] = createSignal(false) + const anchorId = createMemo(() => { const raw = window.location.hash.slice(1) const [id] = raw.split("-") @@ -715,6 +721,54 @@ export default function Share(props: { }) }) + function checkScrollNeed() { + const currentScrollY = window.scrollY + const isScrollingDown = currentScrollY > lastScrollY + const scrolled = currentScrollY > 200 // Show after scrolling 200px + const isNearBottom = window.innerHeight + currentScrollY >= document.body.scrollHeight - 100 + + // Only show when scrolling down, scrolled enough, and not near bottom + const shouldShow = isScrollingDown && scrolled && !isNearBottom + + // Update last scroll position + lastScrollY = currentScrollY + + if (shouldShow) { + setShowScrollButton(true) + // Clear existing timeout + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + // Hide button after 3 seconds of no scrolling (unless hovered) + scrollTimeout = window.setTimeout(() => { + if (!isButtonHovered()) { + setShowScrollButton(false) + } + }, 3000) + } else if (!isButtonHovered()) { + // Only hide if not hovered (to prevent disappearing while user is about to click) + setShowScrollButton(false) + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + } + } + + onMount(() => { + lastScrollY = window.scrollY // Initialize scroll position + checkScrollNeed() + window.addEventListener("scroll", checkScrollNeed) + window.addEventListener("resize", checkScrollNeed) + }) + + onCleanup(() => { + window.removeEventListener("scroll", checkScrollNeed) + window.removeEventListener("resize", checkScrollNeed) + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + }) + const data = createMemo(() => { const result = { rootDir: undefined as string | undefined, @@ -825,6 +879,7 @@ export default function Share(props: { )} + @@ -1902,6 +1957,36 @@ export default function Share(props: { + + + + ) } diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css index ae45d376..dafbdd8a 100644 --- a/packages/web/src/components/share.module.css +++ b/packages/web/src/components/share.module.css @@ -784,3 +784,31 @@ } } } + +.scroll-button { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 2.5rem; + height: 2.5rem; + border-radius: 0.25rem; + border: 1px solid var(--sl-color-divider); + background-color: var(--sl-color-bg-surface); + color: var(--sl-color-text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease, opacity 0.5s ease; + z-index: 100; + appearance: none; + opacity: 1; + + &:active { + transform: translateY(1px); + } + + svg { + display: block; + } +}