mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 02:53:29 +00:00
Rework table of contents to accommodate faded edges
This commit is contained in:
@@ -229,7 +229,7 @@ export function AIChatBody(props: {
|
||||
className="shrink grow basis-80 animate-fade-in-slow [container-type:size]"
|
||||
contentClassName="p-4 gutter-stable flex flex-col gap-4"
|
||||
orientation="vertical"
|
||||
fadeEdges={['leading']}
|
||||
trailing={{ fade: false, button: true }}
|
||||
active={`#message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
|
||||
>
|
||||
{isEmpty ? (
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function EmbeddableDocsPage(
|
||||
orientation="vertical"
|
||||
className="not-hydrated:animate-blur-in-slow"
|
||||
contentClassName="p-4"
|
||||
fadeEdges={context.sections ? [] : ['leading']}
|
||||
leading={{ fade: !context.sections, button: true }}
|
||||
>
|
||||
<TableOfContents context={context} />
|
||||
<PageBody
|
||||
|
||||
@@ -77,7 +77,11 @@ export function SiteSectionTabs(props: {
|
||||
: 'after:contents[] after:absolute after:inset-y-2 after:right-0 after:border-transparent after:border-r after:transition-colors'
|
||||
)}
|
||||
active={`#${currentSection.id}`}
|
||||
trailingEdgeScrollClassName={children ? 'after:border-tint' : ''}
|
||||
trailing={{
|
||||
fade: true,
|
||||
button: true,
|
||||
className: children ? 'after:border-tint' : '',
|
||||
}}
|
||||
>
|
||||
<NavigationMenu.List
|
||||
className={tcls(
|
||||
|
||||
@@ -14,10 +14,10 @@ export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boole
|
||||
<li className="flex flex-col">
|
||||
<div
|
||||
className={tcls(
|
||||
'-top-6 sticky z-1 flex items-center gap-3 px-3 pt-6',
|
||||
'-top-2 sticky z-1 flex items-center gap-3 px-3',
|
||||
'font-semibold text-xs uppercase tracking-wide',
|
||||
'pb-3', // Add extra padding to make the header fade a bit nicer
|
||||
'-mb-1.5', // Then pull the page items a bit closer, effective bottom padding is 1.5 units / 6px.
|
||||
'pt-4 pb-3', // Add extra padding to make the header fade a bit nicer
|
||||
'-mb-1.5 mt-2', // Then pull the page items a bit closer, effective bottom padding is 1.5 units / 6px.
|
||||
'mask-[linear-gradient(rgba(0,0,0,1)_70%,rgba(0,0,0,0))]', // Fade out effect of fixed page items. We want the fade to start past the header, this is a good approximation.
|
||||
'bg-tint-base',
|
||||
'sidebar-filled:bg-tint-subtle',
|
||||
@@ -27,7 +27,7 @@ export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boole
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
'lg:[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
|
||||
'lg:[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint',
|
||||
isFirst ? '-mt-6' : ''
|
||||
isFirst ? '-mt-2' : ''
|
||||
)}
|
||||
>
|
||||
<TOCPageIcon page={page} />
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function TableOfContents(props: {
|
||||
'lg:page-no-toc:[html[style*="--outline-top-offset"]_&]:top-(--outline-top-offset)!',
|
||||
'lg:page-no-toc:[html[style*="--outline-height"]_&]:top-(--outline-height)!',
|
||||
|
||||
'pt-6 pb-4',
|
||||
'pt-4 pb-4',
|
||||
'lg:sidebar-filled:pr-6',
|
||||
'lg:page-no-toc:pr-0',
|
||||
'max-lg:pl-8',
|
||||
@@ -88,7 +88,7 @@ export async function TableOfContents(props: {
|
||||
<div // The actual sidebar, either shown with a filled bg or transparent.
|
||||
className={tcls(
|
||||
'-ms-5',
|
||||
'relative flex grow flex-col overflow-hidden border-tint-subtle',
|
||||
'relative flex min-h-0 grow flex-col border-tint-subtle',
|
||||
|
||||
'sidebar-filled:bg-tint-subtle',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
@@ -108,10 +108,15 @@ export async function TableOfContents(props: {
|
||||
{innerHeader ? innerHeader : null}
|
||||
<ScrollContainer
|
||||
data-testid="toc-scroll-container"
|
||||
fadeEdges={['trailing']}
|
||||
orientation="vertical"
|
||||
contentClassName="flex grow flex-col p-2 gutter-stable"
|
||||
contentClassName="flex flex-col p-2 gutter-stable"
|
||||
active="[data-active=true]"
|
||||
leading={{
|
||||
fade: true,
|
||||
button: {
|
||||
className: '-mt-2',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PagesList
|
||||
pages={pages}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function Trademark(props: {
|
||||
}) {
|
||||
const { className, ...rest } = props;
|
||||
return (
|
||||
<div className={tcls('mt-auto pr-4 pb-2 pl-2', className)}>
|
||||
<div className={tcls('mt-auto px-2 sidebar-default:pr-4 pb-2', className)}>
|
||||
<TrademarkLink {...rest} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import * as React from 'react';
|
||||
import { useScrollListener } from '../hooks/useScrollListener';
|
||||
import { Button } from './Button';
|
||||
import { Button, type ButtonProps } from './Button';
|
||||
|
||||
/**
|
||||
* A container that encapsulates a scrollable area with usability features.
|
||||
@@ -17,17 +17,26 @@ export type ScrollContainerProps = {
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
|
||||
/** Optional class(es) to apply when there the container can be scrolled on the leading (left or top) edge */
|
||||
leadingEdgeScrollClassName?: string;
|
||||
|
||||
/** Optional class(es) to apply when there the container can be scrolled on the trailing (right or bottom) edge */
|
||||
trailingEdgeScrollClassName?: string;
|
||||
|
||||
/** The direction of the scroll container. */
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
|
||||
/** Whether to fade out the edges of the container. */
|
||||
fadeEdges?: ('leading' | 'trailing')[];
|
||||
leading?: {
|
||||
/** Whether to fade out the leading edge of the container. */
|
||||
fade: boolean;
|
||||
/** Whether to show a button to scroll back. */
|
||||
button: boolean | ButtonProps;
|
||||
/** Optional class(es) to apply when there the container can be scrolled on the leading (left or top) edge */
|
||||
className?: string;
|
||||
};
|
||||
|
||||
trailing?: {
|
||||
/** Whether to fade out the trailing edge of the container. */
|
||||
fade: boolean;
|
||||
/** Whether to show a button to scroll forward. */
|
||||
button: boolean | ButtonProps;
|
||||
/** Optional class(es) to apply when there the container can be scrolled on the trailing (right or bottom) edge */
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** The ID or ref of the active item to scroll to. */
|
||||
active?: string | React.RefObject<HTMLElement | null>;
|
||||
@@ -39,10 +48,9 @@ export function ScrollContainer(props: ScrollContainerProps) {
|
||||
className,
|
||||
contentClassName,
|
||||
orientation,
|
||||
fadeEdges = ['leading', 'trailing'],
|
||||
active,
|
||||
leadingEdgeScrollClassName,
|
||||
trailingEdgeScrollClassName,
|
||||
leading = { fade: true, button: true },
|
||||
trailing = { fade: true, button: true },
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -140,28 +148,30 @@ export function ScrollContainer(props: ScrollContainerProps) {
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'group/scroll-container relative flex overflow-hidden',
|
||||
'group/scroll-container relative flex shrink grow',
|
||||
orientation === 'horizontal' ? 'min-w-0' : 'min-h-0',
|
||||
className,
|
||||
scrollPosition > 0 ? leadingEdgeScrollClassName : '',
|
||||
scrollPosition < scrollSize ? trailingEdgeScrollClassName : ''
|
||||
scrollPosition > 0 ? leading?.className : '',
|
||||
scrollPosition < scrollSize ? trailing?.className : ''
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{/* Scrollable content */}
|
||||
<div
|
||||
className={tcls(
|
||||
'flex shrink grow',
|
||||
'flex flex-1 overflow-hidden',
|
||||
orientation === 'horizontal' ? 'min-w-0' : 'min-h-0',
|
||||
orientation === 'horizontal' ? 'no-scrollbar' : 'hide-scrollbar',
|
||||
orientation === 'horizontal' ? 'overflow-x-scroll' : 'flex-col overflow-y-auto',
|
||||
fadeEdges.includes('leading') && scrollPosition > 0
|
||||
leading.fade && scrollPosition > 0
|
||||
? orientation === 'horizontal'
|
||||
? 'mask-l-from-[calc(100%-2rem)]'
|
||||
: 'mask-t-from-[calc(100%-2rem)]'
|
||||
? 'mask-l-from-[calc(100%-1.5rem)]'
|
||||
: 'mask-t-from-[calc(100%-1.5rem)]'
|
||||
: '',
|
||||
fadeEdges.includes('trailing') && scrollPosition < scrollSize
|
||||
trailing.fade && scrollPosition < scrollSize
|
||||
? orientation === 'horizontal'
|
||||
? 'mask-r-from-[calc(100%-2rem)]'
|
||||
: 'mask-b-from-[calc(100%-2rem)]'
|
||||
? 'mask-r-from-[calc(100%-1.5rem)]'
|
||||
: 'mask-b-from-[calc(100%-1.5rem)]'
|
||||
: '',
|
||||
contentClassName
|
||||
)}
|
||||
@@ -171,44 +181,52 @@ export function ScrollContainer(props: ScrollContainerProps) {
|
||||
</div>
|
||||
|
||||
{/* Scroll buttons back & forward */}
|
||||
<Button
|
||||
icon={orientation === 'horizontal' ? 'chevron-left' : 'chevron-up'}
|
||||
iconOnly
|
||||
size="xsmall"
|
||||
variant="secondary"
|
||||
tabIndex={-1}
|
||||
className={tcls(
|
||||
'bg-tint-base!',
|
||||
orientation === 'horizontal'
|
||||
? '-translate-y-1/2! top-1/2 left-0 ml-2'
|
||||
: '-translate-x-1/2! top-0 left-1/2 mt-2',
|
||||
'absolute z-10 not-pointer-none:block hidden scale-0 opacity-0 transition-[scale,opacity]',
|
||||
scrollPosition > 0
|
||||
? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
|
||||
: 'pointer-events-none'
|
||||
)}
|
||||
onClick={scrollBack}
|
||||
label={tString(language, 'scroll_back')}
|
||||
/>
|
||||
<Button
|
||||
icon={orientation === 'horizontal' ? 'chevron-right' : 'chevron-down'}
|
||||
iconOnly
|
||||
size="xsmall"
|
||||
variant="secondary"
|
||||
tabIndex={-1}
|
||||
className={tcls(
|
||||
'bg-tint-base!',
|
||||
orientation === 'horizontal'
|
||||
? '-translate-y-1/2! top-1/2 right-0 mr-2'
|
||||
: '-translate-x-1/2! bottom-0 left-1/2 mb-2',
|
||||
'absolute z-10 not-pointer-none:block hidden scale-0 transition-[scale,opacity]',
|
||||
scrollPosition < scrollSize
|
||||
? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
|
||||
: 'pointer-events-none'
|
||||
)}
|
||||
onClick={scrollFurther}
|
||||
label={tString(language, 'scroll_further')}
|
||||
/>
|
||||
{leading.button !== false ? (
|
||||
<Button
|
||||
icon={orientation === 'horizontal' ? 'chevron-left' : 'chevron-up'}
|
||||
iconOnly
|
||||
size="xsmall"
|
||||
variant="secondary"
|
||||
tabIndex={-1}
|
||||
onClick={scrollBack}
|
||||
label={tString(language, 'scroll_back')}
|
||||
{...(typeof leading.button === 'object' ? leading.button : {})}
|
||||
className={tcls(
|
||||
'bg-tint-base!',
|
||||
orientation === 'horizontal'
|
||||
? '-translate-y-1/2! top-1/2 left-0 ml-2'
|
||||
: '-translate-x-1/2! top-0 left-1/2 mt-2',
|
||||
'absolute z-10 not-pointer-none:block hidden scale-0 opacity-0 transition-[scale,opacity]',
|
||||
scrollPosition > 0
|
||||
? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
|
||||
: 'pointer-events-none',
|
||||
typeof leading.button === 'object' ? leading.button.className : ''
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{trailing.button !== false ? (
|
||||
<Button
|
||||
icon={orientation === 'horizontal' ? 'chevron-right' : 'chevron-down'}
|
||||
iconOnly
|
||||
size="xsmall"
|
||||
variant="secondary"
|
||||
tabIndex={-1}
|
||||
onClick={scrollFurther}
|
||||
label={tString(language, 'scroll_further')}
|
||||
{...(typeof trailing.button === 'object' ? trailing.button : {})}
|
||||
className={tcls(
|
||||
'bg-tint-base!',
|
||||
orientation === 'horizontal'
|
||||
? '-translate-y-1/2! top-1/2 right-0 mr-2'
|
||||
: '-translate-x-1/2! bottom-0 left-1/2 mb-2',
|
||||
'absolute z-10 not-pointer-none:block hidden scale-0 transition-[scale,opacity]',
|
||||
scrollPosition < scrollSize
|
||||
? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
|
||||
: 'pointer-events-none',
|
||||
typeof trailing.button === 'object' ? trailing.button.className : ''
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user