@@ -6,14 +6,26 @@ import { useParams } from "next/navigation";
66
77import { cn } from "@/lib/utils" ;
88
9- import { ArticleWaybackModal } from "./article- wayback-modal" ;
9+ import { ArticleWaybackModal } from "./wayback-modal" ;
1010
1111type TocItem = {
1212 id : string ;
1313 text : string ;
1414 level : number ;
1515} ;
1616
17+ const DESKTOP_HEADING_OFFSET = 96 ;
18+ const MOBILE_HEADING_OFFSET = 72 ;
19+
20+ function getHeadingViewportOffset ( ) {
21+ if ( typeof window === "undefined" ) {
22+ return MOBILE_HEADING_OFFSET ;
23+ }
24+ return window . matchMedia ( "(min-width: 1024px)" ) . matches
25+ ? DESKTOP_HEADING_OFFSET
26+ : MOBILE_HEADING_OFFSET ;
27+ }
28+
1729export default function Layout ( {
1830 titleRow,
1931 metaRow,
@@ -110,13 +122,37 @@ export default function Layout({
110122 const target = document . getElementById ( headingId ) ;
111123 if ( ! target ) return ;
112124
113- const prefersDesktop = window . matchMedia ( "(min-width: 1024px)" ) . matches ;
114- const offset = prefersDesktop ? 96 : 72 ;
125+ const offset = getHeadingViewportOffset ( ) ;
115126 const top = target . getBoundingClientRect ( ) . top + window . scrollY - offset ;
116127 window . scrollTo ( { top, behavior : "smooth" } ) ;
117128 setIsMobileTocOpen ( false ) ;
118129 } , [ ] ) ;
119130
131+ React . useEffect ( ( ) => {
132+ if ( typeof window === "undefined" ) return ;
133+ const container = contentRef . current ;
134+ if ( ! container ) return ;
135+
136+ const handleClick = ( event : MouseEvent ) => {
137+ const anchor = ( event . target as HTMLElement | null ) ?. closest (
138+ ".markdown-heading-anchor" ,
139+ ) as HTMLAnchorElement | null ;
140+ if ( ! anchor ) return ;
141+ const href = anchor . getAttribute ( "href" ) ?? "" ;
142+ if ( ! href . startsWith ( "#" ) ) return ;
143+ const headingId = href . slice ( 1 ) ;
144+ if ( ! headingId ) return ;
145+ event . preventDefault ( ) ;
146+ scrollToHeading ( headingId ) ;
147+ const url = new URL ( window . location . href ) ;
148+ url . hash = headingId ;
149+ window . history . replaceState ( null , "" , url ) ;
150+ } ;
151+
152+ container . addEventListener ( "click" , handleClick ) ;
153+ return ( ) => container . removeEventListener ( "click" , handleClick ) ;
154+ } , [ scrollToHeading ] ) ;
155+
120156 React . useEffect ( ( ) => {
121157 if ( typeof window === "undefined" ) return ;
122158
@@ -254,6 +290,7 @@ export default function Layout({
254290
255291 const updateActiveHeading = ( ) => {
256292 frameId = 0 ;
293+ const offset = getHeadingViewportOffset ( ) ;
257294 if ( ! headingElements . length ) {
258295 headingElements = resolveHeadingElements ( ) ;
259296 if ( ! headingElements . length ) {
@@ -264,7 +301,8 @@ export default function Layout({
264301
265302 let candidateId : string | null = headingElements [ 0 ] ?. id ?? null ;
266303 for ( const element of headingElements ) {
267- if ( element . getBoundingClientRect ( ) . top <= VIEWPORT_TOP_EPSILON ) {
304+ const topDistance = element . getBoundingClientRect ( ) . top - offset ;
305+ if ( topDistance <= VIEWPORT_TOP_EPSILON ) {
268306 candidateId = element . id ;
269307 } else {
270308 break ;
@@ -429,16 +467,11 @@ export default function Layout({
429467 ) }
430468 >
431469 < div className = "article-floating-toc-hitbox" >
432- < button
433- type = "button"
434- aria-label = "查看文章目录"
435- className = "article-floating-toc-button"
436- >
437- < ListTree className = "h-5 w-5" />
438- </ button >
470+ < span className = "article-floating-toc-button" >
471+ < ListTree className = "size-4" />
472+ </ span >
439473 < div className = "article-floating-toc-panel" >
440474 < div className = "article-toc-card" >
441- < div className = "article-toc-card-header" > 内容目录</ div >
442475 < TocNavigation
443476 items = { tocItems }
444477 activeId = { activeHeadingId }
@@ -456,9 +489,11 @@ export default function Layout({
456489 "article-grid grid gap-8" ,
457490 "lg:grid-cols-[minmax(0,8fr)_minmax(0,3.2fr)]" ,
458491 "xl:grid-cols-[minmax(0,8fr)_minmax(0,2.7fr)]" ,
459- "2xl:grid-cols-[minmax(0,3fr)_minmax(0,8fr)_minmax(0,3fr)]" ,
492+ hasToc
493+ ? "2xl:grid-cols-[minmax(0,2fr)_minmax(0,8fr)_minmax(0,3fr)]"
494+ : "2xl:grid-cols-[minmax(0,3fr)_minmax(0,8fr)_minmax(0,3fr)]" ,
460495 hasToc &&
461- "3xl:grid-cols-[minmax(0,2.5fr)_minmax(0,2.4fr )_minmax(0,9fr)_minmax(0,3fr)]" ,
496+ "3xl:grid-cols-[minmax(0,2.5fr)_minmax(0,2fr )_minmax(0,9fr)_minmax(0,3fr)]" ,
462497 ) }
463498 >
464499 < aside className = "hidden 2xl:order-1 2xl:flex 2xl:flex-col 2xl:gap-4" >
@@ -470,7 +505,6 @@ export default function Layout({
470505 className = "article-toc-card sticky"
471506 style = { tocStickyStyle }
472507 >
473- < div className = "article-toc-card-header" > 内容目录</ div >
474508 < TocNavigation
475509 items = { tocItems }
476510 activeId = { activeHeadingId }
@@ -496,7 +530,6 @@ export default function Layout({
496530 className = "article-toc-card sticky"
497531 style = { tocStickyStyle }
498532 >
499- < div className = "article-toc-card-header" > 内容目录</ div >
500533 < TocNavigation
501534 items = { tocItems }
502535 activeId = { activeHeadingId }
@@ -563,10 +596,11 @@ export default function Layout({
563596 isMetaPinned ? "opacity-100" : "opacity-0" ,
564597 ) }
565598 >
566- < div className = "pointer-events-none absolute inset-0 rounded-2xl shadow-sm " />
567- < div className = "h-full overflow-hidden rounded-2xl border border-border bg-background/95 " >
568- < div ref = { floatingMetaRef } className = "px-5 py-4 " >
599+ < div className = "pointer-events-none absolute inset-0" />
600+ < div className = "h-full overflow-hidden" >
601+ < div ref = { floatingMetaRef } className = "pb-2.5 " >
569602 { metaCard }
603+ < hr className = "mt-7" />
570604 </ div >
571605 </ div >
572606 </ div >
@@ -597,9 +631,6 @@ export default function Layout({
597631 >
598632 < div className = "mx-auto w-full max-w-sm rounded-3xl border bg-background p-5 shadow-xl" >
599633 < div className = "mb-4 flex items-center justify-between" >
600- < div className = "text-sm font-semibold tracking-wide text-muted-foreground uppercase" >
601- 内容目录
602- </ div >
603634 < button
604635 type = "button"
605636 className = "rounded-full border p-1 text-muted-foreground transition hover:text-foreground"
@@ -608,7 +639,7 @@ export default function Layout({
608639 < X className = "h-4 w-4" />
609640 </ button >
610641 </ div >
611- < div className = "max-h-[60vh ] overflow-y-auto pr-1" >
642+ < div className = "max-h-[80dvh ] overflow-y-auto pr-1" >
612643 < TocNavigation
613644 items = { tocItems }
614645 activeId = { activeHeadingId }
@@ -649,7 +680,9 @@ function TocNavigation({ items, activeId, onNavigate }: TocNavigationProps) {
649680 ) }
650681 onClick = { ( ) => onNavigate ( item . id ) }
651682 >
652- < span className = "line-clamp-2 text-left" > { item . text } </ span >
683+ < span className = "line-clamp-1 truncate text-left" >
684+ { item . text }
685+ </ span >
653686 </ button >
654687 </ li >
655688 ) ;
0 commit comments