feat(entry-content): introduce navigation hints constants and refactor hint logic

- Added constants for navigation hints, including icons and text, to improve maintainability and clarity.
- Refactored the EntryNavigationHandler and useEntryNavigationHints hook to utilize these constants, enhancing the user experience with clearer guidance.
- Updated styling for hint buttons to improve visual consistency across components.

These changes collectively streamline the navigation hint implementation, providing a more intuitive interaction model for users.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-08-15 21:37:52 +08:00
parent 92dbd65690
commit 60ea682fba
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
5 changed files with 131 additions and 44 deletions

View File

@ -36,6 +36,7 @@ import { useCommandHotkey } from "~/modules/command/hooks/use-register-hotkey"
import { useWheelGestureClose } from "~/modules/entry-column/hooks/useWheelGestureClose"
import { ApplyEntryActions } from "../../ApplyEntryActions"
import { NAVIGATION_HINTS_ICONS, NAVIGATION_HINTS_TEXT } from "../../constants/navigation-hints"
import { useEntryContent } from "../../hooks"
import { useEntryNavigationHints } from "../../hooks/useEntryNavigationHints"
import { AIEntryHeader } from "../entry-header"
@ -278,7 +279,6 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => {
} = useEntryNavigationHints({
enabled: when && !!entryId,
entryId,
scrollThreshold: 100,
})
const isZenMode = useIsZenMode()
@ -323,7 +323,7 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => {
className={clsx(
"group pointer-events-auto flex items-center gap-2",
"rounded-full border px-3.5 py-2",
"border-border/40 bg-material-ultra-thin/70 shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]",
"border-border/40 bg-material-ultra-thick shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]",
"hover:bg-material-thin/70 hover:border-border/60 active:scale-[0.98]",
"backdrop-blur-background",
)}
@ -339,24 +339,24 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => {
{/* First entry hint */}
{showFirstEntryHint &&
renderHintButton(
tw`i-mgc-up-cute-re`,
"Scroll up or click left-top back button to exit",
NAVIGATION_HINTS_ICONS.ARROW_UP,
NAVIGATION_HINTS_TEXT.SCROLL_UP_EXIT,
"top",
)}
{/* Scroll threshold hint or wheel gesture hint */}
{(showScrollThresholdHint || showScrollHint) &&
renderHintButton(
tw`i-mingcute-arrow-left-up-line`,
"Scroll up or click left-top back button to exit",
NAVIGATION_HINTS_ICONS.ARROW_LEFT_UP,
NAVIGATION_HINTS_TEXT.SCROLL_UP_EXIT,
"top",
)}
{/* Bottom hint */}
{showBottomHint &&
renderHintButton(
tw`i-mingcute-arrow-to-down-line`,
"Press ESC or click left-top back button to exit",
NAVIGATION_HINTS_ICONS.ARROW_TO_DOWN,
NAVIGATION_HINTS_TEXT.ESC_EXIT,
"bottom",
)}
</AnimatePresence>

View File

@ -37,6 +37,7 @@ import { WrappedElementProvider } from "~/providers/wrapped-element-provider"
import { AISummary } from "../../AISummary"
import { ApplyEntryActions } from "../../ApplyEntryActions"
import { NAVIGATION_HINTS_ICONS, NAVIGATION_HINTS_TEXT } from "../../constants/navigation-hints"
import { useEntryContent, useEntryMediaInfo } from "../../hooks"
import { useEntryNavigationHints } from "../../hooks/useEntryNavigationHints"
import { EntryHeader } from "../entry-header"
@ -330,7 +331,6 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => {
const { showFirstEntryHint, showScrollHint, showBottomHint } = useEntryNavigationHints({
enabled: when && !!entryId,
entryId,
scrollThreshold: 100,
})
// Render hint button with different states
@ -367,26 +367,22 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => {
{/* First entry hint */}
{showFirstEntryHint &&
renderHintButton(
"i-mgc-arrow-up-cute-re",
"Scroll up or click left-top back button to exit",
NAVIGATION_HINTS_ICONS.ARROW_UP,
NAVIGATION_HINTS_TEXT.SCROLL_UP_EXIT,
"top",
)}
{/* Scroll threshold hint */}
{showScrollHint &&
renderHintButton(
"i-mgc-arrow-up-cute-re",
"Scroll up or click left-top back button to exit",
NAVIGATION_HINTS_ICONS.ARROW_UP,
NAVIGATION_HINTS_TEXT.SCROLL_UP_EXIT,
"top",
)}
{/* Bottom hint */}
{showBottomHint &&
renderHintButton(
"i-mgc-close-cute-re",
"Press ESC or click left-top back button to exit",
"bottom",
)}
renderHintButton(NAVIGATION_HINTS_ICONS.CLOSE, NAVIGATION_HINTS_TEXT.ESC_EXIT, "bottom")}
</AnimatePresence>
)
}

View File

@ -154,7 +154,7 @@ export const EntryScrollingAndNavigationHandler = ({
"pointer-events-none absolute !right-1/2 z-40 !translate-x-1/2",
"bottom-12",
"backdrop-blur-background rounded-full border px-3.5 py-2",
"border-border/40 bg-material-ultra-thin/70 shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]",
"border-border/40 bg-material-ultra-thick shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]",
"hover:bg-material-thin/70 hover:border-border/60 active:scale-[0.98]",
)}
>

View File

@ -0,0 +1,40 @@
/**
* Constants for entry navigation hints behavior
*/
export const NAVIGATION_HINTS_CONSTANTS = {
/** Default scroll threshold to trigger scroll hint (px) */
DEFAULT_SCROLL_THRESHOLD: 100,
/** Delay before showing first entry hint (ms) */
FIRST_HINT_DELAY: 500,
/** Duration to show hints before auto-hiding (ms) */
HINT_DISPLAY_DURATION: 3000,
/** Distance from bottom to trigger bottom hint (px) */
BOTTOM_THRESHOLD: 50,
/** Distance from bottom to hide bottom hint when scrolling up (px) */
BOTTOM_HIDE_THRESHOLD: 100,
/** Throttle interval for scroll handler (ms) */
SCROLL_THROTTLE_INTERVAL: 100,
} as const
/**
* Text constants for navigation hints
*/
export const NAVIGATION_HINTS_TEXT = {
SCROLL_UP_EXIT: "Scroll up or click left-top back button to exit",
ESC_EXIT: "Press ESC or click left-top back button to exit",
} as const
/**
* Icon constants for navigation hints
*/
export const NAVIGATION_HINTS_ICONS = {
ARROW_UP: "i-mgc-up-cute-re",
ARROW_LEFT_UP: "i-mingcute-arrow-left-up-line",
ARROW_TO_DOWN: "i-mingcute-arrow-to-down-line",
CLOSE: "i-mgc-close-cute-re",
} as const

View File

@ -1,14 +1,16 @@
import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
import { throttle } from "es-toolkit"
import { useEffect, useRef, useState } from "react"
import { startTransition, useEffect, useRef, useState } from "react"
import { useEventCallback } from "usehooks-ts"
import { NAVIGATION_HINTS_CONSTANTS } from "../constants/navigation-hints"
interface UseEntryNavigationHintsOptions {
/** Whether hints are enabled */
enabled: boolean
/** Entry ID to track changes */
entryId?: string
/** Scroll threshold to show hint (default: 100px) */
/** Scroll threshold to show hint */
scrollThreshold?: number
}
@ -28,7 +30,7 @@ interface UseEntryNavigationHintsReturn {
export const useEntryNavigationHints = ({
enabled,
entryId,
scrollThreshold = 100,
scrollThreshold = NAVIGATION_HINTS_CONSTANTS.DEFAULT_SCROLL_THRESHOLD,
}: UseEntryNavigationHintsOptions): UseEntryNavigationHintsReturn => {
const $scrollElement = useScrollViewElement()
@ -45,6 +47,8 @@ export const useEntryNavigationHints = ({
const firstHintTimerRef = useRef<ReturnType<typeof setTimeout>>(void 0)
const scrollHintTimerRef = useRef<ReturnType<typeof setTimeout>>(void 0)
const bottomHintTimerRef = useRef<ReturnType<typeof setTimeout>>(void 0)
const lastScrollTopRef = useRef(0)
const scrollDirectionRef = useRef<"up" | "down" | "none">("none")
// Reset hints when entry changes
useEffect(() => {
@ -53,30 +57,38 @@ export const useEntryNavigationHints = ({
hasShownFirstHintRef.current = false
hasShownScrollHintRef.current = false
hasShownBottomHintRef.current = false
lastScrollTopRef.current = 0
scrollDirectionRef.current = "none"
// Clear existing timers
if (firstHintTimerRef.current) clearTimeout(firstHintTimerRef.current)
if (scrollHintTimerRef.current) clearTimeout(scrollHintTimerRef.current)
if (bottomHintTimerRef.current) clearTimeout(bottomHintTimerRef.current)
// Reset all hint states
setShowFirstEntryHint(false)
setShowScrollHint(false)
setShowBottomHint(false)
// Reset all hint states with low priority
startTransition(() => {
setShowFirstEntryHint(false)
setShowScrollHint(false)
setShowBottomHint(false)
})
if (enabled) {
// Show first entry hint after a brief delay
firstHintTimerRef.current = setTimeout(() => {
if (!hasShownFirstHintRef.current) {
setShowFirstEntryHint(true)
startTransition(() => {
setShowFirstEntryHint(true)
})
hasShownFirstHintRef.current = true
// Hide after 3 seconds
// Hide after configured duration
firstHintTimerRef.current = setTimeout(() => {
setShowFirstEntryHint(false)
}, 3000)
startTransition(() => {
setShowFirstEntryHint(false)
})
}, NAVIGATION_HINTS_CONSTANTS.HINT_DISPLAY_DURATION)
}
}, 500) // Small delay to allow content to load
}, NAVIGATION_HINTS_CONSTANTS.FIRST_HINT_DELAY) // Small delay to allow content to load
}
}
}, [entryId, enabled])
@ -91,35 +103,74 @@ export const useEntryNavigationHints = ({
const { clientHeight } = $scrollElement
const scrollBottom = scrollHeight - clientHeight - scrollTop
// Check if scrolled past threshold
if (scrollTop > scrollThreshold && !hasShownScrollHintRef.current) {
// Detect scroll direction
const lastScrollTop = lastScrollTopRef.current
if (scrollTop > lastScrollTop) {
scrollDirectionRef.current = "down"
} else if (scrollTop < lastScrollTop) {
scrollDirectionRef.current = "up"
}
lastScrollTopRef.current = scrollTop
// Check if scrolled past threshold and scrolling up
if (
scrollTop > scrollThreshold &&
!hasShownScrollHintRef.current &&
scrollDirectionRef.current === "up"
) {
hasShownScrollHintRef.current = true
setShowScrollHint(true)
startTransition(() => {
setShowScrollHint(true)
})
// Clear previous timer
if (scrollHintTimerRef.current) clearTimeout(scrollHintTimerRef.current)
// Hide after 3 seconds
// Hide after configured duration
scrollHintTimerRef.current = setTimeout(() => {
setShowScrollHint(false)
}, 3000)
startTransition(() => {
setShowScrollHint(false)
})
}, NAVIGATION_HINTS_CONSTANTS.HINT_DISPLAY_DURATION)
}
// Check if at bottom (within 50px threshold)
if (scrollBottom <= 50 && !hasShownBottomHintRef.current) {
// Check if at bottom (within configured threshold)
if (
scrollBottom <= NAVIGATION_HINTS_CONSTANTS.BOTTOM_THRESHOLD &&
!hasShownBottomHintRef.current
) {
hasShownBottomHintRef.current = true
setShowBottomHint(true)
startTransition(() => {
setShowBottomHint(true)
})
// Clear previous timer
if (bottomHintTimerRef.current) clearTimeout(bottomHintTimerRef.current)
// Hide after 3 seconds
// Hide after configured duration
bottomHintTimerRef.current = setTimeout(() => {
setShowBottomHint(false)
startTransition(() => {
setShowBottomHint(false)
})
hasShownBottomHintRef.current = false
}, 3000)
}, NAVIGATION_HINTS_CONSTANTS.HINT_DISPLAY_DURATION)
}
}, 100),
// Hide bottom hint if user scrolls up from bottom
if (
scrollBottom > NAVIGATION_HINTS_CONSTANTS.BOTTOM_HIDE_THRESHOLD &&
hasShownBottomHintRef.current &&
scrollDirectionRef.current === "up"
) {
// Clear timer if exists
if (bottomHintTimerRef.current) clearTimeout(bottomHintTimerRef.current)
startTransition(() => {
setShowBottomHint(false)
})
hasShownBottomHintRef.current = false
}
}, NAVIGATION_HINTS_CONSTANTS.SCROLL_THROTTLE_INTERVAL),
)
// Attach scroll listener