diff --git a/.changeset/slimy-points-talk.md b/.changeset/slimy-points-talk.md new file mode 100644 index 0000000000..0c2363b3e1 --- /dev/null +++ b/.changeset/slimy-points-talk.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Add scrollcontainer component diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx index 6499b2a840..066625f423 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx @@ -5,10 +5,9 @@ import { motion } from 'framer-motion'; import React from 'react'; import { type ClassValue, tcls } from '@/lib/tailwind'; - -import { TOCScrollContainer, useScrollToActiveTOCItem } from '../TableOfContents/TOCScroller'; -import { useIsMounted, useToggleAnimation } from '../hooks'; +import { useToggleAnimation } from '../hooks'; import { Link } from '../primitives'; +import { ScrollContainer } from '../primitives/ScrollContainer'; import { SectionIcon } from './SectionIcon'; import type { ClientSiteSection, @@ -36,30 +35,34 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className className )} > - - {sectionsAndGroups.map((item) => { - if (item.object === 'site-section-group') { +
+ {sectionsAndGroups.map((item) => { + if (item.object === 'site-section-group') { + return ( + + ); + } + return ( - ); - } - - return ( - - ); - })} - + })} +
+ ) ); @@ -72,17 +75,11 @@ export function SiteSectionListItem(props: { }) { const { section, isActive, className, ...otherProps } = props; - const isMounted = useIsMounted(); - React.useEffect(() => {}, [isMounted]); // This updates the useScrollToActiveTOCItem hook once we're mounted, so we can actually scroll to the this item - - const anchorRef = React.createRef(); - useScrollToActiveTOCItem({ anchorRef, isActive }); - return ( -
@@ -123,7 +131,7 @@ export function SiteSectionTabs(props: { ); })} -
+ {children} diff --git a/packages/gitbook/src/components/primitives/ScrollContainer.tsx b/packages/gitbook/src/components/primitives/ScrollContainer.tsx new file mode 100644 index 0000000000..edbab415f5 --- /dev/null +++ b/packages/gitbook/src/components/primitives/ScrollContainer.tsx @@ -0,0 +1,177 @@ +'use client'; + +import { tString, useLanguage } from '@/intl/client'; +import { tcls } from '@/lib/tailwind'; +import * as React from 'react'; +import { Button } from './Button'; + +/** + * A container that encapsulates a scrollable area with usability features. + * - Faded edges when there is more content than the container can display. + * - Buttons to advance the scroll position. + * - Auto-scroll to the active item when it's initially active. + */ +export type ScrollContainerProps = { + children: React.ReactNode; + className?: string; + + /** The direction of the scroll container. */ + orientation: 'horizontal' | 'vertical'; + + /** The ID of the active item to scroll to. */ + activeId?: string; +} & React.HTMLAttributes; + +export function ScrollContainer(props: ScrollContainerProps) { + const { children, className, orientation, activeId, ...rest } = props; + + const containerRef = React.useRef(null); + + const [scrollPosition, setScrollPosition] = React.useState(0); + const [scrollSize, setScrollSize] = React.useState(0); + + const language = useLanguage(); + + React.useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + + // Update scroll position on scroll using requestAnimationFrame + const scrollListener: EventListener = () => { + requestAnimationFrame(() => { + setScrollPosition( + orientation === 'horizontal' ? container.scrollLeft : container.scrollTop + ); + }); + }; + container.addEventListener('scroll', scrollListener); + + // Update max scroll position using resize observer + const resizeObserver = new ResizeObserver((entries) => { + const containerEntry = entries.find((i) => i.target === containerRef.current); + if (containerEntry) { + setScrollSize( + orientation === 'horizontal' + ? containerEntry.target.scrollWidth - containerEntry.target.clientWidth - 1 + : containerEntry.target.scrollHeight - + containerEntry.target.clientHeight - + 1 + ); + } + }); + resizeObserver.observe(container); + + return () => { + container.removeEventListener('scroll', scrollListener); + resizeObserver.disconnect(); + }; + }, [orientation]); + + // Scroll to the active item + React.useEffect(() => { + const container = containerRef.current; + if (!container || !activeId) { + return; + } + const activeItem = container.querySelector(`#${CSS.escape(activeId)}`); + if (activeItem) { + activeItem.scrollIntoView({ + inline: 'center', + block: 'center', + }); + } + }, [activeId]); + + const scrollFurther = () => { + const container = containerRef.current; + if (!container) { + return; + } + container.scrollTo({ + top: orientation === 'vertical' ? scrollPosition + container.clientHeight : undefined, + left: orientation === 'horizontal' ? scrollPosition + container.clientWidth : undefined, + behavior: 'smooth', + }); + }; + + const scrollBack = () => { + const container = containerRef.current; + if (!container) { + return; + } + container.scrollTo({ + top: orientation === 'vertical' ? scrollPosition - container.clientHeight : undefined, + left: orientation === 'horizontal' ? scrollPosition - container.clientWidth : undefined, + behavior: 'smooth', + }); + }; + + return ( +
+ {/* Scrollable content */} +
0 + ? orientation === 'horizontal' + ? 'mask-l-from-[calc(100%-2rem)]' + : 'mask-t-from-[calc(100%-2rem)]' + : '', + scrollPosition < scrollSize + ? orientation === 'horizontal' + ? 'mask-r-from-[calc(100%-2rem)]' + : 'mask-b-from-[calc(100%-2rem)]' + : '' + )} + ref={containerRef} + > + {children} +
+ + {/* Scroll buttons back & forward */} +
+ ); +} diff --git a/packages/gitbook/src/intl/translations/de.ts b/packages/gitbook/src/intl/translations/de.ts index 7c5811ec1b..721637a490 100644 --- a/packages/gitbook/src/intl/translations/de.ts +++ b/packages/gitbook/src/intl/translations/de.ts @@ -120,4 +120,6 @@ export const de = { copy_mcp_url: 'MCP-Server-URL kopieren', press_to_confirm: 'Drücke ${1} zum Bestätigen', tool_call_skipped: 'Übersprungen "${1}"', + scroll_back: 'Zurück scrollen', + scroll_further: 'Weiter scrollen', }; diff --git a/packages/gitbook/src/intl/translations/en.ts b/packages/gitbook/src/intl/translations/en.ts index c06e5b92e1..346ff0f2cd 100644 --- a/packages/gitbook/src/intl/translations/en.ts +++ b/packages/gitbook/src/intl/translations/en.ts @@ -117,4 +117,6 @@ export const en = { copy_mcp_url: 'Copy the MCP Server URL', press_to_confirm: 'Press ${1} to confirm', tool_call_skipped: 'Skipped "${1}"', + scroll_back: 'Scroll back', + scroll_further: 'Scroll further', }; diff --git a/packages/gitbook/src/intl/translations/es.ts b/packages/gitbook/src/intl/translations/es.ts index 8f28f36656..606136f329 100644 --- a/packages/gitbook/src/intl/translations/es.ts +++ b/packages/gitbook/src/intl/translations/es.ts @@ -121,4 +121,6 @@ export const es: TranslationLanguage = { copy_mcp_url: 'Copiar URL del servidor MCP', press_to_confirm: 'Presiona ${1} para confirmar', tool_call_skipped: 'Omitido "${1}"', + scroll_back: 'Desplazar hacia atrás', + scroll_further: 'Desplazar más', }; diff --git a/packages/gitbook/src/intl/translations/fr.ts b/packages/gitbook/src/intl/translations/fr.ts index d99fddd7fe..7afa404488 100644 --- a/packages/gitbook/src/intl/translations/fr.ts +++ b/packages/gitbook/src/intl/translations/fr.ts @@ -116,4 +116,6 @@ export const fr = { copy_mcp_url: "Copier l'URL du serveur MCP", press_to_confirm: 'Appuyez sur ${1} pour confirmer', tool_call_skipped: 'Ignoré "${1}"', + scroll_back: "Défiler vers l'arrière", + scroll_further: 'Défiler plus loin', }; diff --git a/packages/gitbook/src/intl/translations/ja.ts b/packages/gitbook/src/intl/translations/ja.ts index f344796edf..15709f02c4 100644 --- a/packages/gitbook/src/intl/translations/ja.ts +++ b/packages/gitbook/src/intl/translations/ja.ts @@ -119,4 +119,6 @@ export const ja: TranslationLanguage = { copy_mcp_url: 'MCPサーバーのURLをコピー', press_to_confirm: '確認するには${1}を押してください', tool_call_skipped: '"${1}" をスキップしました', + scroll_back: '戻る', + scroll_further: '進む', }; diff --git a/packages/gitbook/src/intl/translations/nl.ts b/packages/gitbook/src/intl/translations/nl.ts index 87432b1249..042c5b9a37 100644 --- a/packages/gitbook/src/intl/translations/nl.ts +++ b/packages/gitbook/src/intl/translations/nl.ts @@ -119,4 +119,6 @@ export const nl: TranslationLanguage = { copy_mcp_url: 'Kopieer MCP-server URL', press_to_confirm: 'Druk op ${1} om te bevestigen', tool_call_skipped: '"${1}" overgeslagen', + scroll_back: 'Terug scrollen', + scroll_further: 'Verder scrollen', }; diff --git a/packages/gitbook/src/intl/translations/no.ts b/packages/gitbook/src/intl/translations/no.ts index 4d12613a04..8b1a5813e1 100644 --- a/packages/gitbook/src/intl/translations/no.ts +++ b/packages/gitbook/src/intl/translations/no.ts @@ -120,4 +120,6 @@ export const no: TranslationLanguage = { copy_mcp_url: 'Kopier MCP-server URL', press_to_confirm: 'Trykk ${1} for å bekrefte', tool_call_skipped: 'Hoppet over "${1}"', + scroll_back: 'Rull tilbake', + scroll_further: 'Rull videre', }; diff --git a/packages/gitbook/src/intl/translations/pt-br.ts b/packages/gitbook/src/intl/translations/pt-br.ts index eb2ddc15e5..50953e6275 100644 --- a/packages/gitbook/src/intl/translations/pt-br.ts +++ b/packages/gitbook/src/intl/translations/pt-br.ts @@ -119,4 +119,6 @@ export const pt_br = { copy_mcp_url: 'Copiar URL do servidor MCP', press_to_confirm: 'Pressione ${1} para confirmar', tool_call_skipped: 'Pulado "${1}"', + scroll_back: 'Rolar para trás', + scroll_further: 'Rolar para frente', }; diff --git a/packages/gitbook/src/intl/translations/ru.ts b/packages/gitbook/src/intl/translations/ru.ts index 4a1b9523ec..7771b09399 100644 --- a/packages/gitbook/src/intl/translations/ru.ts +++ b/packages/gitbook/src/intl/translations/ru.ts @@ -118,4 +118,6 @@ export const ru = { copy_mcp_url: 'Скопировать URL MCP-сервера', press_to_confirm: 'Нажмите ${1} для подтверждения', tool_call_skipped: 'Пропущен "${1}"', + scroll_back: 'Прокрутить назад', + scroll_further: 'Прокрутить дальше', }; diff --git a/packages/gitbook/src/intl/translations/zh.ts b/packages/gitbook/src/intl/translations/zh.ts index 57813af063..74baf6828f 100644 --- a/packages/gitbook/src/intl/translations/zh.ts +++ b/packages/gitbook/src/intl/translations/zh.ts @@ -116,4 +116,6 @@ export const zh: TranslationLanguage = { copy_mcp_url: '复制 MCP 服务器 URL', press_to_confirm: '按 ${1} 确认', tool_call_skipped: '已跳过 "${1}"', + scroll_back: '向后滚动', + scroll_further: '向前滚动', };