Skip to content

Commit 06ef54d

Browse files
committed
fix(toc): stabilize scroll spy and TOC list viewport
Track only heading nodes for active-section state, keep the document title outside the list scroller so alignment math matches the visible list, and tighten focus rail behavior for parent rows. Made-with: Cursor
1 parent fe2b1ae commit 06ef54d

8 files changed

Lines changed: 197 additions & 112 deletions

File tree

packages/webapp/src/components/pages/document/components/Toc.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import TableOfContentsLoader from '@components/skeleton/TableOfContentsLoader'
2-
import { TocDesktop } from '@components/toc'
2+
import { TocDesktop, TocHeader } from '@components/toc'
33
import { ScrollArea } from '@components/ui/ScrollArea'
44
import { useStore } from '@stores'
5+
import { twMerge } from 'tailwind-merge'
56

67
const TOC = ({ className = '' }: { className?: string }) => {
78
const loading = useStore((state) => state.settings.editor.loading)
@@ -18,12 +19,12 @@ const TOC = ({ className = '' }: { className?: string }) => {
1819
}
1920

2021
return (
21-
<ScrollArea
22-
className={`${className} tiptap__toc h-full w-full !pt-0`}
23-
orientation="vertical"
24-
scrollbarSize="thin">
25-
<TocDesktop className="hover:overscroll-contain" />
26-
</ScrollArea>
22+
<div className={twMerge('tiptap__toc flex h-full min-h-0 w-full flex-col !pt-0', className)}>
23+
<TocHeader variant="desktop" />
24+
<ScrollArea className="min-h-0 flex-1 !pt-0" scrollbarSize="thin" hideScrollbar>
25+
<TocDesktop className="hover:overscroll-contain" />
26+
</ScrollArea>
27+
</div>
2728
)
2829
}
2930

packages/webapp/src/components/pages/document/components/TocModal.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import AppendHeadingButton from '@components/pages/document/components/AppendHeadingButton'
2-
import { TocMobile } from '@components/toc'
2+
import { TocHeader, TocMobile } from '@components/toc'
33
import Button from '@components/ui/Button'
44
import { useModal } from '@components/ui/ModalDrawer'
5+
import { ScrollArea } from '@components/ui/ScrollArea'
56
import { DocsPlusIcon } from '@icons'
67
import { Icons } from '@icons'
78
import { useSheetStore, useStore } from '@stores'
@@ -77,9 +78,12 @@ const TocModal = ({ filterModalRef: _filterModalRef }: TocModalProps) => {
7778
</div>
7879
</header>
7980

80-
{/* Content — scrollable TOC list */}
81-
<div className="bg-base-200 flex-1 overflow-y-auto">
82-
<TocMobile className="tiptap__toc w-full pb-4 !pl-2" hideAppendButton />
81+
{/* Content — doc title outside scroll so list viewport matches scrollIntoView */}
82+
<div className="bg-base-200 flex min-h-0 flex-1 flex-col overflow-hidden">
83+
<TocHeader variant="mobile" />
84+
<ScrollArea className="min-h-0 flex-1" scrollbarSize="thin" hideScrollbar>
85+
<TocMobile className="tiptap__toc w-full pb-4 !pl-2" hideAppendButton />
86+
</ScrollArea>
8387
</div>
8488

8589
{/* Sticky footer — add heading button */}

packages/webapp/src/components/toc/TocDesktop.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import React, { useCallback, useRef, useState } from 'react'
88
import { DropIndicatorPortal, pointerYCollision, tocDragModifier } from './dnd'
99
import { useToc, useTocAutoScroll, useTocDrag } from './hooks'
1010
import { TocContextMenu } from './TocContextMenu'
11-
import { TocHeader } from './TocHeader'
1211
import { TocItemDesktop } from './TocItemDesktop'
1312
import { buildNestedToc } from './utils'
1413

@@ -71,16 +70,11 @@ export function TocDesktop({ className = '' }: TocDesktopProps) {
7170
const nestedItems = buildNestedToc(items)
7271

7372
if (!items.length) {
74-
return (
75-
<div className={`${className}`}>
76-
<TocHeader variant="desktop" />
77-
</div>
78-
)
73+
return <div className={className} />
7974
}
8075

8176
return (
82-
<div className={`${className}`} ref={contextMenuRef}>
83-
<TocHeader variant="desktop" />
77+
<div className={className} ref={contextMenuRef}>
8478
<DndContext
8579
sensors={sensors}
8680
collisionDetection={pointerYCollision}

packages/webapp/src/components/toc/TocHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function TocHeader({ variant }: TocHeaderProps) {
4848

4949
if (variant === 'mobile') {
5050
return (
51-
<div className="border-base-300 bg-base-100 sticky top-0 isolate z-30 border-b">
51+
<div className="border-base-300 bg-base-100 isolate z-30 shrink-0 border-b">
5252
<div className="group relative flex items-center justify-between py-2">
5353
<span className="text-base-content text-lg font-bold">{docMetadata?.title}</span>
5454
<button
@@ -76,7 +76,7 @@ export function TocHeader({ variant }: TocHeaderProps) {
7676
}
7777

7878
return (
79-
<div className="border-base-300 bg-base-200 relative sticky top-0 isolate z-30 w-full border-b pt-2 pb-1">
79+
<div className="border-base-300 bg-base-200 relative isolate z-30 w-full shrink-0 border-b pt-2 pb-1">
8080
<div
8181
className={twMerge(
8282
`${TOC_CLASSES.headerRow} group hover:bg-base-300/50 flex cursor-pointer items-center justify-between gap-0.5 rounded-md p-1 pr-3 pl-2`,

packages/webapp/src/components/toc/TocMobile.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import AppendHeadingButton from '@components/pages/document/components/AppendHea
22
import React from 'react'
33

44
import { useToc, useTocAutoScroll } from './hooks'
5-
import { TocHeader } from './TocHeader'
65
import { TocItemMobile } from './TocItemMobile'
76
import { buildNestedToc } from './utils'
87

@@ -26,7 +25,6 @@ export function TocMobile({ className = '', hideAppendButton = false }: TocMobil
2625

2726
return (
2827
<div className={className}>
29-
<TocHeader variant="mobile" />
3028
<ul className="toc__list menu my-2 p-0">
3129
{nestedItems.map(({ item, nodes }) => (
3230
<TocItemMobile key={item.id} item={item} nestedNodes={nodes} onToggle={toggleSection} />

0 commit comments

Comments
 (0)