Skip to content

Commit 7d68393

Browse files
committed
feat: enhance HoverCard and LineToc components with pinning functionality and improved hover behavior
1 parent d8f81f8 commit 7d68393

File tree

2 files changed

+80
-38
lines changed

2 files changed

+80
-38
lines changed
Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { Pin, PinOff } from 'lucide-preact'
2+
import { Button } from '@/components/ui/button'
13
import { cn } from '@/lib/utils'
24
import type { TocDataItem } from './toc-item'
35

46
interface Props {
5-
show: boolean
7+
open: boolean
8+
pinned: boolean
69
toc: TocDataItem[]
710
rootDepth: number
811
currentScrollRange: [number, number]
@@ -12,57 +15,71 @@ interface Props {
1215
anchorId: string,
1316
) => void
1417
onMouseLeave: () => void
18+
onPinToggle: () => void
1519
}
1620

1721
export function HoverCard({
18-
show,
22+
open,
23+
pinned,
1924
toc,
2025
rootDepth,
2126
currentScrollRange,
2227
onScrollToButtonClick,
2328
onMouseLeave,
29+
onPinToggle,
2430
}: Props) {
2531
return (
26-
<ul
32+
<div
2733
className={cn(
28-
'fixed top-24 right-0 z-1000 -mt-1 rounded-2xl border',
29-
'px-4 py-2 text-xs',
30-
'bg-background shadow-context-menu backdrop-blur-background',
31-
'scrollbar-none max-h-[calc(100svh-4rem)] max-w-xs overflow-auto',
32-
'transition-all duration-200 ease-in-out',
33-
show
34+
'fixed top-24 right-0 z-1000 -mt-1 flex max-h-[calc(100svh-4rem)] max-w-72 flex-col text-xs',
35+
'bg-background',
36+
'scrollbar-none overflow-hidden',
37+
'transition-transform duration-200 ease-in-out',
38+
open
3439
? 'pointer-events-auto translate-x-0 opacity-100'
3540
: 'pointer-events-none translate-x-2 opacity-0',
3641
)}
3742
onMouseLeave={onMouseLeave}
3843
>
39-
{toc.map((heading, index) => (
40-
<li
41-
key={heading.anchorId}
42-
className='flex w-full items-center'
43-
style={{
44-
paddingLeft: `${(heading.depth - rootDepth) * 12}px`,
45-
}}
44+
<div className='flex justify-end p-1'>
45+
<Button
46+
variant='ghost'
47+
size='icon-sm'
48+
onClick={onPinToggle}
49+
aria-label={pinned ? '取消钉住' : '钉住'}
4650
>
47-
<button
48-
className={cn(
49-
'group flex w-full justify-between py-1 text-muted-foreground',
50-
index === currentScrollRange[0] ? 'text-foreground' : '',
51-
)}
52-
type='button'
53-
onClick={() => {
54-
onScrollToButtonClick(index, heading.$heading, heading.anchorId)
51+
{pinned ? <PinOff /> : <Pin />}
52+
</Button>
53+
</div>
54+
<ul className='scrollbar-none overflow-auto rounded-2xl border px-4 py-2'>
55+
{toc.map((heading, index) => (
56+
<li
57+
key={heading.anchorId}
58+
className='flex w-full items-center'
59+
style={{
60+
paddingLeft: `${(heading.depth - rootDepth) * 12}px`,
5561
}}
5662
>
57-
<span className='max-w-prose select-none truncate duration-200 group-hover:text-foreground/80'>
58-
{heading.title}
59-
</span>
60-
<span className='ml-4 select-none text-[8px] opacity-50'>
61-
H{heading.depth}
62-
</span>
63-
</button>
64-
</li>
65-
))}
66-
</ul>
63+
<button
64+
className={cn(
65+
'group flex w-full justify-between py-1 text-muted-foreground',
66+
index === currentScrollRange[0] ? 'text-foreground' : '',
67+
)}
68+
type='button'
69+
onClick={() => {
70+
onScrollToButtonClick(index, heading.$heading, heading.anchorId)
71+
}}
72+
>
73+
<span className='max-w-prose select-none truncate duration-200 group-hover:text-foreground/80'>
74+
{heading.title}
75+
</span>
76+
<span className='ml-4 select-none text-[8px] opacity-50'>
77+
H{heading.depth}
78+
</span>
79+
</button>
80+
</li>
81+
))}
82+
</ul>
83+
</div>
6784
)
6885
}

themes/shadcn/src/plugins/app/components/post-toc/line-toc.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useState } from 'preact/hooks'
1+
import { useRef, useState } from 'preact/hooks'
2+
import { useLocalStorage } from 'tona-hooks'
23
import { cn } from '@/lib/utils'
34
import { HoverCard } from './hover-card'
45
import { MemoedItem } from './memoed-toc-item'
@@ -20,6 +21,27 @@ export const LineToc = ({
2021
handleScrollTo,
2122
}: Props) => {
2223
const [hoverShow, setHoverShow] = useState(false)
24+
const [pinned, setPinned] = useLocalStorage<boolean>(
25+
'_tona_theme-shadcn-post-toc-pinned',
26+
false,
27+
)
28+
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
29+
const shouldHoverCardOpen = pinned || hoverShow
30+
31+
const handleMouseEnter = () => {
32+
hoverTimerRef.current = setTimeout(() => setHoverShow(true), 800)
33+
}
34+
35+
const handleMouseLeave = () => {
36+
if (hoverTimerRef.current) {
37+
clearTimeout(hoverTimerRef.current)
38+
hoverTimerRef.current = null
39+
}
40+
}
41+
42+
const handlePinToggle = () => {
43+
setPinned(!pinned)
44+
}
2345

2446
return (
2547
<div className='scrollbar-none flex grow flex-col scroll-smooth px-2'>
@@ -28,7 +50,8 @@ export const LineToc = ({
2850
'group scrollbar-none overflow-auto opacity-60 duration-200 group-hover:opacity-100',
2951
className,
3052
)}
31-
onMouseEnter={() => setHoverShow(true)}
53+
onMouseEnter={handleMouseEnter}
54+
onMouseLeave={handleMouseLeave}
3255
>
3356
{toc.map((heading, index) => (
3457
<MemoedItem
@@ -42,12 +65,14 @@ export const LineToc = ({
4265
))}
4366
</div>
4467
<HoverCard
45-
show={hoverShow}
68+
open={shouldHoverCardOpen}
69+
pinned={!!pinned}
4670
toc={toc}
4771
rootDepth={rootDepth}
4872
currentScrollRange={currentScrollRange}
4973
onScrollToButtonClick={handleScrollTo}
50-
onMouseLeave={() => setHoverShow(false)}
74+
onMouseLeave={() => !pinned && setHoverShow(false)}
75+
onPinToggle={handlePinToggle}
5176
/>
5277
</div>
5378
)

0 commit comments

Comments
 (0)