diff --git a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx
index d4a34dff2..bde9713db 100644
--- a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx
@@ -1,41 +1,140 @@
'use client';
+import React from 'react';
import type { ClientTOCPageGroup } from './encodeClientTableOfContents';
import { tcls } from '@/lib/tailwind';
+import { ToggleChevron } from '../primitives';
import { PagesList } from './PagesList';
import { TOCPageIcon } from './TOCPageIcon';
+import { ToCButtonItemStyles } from './styles';
export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boolean }) {
const { page, isFirst } = props;
+ const descendants = page.descendants ?? [];
+ const hasDescendants = descendants.length > 0;
+ const [isOpen, setIsOpen] = React.useState(true);
+ const { sentinelRef, isSticking } = useIsSticking();
+
+ const handleToggle = () => {
+ if (!hasDescendants) {
+ return;
+ }
+
+ setIsOpen((prev) => !prev);
+ };
return (
+
-
- {page.title}
+
- {page.descendants && page.descendants.length > 0 ? (
-
+ {hasDescendants ? (
+
) : null}
);
}
+
+/**
+ * Detect when a sticky element becomes "stuck" using an IntersectionObserver on a sentinel.
+ * Place the sentinel ref on a 0-height element right before the sticky element.
+ */
+function useIsSticking() {
+ const sentinelRef = React.useRef(null);
+ const [isSticking, setIsSticking] = React.useState(false);
+
+ React.useEffect(() => {
+ const sentinel = sentinelRef.current;
+ if (!sentinel) return;
+
+ // Find the closest scrollable ancestor to use as IntersectionObserver root
+ let scrollParent: Element | null = sentinel.parentElement;
+ while (scrollParent) {
+ const { overflowY } = getComputedStyle(scrollParent);
+ if (overflowY === 'auto' || overflowY === 'scroll') break;
+ scrollParent = scrollParent.parentElement;
+ }
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry) {
+ setIsSticking(!entry.isIntersecting);
+ }
+ },
+ { root: scrollParent }
+ );
+
+ observer.observe(sentinel);
+ return () => observer.disconnect();
+ }, []);
+
+ return { sentinelRef, isSticking };
+}
diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
index e1f013093..8e8db7f20 100644
--- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
+++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx
@@ -18,7 +18,7 @@ export function PageLinkItem(props: { page: ClientTOCPageLink }) {
diff --git a/packages/gitbook/src/components/TableOfContents/styles.ts b/packages/gitbook/src/components/TableOfContents/styles.ts
index e50bc9e11..c61ab9645 100644
--- a/packages/gitbook/src/components/TableOfContents/styles.ts
+++ b/packages/gitbook/src/components/TableOfContents/styles.ts
@@ -1,14 +1,26 @@
-export const ToggleableLinkItemStyles = [
- 'group/toclink toclink relative transition-colors',
+export const ToCItemBaseStyles = [
'flex flex-row justify-start items-center gap-3',
'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
- 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
- 'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12',
+ 'focus-visible:-outline-offset-2',
'before:contents[] before:-left-px before:absolute before:inset-y-0',
'sidebar-list-line:rounded-l-none! sidebar-list-line:before:w-px [&+div_a]:sidebar-list-default:rounded-l-none [&+div_a]:pl-5 [&+div_a]:sidebar-list-default:before:w-px',
];
-export const ToggleableLinkItemActiveStyles = [
+export const ToCLinkItemStyles = [
+ 'group/toclink toclink relative transition-colors',
+ ToCItemBaseStyles,
+ 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
+ 'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12',
+];
+
+export const ToCButtonItemStyles = [
+ 'relative transition-colors',
+ ToCItemBaseStyles,
+ 'text-balance font-normal text-sm text-tint-strong hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
+ 'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12',
+];
+
+export const ToCLinkItemActiveStyles = [
'font-semibold',
'sidebar-list-line:before:w-0.5',
diff --git a/packages/gitbook/src/components/primitives/StyleProvider.tsx b/packages/gitbook/src/components/primitives/StyleProvider.tsx
index 1ed8edbd8..fd9e57357 100644
--- a/packages/gitbook/src/components/primitives/StyleProvider.tsx
+++ b/packages/gitbook/src/components/primitives/StyleProvider.tsx
@@ -2,10 +2,7 @@
import type { ClassValue } from '@/lib/tailwind';
import { RecordCardLinkStyles, RecordCardStyles } from '../DocumentView/Table/styles';
-import {
- ToggleableLinkItemActiveStyles,
- ToggleableLinkItemStyles,
-} from '../TableOfContents/styles';
+import { ToCLinkItemActiveStyles, ToCLinkItemStyles } from '../TableOfContents/styles';
import { ButtonStyles, CardStyles, LinkStyles } from './styles';
const styles = {
@@ -14,8 +11,8 @@ const styles = {
ButtonStyles,
RecordCardStyles,
RecordCardLinkStyles,
- ToggleableLinkItemStyles,
- ToggleableLinkItemActiveStyles,
+ ToCLinkItemStyles,
+ ToCLinkItemActiveStyles,
};
export type DesignTokenName = keyof typeof styles;