Parcourir la source

Merge branch 'jeremyosih-feat/scroll-to-bottom-button' into dev

Jay V il y a 8 mois
Parent
commit
688f3fd12f

+ 85 - 0
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<string, Message.Info>
 }) {
+  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<string | null>(() => {
     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: {
               </span>
             )}
           </div>
+
         </div>
       </div>
 
@@ -1902,6 +1957,36 @@ export default function Share(props: {
           </div>
         </div>
       </Show>
+
+      <Show when={showScrollButton()}>
+        <button
+          type="button"
+          class={styles["scroll-button"]}
+          onClick={() =>
+            document.body.scrollIntoView({ behavior: "smooth", block: "end" })
+          }
+          onMouseEnter={() => {
+            setIsButtonHovered(true)
+            if (scrollTimeout) {
+              clearTimeout(scrollTimeout)
+            }
+          }}
+          onMouseLeave={() => {
+            setIsButtonHovered(false)
+            if (showScrollButton()) {
+              scrollTimeout = window.setTimeout(() => {
+                if (!isButtonHovered()) {
+                  setShowScrollButton(false)
+                }
+              }, 3000)
+            }
+          }}
+          title="Scroll to bottom"
+          aria-label="Scroll to bottom"
+        >
+          <IconArrowDown width={20} height={20} />
+        </button>
+      </Show>
     </main>
   )
 }

+ 28 - 0
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;
+  }
+}