diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index 07b12946..5510f5a6 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -40,6 +40,7 @@ import { IconMagnifyingGlass, IconWrenchScrewdriver, IconDocumentMagnifyingGlass, + IconArrowDown, } from "./icons" import DiffView from "./DiffView" import CodeBlock from "./CodeBlock" @@ -721,6 +722,83 @@ export default function Share(props: { }) }) + const [showScrollButton, setShowScrollButton] = createSignal(false) + const [isButtonHovered, setIsButtonHovered] = createSignal(false) + let scrollTimeout: number | undefined + let lastScrollY = 0 + + const 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) + } + } + } + + const handleButtonMouseEnter = () => { + setIsButtonHovered(true) + // Clear timeout when hovering + if (scrollTimeout) { + clearTimeout(scrollTimeout) + } + } + + const handleButtonMouseLeave = () => { + setIsButtonHovered(false) + // Restart timeout when leaving hover + if (showScrollButton()) { + scrollTimeout = window.setTimeout(() => { + if (!isButtonHovered()) { + setShowScrollButton(false) + } + }, 3000) + } + } + + const scrollToBottom = () => { + document.body.scrollIntoView({ behavior: "smooth", block: "end" }) + } + + 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, @@ -875,6 +953,7 @@ export default function Share(props: { )} + @@ -1975,6 +2054,21 @@ export default function Share(props: { + + {/* Floating scroll to bottom button */} + + + ) } diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css index 53f082c9..a52fd176 100644 --- a/packages/web/src/components/share.module.css +++ b/packages/web/src/components/share.module.css @@ -760,3 +760,38 @@ } } } + +.scrollButton { + 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; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.15s ease, opacity 0.5s ease; + z-index: 100; + appearance: none; + opacity: 1; + + &:hover { + color: var(--sl-color-text); + border-color: var(--sl-color-hairline); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &:active { + transform: translateY(1px); + } + + svg { + display: block; + } +}