feat: auto-hide mobile buttons on scroll down

- Add useScrollDirection hook for scroll direction detection
- Hide bookmark and highlight buttons when scrolling down
- Show buttons again when scrolling up
- Smooth opacity transitions for better UX
- Only detect scroll when buttons are visible
- Improves mobile reading experience by maximizing content area
This commit is contained in:
Gigi
2025-10-11 01:39:24 +01:00
parent 72a264a01e
commit 6783ff23f9
4 changed files with 97 additions and 5 deletions

View File

@@ -1,3 +1,6 @@
---
alwaysApply: true
description: anything related to UI/UX
alwaysApply: false
---
This is a mobile-first application. All UI elements should be designed with that in mind. The application should work well on small screens, including older smartphones. The UX should be immaculate on mobile, even when in flight mode. (We use local caches and local relays, so that app works offline too.)

View File

@@ -19,6 +19,7 @@ import { HighlightVisibility } from './HighlightsPanel'
import { HighlightButtonRef } from './HighlightButton'
import { BookmarkReference } from '../utils/contentLoader'
import { useIsMobile } from '../hooks/useMediaQuery'
import { useScrollDirection } from '../hooks/useScrollDirection'
interface ThreePaneLayoutProps {
// Layout state
@@ -86,6 +87,13 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
const isMobile = useIsMobile()
const sidebarRef = useRef<HTMLDivElement>(null)
const highlightsRef = useRef<HTMLDivElement>(null)
// Detect scroll direction to hide/show mobile buttons
const scrollDirection = useScrollDirection({
threshold: 10,
enabled: isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed
})
const showMobileButtons = scrollDirection !== 'down'
// Lock body scroll when mobile sidebar or highlights is open
useEffect(() => {
@@ -204,7 +212,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
{/* Mobile bookmark button - only show when viewing article */}
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && (
<button
className="mobile-hamburger-btn"
className={`mobile-hamburger-btn ${showMobileButtons ? 'visible' : 'hidden'}`}
onClick={props.onToggleSidebar}
aria-label="Open bookmarks"
aria-expanded={props.isSidebarOpen}
@@ -216,7 +224,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
{/* Mobile highlights button - only show when viewing article */}
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && (
<button
className="mobile-highlights-btn"
className={`mobile-highlights-btn ${showMobileButtons ? 'visible' : 'hidden'}`}
onClick={props.onToggleHighlightsPanel}
aria-label="Open highlights"
aria-expanded={!props.isHighlightsCollapsed}

View File

@@ -0,0 +1,59 @@
import { useState, useEffect } from 'react'
export type ScrollDirection = 'up' | 'down' | 'none'
interface UseScrollDirectionOptions {
threshold?: number
enabled?: boolean
}
/**
* Hook to detect scroll direction
* @param options Configuration options
* @param options.threshold Minimum scroll distance to trigger direction change (default: 10)
* @param options.enabled Whether scroll detection is enabled (default: true)
* @returns Current scroll direction ('up', 'down', or 'none')
*/
export function useScrollDirection({
threshold = 10,
enabled = true
}: UseScrollDirectionOptions = {}): ScrollDirection {
const [scrollDirection, setScrollDirection] = useState<ScrollDirection>('none')
useEffect(() => {
if (!enabled) return
let lastScrollY = window.scrollY
let ticking = false
const updateScrollDirection = () => {
const scrollY = window.scrollY
// Only update if scroll distance exceeds threshold
if (Math.abs(scrollY - lastScrollY) < threshold) {
ticking = false
return
}
setScrollDirection(scrollY > lastScrollY ? 'down' : 'up')
lastScrollY = scrollY > 0 ? scrollY : 0
ticking = false
}
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDirection)
ticking = true
}
}
window.addEventListener('scroll', onScroll)
return () => {
window.removeEventListener('scroll', onScroll)
}
}, [threshold, enabled])
return scrollDirection
}

View File

@@ -156,7 +156,18 @@ body.mobile-sidebar-open {
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
transition: transform 0.2s ease, opacity 0.3s ease, visibility 0.3s ease;
}
.mobile-hamburger-btn.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.mobile-hamburger-btn.visible {
opacity: 1;
visibility: visible;
}
.mobile-hamburger-btn:active {
@@ -717,7 +728,18 @@ body.mobile-sidebar-open {
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
transition: transform 0.2s ease, opacity 0.3s ease, visibility 0.3s ease;
}
.mobile-highlights-btn.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.mobile-highlights-btn.visible {
opacity: 1;
visibility: visible;
}
.mobile-highlights-btn:active {