Skip to content

Commit 0c3a6eb

Browse files
committed
fix: toc
Signed-off-by: Innei <[email protected]>
1 parent cc0cc47 commit 0c3a6eb

File tree

5 files changed

+157
-73
lines changed

5 files changed

+157
-73
lines changed

global.d.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ declare global {
1111
} & Props
1212
>
1313

14-
export type Component<P = {}> = FC<
15-
{
16-
className?: string
17-
} & P &
18-
PropsWithChildren
19-
>
14+
export type Component<P = {}> = FC<ComponentType & P>
15+
16+
export type ComponentType = {
17+
className?: string
18+
} & PropsWithChildren
2019

2120
// TODO should remove in next TypeScript version
2221
interface Document {

src/components/widgets/shared/ArticleRightAside.tsx

+63-9
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,73 @@
1-
import React from 'react'
1+
'use client'
2+
3+
import React, { useEffect, useRef, useState } from 'react'
4+
import clsx from 'clsx'
5+
import type { TocAsideRef } from '../toc'
6+
7+
import { useViewport } from '~/atoms'
28

39
import { TocAside } from '../toc'
410
import { ReadIndicator } from './ReadIndicator'
511

612
export const ArticleRightAside: Component = ({ children }) => {
13+
const asideRef = useRef<TocAsideRef>(null)
14+
const [isScrollToBottom, setIsScrollToBottom] = useState(false)
15+
const [isScrollToTop, setIsScrollToTop] = useState(false)
16+
const [canScroll, setCanScroll] = useState(false)
17+
const h = useViewport((v) => v.h)
18+
useEffect(() => {
19+
const $ = asideRef.current?.getContainer()
20+
if (!$) return
21+
22+
// if $ can not scroll, return null
23+
if ($.scrollHeight <= $.clientHeight + 2) return
24+
25+
setCanScroll(true)
26+
27+
const handler = () => {
28+
// to bottom
29+
if ($.scrollTop + $.clientHeight + 20 > $.scrollHeight) {
30+
setIsScrollToBottom(true)
31+
setIsScrollToTop(false)
32+
}
33+
34+
// if scroll to top,
35+
// set isScrollToTop to true
36+
else if ($.scrollTop === 0) {
37+
setIsScrollToTop(true)
38+
setIsScrollToBottom(false)
39+
} else {
40+
setIsScrollToBottom(false)
41+
setIsScrollToTop(false)
42+
}
43+
}
44+
$.addEventListener('scroll', handler)
45+
46+
handler()
47+
48+
return () => {
49+
$.removeEventListener('scroll', handler)
50+
}
51+
}, [h])
52+
753
return (
8-
<aside className="sticky top-2 h-[calc(100vh-6rem-4.5rem-150px)]">
9-
<TocAside
10-
as="div"
11-
className="top-[120px] ml-4"
12-
treeClassName="absolute h-full min-h-[120px]"
13-
accessory={ReadIndicator}
14-
/>
54+
<aside className="sticky top-[120px] mt-[120px] h-[calc(100vh-6rem-4.5rem-150px-120px)]">
55+
<div className="relative h-full">
56+
<TocAside
57+
as="div"
58+
className="static ml-4"
59+
treeClassName={clsx(
60+
'absolute h-full min-h-[120px] overflow-auto',
61+
isScrollToBottom && 'mask-t',
62+
isScrollToTop && 'mask-b',
63+
canScroll && !isScrollToBottom && !isScrollToTop && 'mask-both',
64+
)}
65+
accessory={ReadIndicator}
66+
ref={asideRef}
67+
/>
68+
</div>
1569
{React.cloneElement(children as any, {
16-
className: 'ml-4 translate-y-full',
70+
className: 'ml-4 translate-y-[calc(100%+24px)]',
1771
})}
1872
</aside>
1973
)
+64-48
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
'use client'
22

3-
import React, { useEffect, useMemo, useRef } from 'react'
3+
import React, {
4+
forwardRef,
5+
useEffect,
6+
useImperativeHandle,
7+
useMemo,
8+
useRef,
9+
} from 'react'
410

511
import { throttle } from '~/lib/_'
612
import { clsxm } from '~/lib/helper'
@@ -17,56 +23,66 @@ export interface TocSharedProps {
1723

1824
as?: React.ElementType
1925
}
20-
export const TocAside: Component<TocAsideProps & TocSharedProps> = ({
21-
className,
22-
children,
23-
treeClassName,
24-
accessory,
25-
as: As = 'aside',
26-
}) => {
27-
const containerRef = useRef<HTMLUListElement>(null)
28-
const $article = useWrappedElement()
26+
export interface TocAsideRef {
27+
getContainer: () => HTMLUListElement | null
28+
}
29+
export const TocAside = forwardRef<
30+
TocAsideRef,
31+
TocAsideProps & TocSharedProps & ComponentType
32+
>(
33+
(
34+
{ className, children, treeClassName, accessory, as: As = 'aside' },
35+
ref,
36+
) => {
37+
const containerRef = useRef<HTMLUListElement>(null)
38+
const $article = useWrappedElement()
39+
40+
useImperativeHandle(ref, () => ({
41+
getContainer: () => containerRef.current,
42+
}))
2943

30-
if (typeof $article === 'undefined') {
31-
throw new Error('<Toc /> must be used in <WrappedElementProvider />')
32-
}
33-
const $headings = useMemo(() => {
34-
if (!$article) {
35-
return []
44+
if (typeof $article === 'undefined') {
45+
throw new Error('<Toc /> must be used in <WrappedElementProvider />')
3646
}
47+
const $headings = useMemo(() => {
48+
if (!$article) {
49+
return []
50+
}
3751

38-
return [
39-
...$article.querySelectorAll('h1,h2,h3,h4,h5,h6'),
40-
] as HTMLHeadingElement[]
41-
}, [$article])
52+
return [
53+
...$article.querySelectorAll('h1,h2,h3,h4,h5,h6'),
54+
] as HTMLHeadingElement[]
55+
}, [$article])
4256

43-
useEffect(() => {
44-
const setMaxWidth = throttle(() => {
45-
if (containerRef.current) {
46-
containerRef.current.style.maxWidth = `${
47-
window.innerWidth -
48-
containerRef.current.getBoundingClientRect().x -
49-
30
50-
}px`
51-
}
52-
}, 14)
53-
setMaxWidth()
57+
useEffect(() => {
58+
const setMaxWidth = throttle(() => {
59+
if (containerRef.current) {
60+
containerRef.current.style.maxWidth = `${
61+
window.innerWidth -
62+
containerRef.current.getBoundingClientRect().x -
63+
30
64+
}px`
65+
}
66+
}, 14)
67+
setMaxWidth()
5468

55-
window.addEventListener('resize', setMaxWidth)
56-
return () => {
57-
window.removeEventListener('resize', setMaxWidth)
58-
}
59-
}, [])
69+
window.addEventListener('resize', setMaxWidth)
70+
return () => {
71+
window.removeEventListener('resize', setMaxWidth)
72+
}
73+
}, [])
6074

61-
return (
62-
<As className={clsxm('st-toc z-[3]', 'relative font-sans', className)}>
63-
<TocTree
64-
$headings={$headings}
65-
containerRef={containerRef}
66-
className={clsxm('absolute max-h-[75vh]', treeClassName)}
67-
accessory={accessory}
68-
/>
69-
{children}
70-
</As>
71-
)
72-
}
75+
return (
76+
<As className={clsxm('st-toc z-[3]', 'relative font-sans', className)}>
77+
<TocTree
78+
$headings={$headings}
79+
containerRef={containerRef}
80+
className={clsxm('absolute max-h-[75vh]', treeClassName)}
81+
accessory={accessory}
82+
/>
83+
{children}
84+
</As>
85+
)
86+
},
87+
)
88+
TocAside.displayName = 'TocAside'

src/components/widgets/toc/TocItem.tsx

-9
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,6 @@ export const TocItem: FC<{
3838

3939
const $ref = useRef<HTMLAnchorElement>(null)
4040

41-
// useEffect(() => {
42-
// if (!active) {
43-
// return
44-
// }
45-
// if (!getIsInteractive()) return
46-
// const state = history.state
47-
// history.replaceState(state, '', `#${anchorId}`)
48-
// }, [active, anchorId])
49-
5041
useEffect(() => {
5142
if (active) {
5243
$ref.current?.scrollIntoView({ behavior: 'smooth' })

src/styles/theme.css

+25-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
background-size: 0% 1.5px;
1010
background-repeat: no-repeat;
1111
/* NOTE: this won't work with background images */
12-
text-shadow: 0.05em 0 theme(colors.base-100),
12+
text-shadow:
13+
0.05em 0 theme(colors.base-100),
1314
-0.05em 0 theme(colors.base-100);
1415
transition: all 500ms ease;
1516

@@ -114,3 +115,26 @@
114115
}
115116
}
116117
}
118+
119+
.mask-both {
120+
mask-image: linear-gradient(
121+
rgba(255, 255, 255, 0) 0%,
122+
rgb(255, 255, 255) 10%,
123+
rgb(255, 255, 255) 90%,
124+
rgba(255, 255, 255, 0) 100%
125+
);
126+
}
127+
128+
.mask-b {
129+
mask-image: linear-gradient(
130+
rgb(255, 255, 255) 90%,
131+
rgba(255, 255, 255, 0) 100%
132+
);
133+
}
134+
135+
.mask-t {
136+
mask-image: linear-gradient(
137+
rgba(255, 255, 255, 0) 0%,
138+
rgb(255, 255, 255) 10%
139+
);
140+
}

0 commit comments

Comments
 (0)