Merge remote-tracking branch 'origin/main' into conico/rnd-9299-fallback-computed

This commit is contained in:
Nicolas Dorseuil
2026-01-15 10:22:11 +01:00
22 changed files with 146 additions and 152 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Align styling of PageLinkItem and PageDocumentItem
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Better conditional display of page actions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Standardise toggle chevrons across the app
+5 -5
View File
@@ -273,10 +273,10 @@ const testCases: TestsCase[] = [
await expect(navigationLink).not.toBeVisible();
// Find and click the chevron element that is next to "Editor" in the TOC
// It is a span inside the link
// It is a button inside the link
const editorChevron = page
.getByRole('link', { name: 'Editor' })
.locator('span');
.locator('button');
await editorChevron.click();
// Verify "Navigation" link becomes visible after expansion
@@ -295,10 +295,10 @@ const testCases: TestsCase[] = [
await expect(navigationLink).not.toBeVisible();
// Find and click the chevron element that is next to "Editor" in the TOC
// It is a span inside the link
// It is a button inside the link
const editorChevron = page
.getByRole('link', { name: 'Editor' })
.locator('span');
.locator('button');
await editorChevron.click();
// At this stage the link should still not be visible
@@ -307,7 +307,7 @@ const testCases: TestsCase[] = [
// Then we click 'Content Structure' chevron to expand further
const contentStructureChevron = page
.getByRole('link', { name: 'Content Structure' })
.locator('span');
.locator('button');
await contentStructureChevron.click();
// Verify "Spaces" link becomes visible after expansion
@@ -6,7 +6,7 @@ import { useEffect, useId, useMemo, useRef, useState } from 'react';
import { useAdaptiveVisitor } from '@/components/Adaptive';
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
import { useScrollListener } from '@/components/hooks/useScrollListener';
import { Button } from '@/components/primitives';
import { Button, ToggleChevron } from '@/components/primitives';
import { t, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { useDebounceCallback } from 'usehooks-ts';
@@ -183,7 +183,7 @@ function CodeBlockExpandable(props: {
</div>
<div className="pointer-events-none absolute bottom-0 flex w-full justify-center">
<Button
icon={isExpanded ? 'chevron-up' : 'chevron-down'}
icon={<ToggleChevron open={isExpanded} />}
size="xsmall"
variant="blank"
type="button"
@@ -4,6 +4,7 @@ import { Icon } from '@gitbook/icons';
import { getNodeFragmentByType } from '@/lib/document';
import { tcls } from '@/lib/tailwind';
import { ToggleChevron } from '@/components/primitives';
import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks';
import { Inlines } from '../Inlines';
@@ -46,14 +47,14 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
'[&::-webkit-details-marker]:hidden'
)}
>
<Icon
icon="chevron-right"
<ToggleChevron
orientation="right-to-down"
open={context.mode === 'print'}
className={tcls(
'inline-block',
'size-3',
'mr-3',
'mb-1',
'transition-transform',
'shrink-0',
'group-open/expandable:rotate-90'
)}
@@ -11,10 +11,9 @@ import assertNever from 'assert-never';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { Button, Link } from '../primitives';
import { Button, Link, ToggleChevron } from '../primitives';
import {
type DropdownButtonProps,
DropdownChevron,
DropdownMenu,
DropdownMenuItem,
} from '../primitives/DropdownMenu';
@@ -177,7 +176,7 @@ function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
{...rest}
>
{title}
{isDropdown ? <DropdownChevron /> : null}
{isDropdown ? <ToggleChevron /> : null}
</Link>
);
}
@@ -195,7 +194,7 @@ function HeaderItemDropdown(
{...rest}
>
{title}
<DropdownChevron />
<ToggleChevron />
</span>
);
}
@@ -10,12 +10,8 @@ import type React from 'react';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import {
DropdownChevron,
DropdownMenu,
DropdownMenuItem,
DropdownSubMenu,
} from '../primitives/DropdownMenu';
import { ToggleChevron } from '../primitives';
import { DropdownMenu, DropdownMenuItem, DropdownSubMenu } from '../primitives/DropdownMenu';
import styles from './headerLinks.module.css';
/**
@@ -44,7 +40,7 @@ export function HeaderLinkMore(props: {
>
<span className="sr-only">{label}</span>
<Icon icon="ellipsis" className={tcls('size-4')} />
<DropdownChevron />
<ToggleChevron />
</button>
);
@@ -4,8 +4,8 @@ import { useMemo } from 'react';
import type { GitBookSiteContext } from '@/lib/context';
import { getSiteSpaceURL } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { Button, type ButtonProps } from '../primitives';
import { DropdownChevron, DropdownMenu } from '../primitives/DropdownMenu';
import { Button, type ButtonProps, ToggleChevron } from '../primitives';
import { DropdownMenu } from '../primitives/DropdownMenu';
import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
// Memoized regex for checking if a string starts with an emoji
@@ -37,7 +37,7 @@ export function SpacesDropdown(props: {
data-testid="space-dropdown-button"
size="small"
variant={variant}
trailing={<DropdownChevron />}
trailing={<ToggleChevron />}
className={tcls('bg-tint-base', className)}
>
<span className="button-content">{siteSpace.title}</span>
@@ -4,9 +4,9 @@ import { Button, ButtonGroup } from '@/components/primitives/Button';
import { DropdownMenu, DropdownMenuSeparator } from '@/components/primitives/DropdownMenu';
import { tString, useLanguage } from '@/intl/client';
import type { GitSyncState, SiteCustomizationSettings } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import React, { useRef } from 'react';
import { useAI } from '../AI';
import { ToggleChevron } from '../primitives';
import {
ActionCopyMCPURL,
ActionCopyMarkdown,
@@ -51,18 +51,13 @@ export function PageActionsDropdown(props: PageActionsDropdownProps) {
return defaultAction || dropdownActions.length > 0 ? (
<ButtonGroup ref={ref} className={props.className}>
{defaultAction}
{dropdownActions.length > 0 ? (
{!defaultAction || dropdownActions.length > 1 ? (
<DropdownMenu
align="end"
className="!min-w-60 max-w-max"
button={
<Button
icon={
<Icon
icon="chevron-down"
className="size-text-sm transition-transform group-data-[state=open]/button:rotate-180"
/>
}
icon={<ToggleChevron className="size-text-sm" />}
label={tString(language, defaultAction ? 'more' : 'actions')}
iconOnly={!!defaultAction}
size="xsmall"
@@ -28,6 +28,14 @@ export async function PageHeader(props: {
const hasAncestors = ancestors.length > 0;
// Show page actions if *any* of the actions are enabled
const hasPageActions = [
...Object.values(context.customization.pageActions),
context.customization.pdf.enabled,
context.customization.git.showEditLink,
withRSSFeed,
].some(Boolean);
return (
<header
className={tcls(
@@ -41,8 +49,7 @@ export async function PageHeader(props: {
hasAncestors ? 'page-has-ancestors' : 'page-no-ancestors'
)}
>
{page.layout.tableOfContents ? (
// Show page actions if *any* of the actions are enabled
{page.layout.tableOfContents && hasPageActions ? (
<PageActionsDropdown
siteTitle={context.site.title}
urls={getPageActionsURLs({ context, page, withRSSFeed })}
@@ -2,7 +2,7 @@
import { t, tString, useLanguage } from '@/intl/client';
import type { SiteSection } from '@gitbook/api';
import { Button, DropdownChevron, DropdownMenu, DropdownMenuItem } from '../primitives';
import { Button, DropdownMenu, DropdownMenuItem, ToggleChevron } from '../primitives';
import { useSearch } from './useSearch';
interface SearchScopeControlProps {
@@ -64,7 +64,7 @@ function SearchScopeSectionControl(props: SearchScopeControlProps & { isExtended
isExtended ? 'search_scope_section_all' : 'search_scope_section_current',
section?.title ?? ''
)}
trailing={<DropdownChevron />}
trailing={<ToggleChevron />}
/>
}
>
@@ -123,7 +123,7 @@ function SearchScopeVariantControl(props: SearchScopeControlProps & { isExtended
isExtended ? 'search_scope_variant_all' : 'search_scope_variant_current',
spaceTitle ?? ''
)}
trailing={<DropdownChevron />}
trailing={<ToggleChevron />}
/>
}
>
@@ -1,13 +1,13 @@
'use client';
import { Icon, type IconName } from '@gitbook/icons';
import type { IconName } from '@gitbook/icons';
import { motion } from 'motion/react';
import React from 'react';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { findSectionInGroup } from '@/lib/utils';
import { useToggleAnimation } from '../hooks';
import { Link } from '../primitives';
import { Link, ToggleChevron } from '../primitives';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { SectionIcon } from './SectionIcon';
import type {
@@ -199,21 +199,18 @@ export function SiteSectionGroupItem(props: {
isActiveGroup && 'hover:bg-tint-hover'
)}
>
<Icon
icon="chevron-right"
<ToggleChevron
open={isOpen}
orientation="right-to-down"
className={tcls(
'grid',
'shrink-0',
'size-3',
'm-1',
'transition-opacity',
'text-current',
'transition-transform',
'opacity-6',
'group-hover:opacity-11',
'contrast-more:opacity-11',
isOpen ? 'rotate-90' : 'rotate-0'
'contrast-more:opacity-11'
)}
/>
</span>
@@ -4,7 +4,7 @@ import type { IconName } from '@gitbook/icons';
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
import React from 'react';
import { Button, DropdownChevron, Link } from '@/components/primitives';
import { Button, Link, ToggleChevron } from '@/components/primitives';
import { tcls } from '@/lib/tailwind';
import { findSectionInGroup } from '@/lib/utils';
import { useIsMobile } from '../hooks/useIsMobile';
@@ -188,7 +188,7 @@ const SectionTab = React.forwardRef(function SectionTab(
{...rest}
icon={icon ? <SectionIcon isActive={isActive} icon={icon} /> : null}
label={title}
trailing={isGroup ? <DropdownChevron /> : null}
trailing={isGroup ? <ToggleChevron /> : null}
active={isActive}
className={tcls(
'group/dropdown relative my-1.5 overflow-visible',
@@ -38,14 +38,8 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
) : null
}
>
{page.emoji || page.icon ? (
<span className="flex items-center gap-3">
<TOCPageIcon page={page} />
{page.title}
</span>
) : (
page.title
)}
<TOCPageIcon page={page} />
{page.title}
</ToggleableLinkItem>
</li>
);
@@ -12,11 +12,13 @@ import { TOCPageIcon } from './TOCPageIcon';
export function PageLinkItem(props: { page: ClientTOCPageLink }) {
const { page } = props;
const isExternal = page.target.kind === 'url';
return (
<li className={tcls('flex', 'flex-col')}>
<Link
href={page.href ?? '#'}
classNames={['PageLinkItemStyles']}
classNames={['ToggleableLinkItemStyles']}
insights={{
type: 'link_click',
link: {
@@ -27,21 +29,24 @@ export function PageLinkItem(props: { page: ClientTOCPageLink }) {
>
<TOCPageIcon page={page} />
{page.title}
<Icon
icon="arrow-up-right-from-square"
className={tcls(
'size-3',
'mr-1',
'mt-1',
'place-self-start',
'shrink-0',
'text-current',
'transition-colors',
'[&>path]:transition-opacity',
'[&>path]:opacity-[0.4]',
'group-hover:[&>path]:opacity-11'
)}
/>
{isExternal ? (
<Icon
icon="arrow-up-right"
className={tcls(
'size-3',
'ml-auto',
'mr-1',
'mt-1',
'place-self-start',
'shrink-0',
'text-current',
'transition-all',
'opacity-6',
'group-hover/toclink:opacity-11',
'contrast-more:opacity-11'
)}
/>
) : null}
</Link>
</li>
);
@@ -1,13 +1,8 @@
'use client';
import { Icon } from '@gitbook/icons';
import { AnimatePresence, motion } from 'motion/react';
import React, { useRef } from 'react';
import { tcls } from '@/lib/tailwind';
import { useCurrentPagePath } from '../hooks';
import { Link, type LinkInsightsProps, type LinkProps } from '../primitives';
import { Button, Link, type LinkInsightsProps, type LinkProps, ToggleChevron } from '../primitives';
import { useScrollToActiveTOCItem } from './TOCScroller';
/**
@@ -138,42 +133,28 @@ function Toggler(props: {
isOpen: boolean;
onToggle: () => void;
}) {
const { isLinkActive, isOpen, onToggle } = props;
const { isOpen, onToggle } = props;
return (
<span
className={tcls(
'group',
'relative',
'rounded-full',
'straight-corners:rounded-xs',
'w-5',
'h-5',
'after:grid-area-1-1',
'after:absolute',
'after:-top-1',
'after:grid',
'after:-left-1',
'after:w-7',
'after:h-7',
'hover:bg-tint-active',
'hover:text-current',
isLinkActive && 'hover:bg-tint-hover'
)}
<Button
icon={
<ToggleChevron
open={isOpen}
orientation="right-to-down"
className="m-0! size-3! opacity-6 group-hover:opacity-11"
/>
}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onToggle();
}}
>
<Icon
icon="chevron-right"
className={tcls(
'm-1 grid size-3 shrink-0 text-current opacity-6 transition',
'group-hover:opacity-11 contrast-more:opacity-11',
isOpen ? 'rotate-90' : 'rotate-0'
)}
/>
</span>
label={undefined}
size="xsmall"
iconOnly
variant="blank"
className="ml-auto text-current hover:bg-tint-base"
tabIndex={-1} // Prevent focus on the button since it's already inside a clickable link that performs the same toggle action.
/>
);
}
@@ -1,26 +1,6 @@
export const PageLinkItemStyles = [
'flex',
'justify-start',
'items-center',
'gap-3',
'p-1.5',
'pl-3',
'text-sm',
'transition-colors',
'duration-100',
'text-tint-strong/7',
'rounded-md',
'straight-corners:rounded-none',
'circular-corners:rounded-xl',
'before:content-none',
'font-normal',
'hover:bg-tint',
'hover:text-tint-strong',
];
export const ToggleableLinkItemStyles = [
'group/toclink toclink relative transition-colors',
'flex flex-row justify-between',
'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',
@@ -10,6 +10,7 @@ import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import { assert } from 'ts-essentials';
import { Link, type LinkInsightsProps } from '.';
import { ToggleChevron } from './ToggleChevron';
export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
@@ -99,26 +100,6 @@ export function DropdownMenu(props: {
);
}
/**
* Animated chevron to display in the dropdown button.
*/
export function DropdownChevron() {
return (
<Icon
icon="chevron-down"
className={tcls(
'shrink-0',
'opacity-6',
'size-3',
'transition-all',
'group-hover/dropdown:opacity-11',
'group-data-[state=open]/dropdown:opacity-11',
'group-data-[state=open]/dropdown:rotate-180'
)}
/>
);
}
/**
* Button with a chevron for use in dropdowns.
*/
@@ -131,7 +112,7 @@ export function DropdownButton(props: {
return (
<div className={tcls('group/dropdown', 'flex', 'items-center', className)}>
{children}
<DropdownChevron />
<ToggleChevron />
</div>
);
}
@@ -3,7 +3,6 @@ import type { ClassValue } from '@/lib/tailwind';
import { RecordCardStyles } from '../DocumentView/Table/styles';
import {
PageLinkItemStyles,
ToggleableLinkItemActiveStyles,
ToggleableLinkItemStyles,
} from '../TableOfContents/styles';
@@ -14,7 +13,6 @@ const styles = {
CardStyles,
ButtonStyles,
RecordCardStyles,
PageLinkItemStyles,
ToggleableLinkItemStyles,
ToggleableLinkItemActiveStyles,
};
@@ -0,0 +1,44 @@
import { type ClassValue, tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
/**
* Animated chevron to display in dropdowns and other toggleable elements.
* Uses the `data-[state=open]` attribute to animate the chevron, or you can pass it an `open` prop to control the state.
*/
export function ToggleChevron(props: {
open?: boolean;
orientation?: 'down-to-up' | 'right-to-down'; // The direction of the chevron when open.
className?: ClassValue;
}) {
const {
open,
orientation = 'down-to-up',
className = 'opacity-6 group-hover/dropdown:opacity-11',
} = props;
const classes = {
'down-to-up': {
icon: 'chevron-down',
animation: 'rotate-180',
autoAnimation: 'group-data-[state=open]/dropdown:rotate-180 group-open:rotate-180',
},
'right-to-down': {
icon: 'chevron-right',
animation: 'rotate-90',
autoAnimation: 'group-data-[state=open]/dropdown:rotate-90 group-open:rotate-90',
},
};
return (
<Icon
icon={classes[orientation].icon as IconName}
className={tcls(
'shrink-0',
open ? classes[orientation].animation : classes[orientation].autoAnimation,
'size-3',
'transition-all',
className
)}
/>
);
}
@@ -14,3 +14,4 @@ export * from './LoadingStateProvider';
export * from './HoverCard';
export * from './DropdownMenu';
export * from './Input';
export * from './ToggleChevron';