mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 10:14:22 +01:00
feat(web): add scroll to last message button
Add intelligent floating scroll button for long conversations that: - Only appears when scrolling down (direction-aware) - Auto-hides after 3 seconds of inactivity - Stays visible on hover to prevent accidental disappearance - Uses consistent design patterns with repo styling - Includes proper accessibility features 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: Jeremy Osih <osih.jeremy@gmail.com> Co-Authored-By: opencode <noreply@opencode.ai>
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
|||||||
IconMagnifyingGlass,
|
IconMagnifyingGlass,
|
||||||
IconWrenchScrewdriver,
|
IconWrenchScrewdriver,
|
||||||
IconDocumentMagnifyingGlass,
|
IconDocumentMagnifyingGlass,
|
||||||
|
IconArrowDown,
|
||||||
} from "./icons"
|
} from "./icons"
|
||||||
import DiffView from "./DiffView"
|
import DiffView from "./DiffView"
|
||||||
import CodeBlock from "./CodeBlock"
|
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 data = createMemo(() => {
|
||||||
const result = {
|
const result = {
|
||||||
rootDir: undefined as string | undefined,
|
rootDir: undefined as string | undefined,
|
||||||
@@ -875,6 +953,7 @@ export default function Share(props: {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1975,6 +2054,21 @@ export default function Share(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Floating scroll to bottom button */}
|
||||||
|
<Show when={showScrollButton()}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={styles.scrollButton}
|
||||||
|
onClick={scrollToBottom}
|
||||||
|
onMouseEnter={handleButtonMouseEnter}
|
||||||
|
onMouseLeave={handleButtonMouseLeave}
|
||||||
|
title="Scroll to bottom"
|
||||||
|
aria-label="Scroll to bottom"
|
||||||
|
>
|
||||||
|
<IconArrowDown width={20} height={20} />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user