mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 07:35:16 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88cd02f1d5 | |||
| 53433efb65 | |||
| b92e78b573 | |||
| b357acb586 | |||
| 8795621331 | |||
| 040be0b6ca | |||
| 06b8a279c7 | |||
| 01db884fb2 | |||
| bc8d59fcb9 | |||
| f855d49631 | |||
| 15e0adcc9e | |||
| ab4af82b43 | |||
| ed007c818d | |||
| 8d88eb6532 | |||
| 098fe2d290 | |||
| b8dc0e6e2c | |||
| abfba66fed | |||
| 92ae60fe87 | |||
| a2a852eb17 | |||
| eef6d2f391 | |||
| 7b6dff7a9d | |||
| cdcd991cd2 | |||
| e848cf518d | |||
| 6659bfbc14 | |||
| e018b65ce6 | |||
| 2d5fc43395 | |||
| f5235756d4 | |||
| 451290a9d8 | |||
| a56216d00d | |||
| bb850e5b30 | |||
| 3a762dc494 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Revamp mobile navigation
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
@@ -98,6 +98,7 @@
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-map": "^7.0.3",
|
||||
"quick-lru": "^7.0.1",
|
||||
"react-aria": "^3.37.0",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
@@ -166,7 +167,7 @@
|
||||
},
|
||||
"packages/openapi-parser": {
|
||||
"name": "@gitbook/openapi-parser",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.18.0",
|
||||
"@scalar/openapi-types": "^0.1.9",
|
||||
@@ -213,7 +214,7 @@
|
||||
},
|
||||
"packages/react-openapi": {
|
||||
"name": "@gitbook/react-openapi",
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.4",
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "^1.3.16",
|
||||
|
||||
@@ -1695,6 +1695,57 @@ const testCases: TestsCase[] = [
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Mobile menu',
|
||||
contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Mobile menu open',
|
||||
viewports: ['iphone-x'],
|
||||
url: '',
|
||||
run: async (page) => {
|
||||
// Set mobile viewport size to ensure mobile menu is visible
|
||||
await page.setViewportSize({ width: 375, height: 812 }); // iPhone X dimensions
|
||||
|
||||
await page.locator('[data-testid="mobile-menu-button"]').click();
|
||||
|
||||
// Wait for table of contents to appear
|
||||
const tableOfContents = page.locator('[data-testid="table-of-contents"]');
|
||||
await tableOfContents.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await expect(tableOfContents).toBeVisible();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Mobile menu with dropdown menu',
|
||||
viewports: ['iphone-x'],
|
||||
url: 'multi-variants/',
|
||||
run: async (page) => {
|
||||
// Set mobile viewport size to ensure mobile menu is visible
|
||||
await page.setViewportSize({ width: 375, height: 812 }); // iPhone X dimensions
|
||||
|
||||
await page.locator('[data-testid="mobile-menu-button"]').click();
|
||||
|
||||
// Wait for table of contents to appear
|
||||
const tableOfContents = page.locator('[data-testid="table-of-contents"]');
|
||||
await tableOfContents.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await expect(tableOfContents).toBeVisible();
|
||||
|
||||
// Wait for space dropdown button to be visible
|
||||
const spaceDropdownButton = tableOfContents.locator(
|
||||
'[data-testid="space-dropdown-button"]'
|
||||
);
|
||||
await spaceDropdownButton.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await expect(spaceDropdownButton).toBeVisible();
|
||||
await spaceDropdownButton.click();
|
||||
|
||||
// Wait for space dropdown to appear
|
||||
const spaceDropdown = page.locator('[data-testid="dropdown-menu"]');
|
||||
await spaceDropdown.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await expect(spaceDropdown).toBeVisible();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
runTestCases(testCases);
|
||||
|
||||
@@ -67,6 +67,10 @@ export interface Test {
|
||||
* Whether to only run this test.
|
||||
*/
|
||||
only?: boolean;
|
||||
/**
|
||||
* Viewport to use for the test.
|
||||
*/
|
||||
viewports?: ('macbook-16' | 'macbook-13' | 'ipad-2' | 'iphone-x')[];
|
||||
}
|
||||
|
||||
export type TestsCase = {
|
||||
@@ -162,7 +166,7 @@ export function runTestCases(testCases: TestsCase[]) {
|
||||
|
||||
test.describe(testCase.name, () => {
|
||||
for (const testEntry of testCase.tests) {
|
||||
const { mode = 'page' } = testEntry;
|
||||
const { mode = 'page', viewports } = testEntry;
|
||||
const testFn = testEntry.only ? test.only : test;
|
||||
testFn(testEntry.name, async ({ page, context }) => {
|
||||
const testEntryPathname =
|
||||
@@ -207,13 +211,18 @@ export function runTestCases(testCases: TestsCase[]) {
|
||||
const screenshotName = `${testCase.name} - ${testEntry.name}`;
|
||||
if (mode === 'image') {
|
||||
await argosScreenshot(page, screenshotName, {
|
||||
viewports: ['macbook-13'],
|
||||
viewports: viewports ?? ['macbook-13'],
|
||||
threshold: screenshotOptions?.threshold ?? undefined,
|
||||
fullPage: true,
|
||||
});
|
||||
} else {
|
||||
await argosScreenshot(page, screenshotName, {
|
||||
viewports: ['macbook-16', 'macbook-13', 'ipad-2', 'iphone-x'],
|
||||
viewports: viewports ?? [
|
||||
'macbook-16',
|
||||
'macbook-13',
|
||||
'ipad-2',
|
||||
'iphone-x',
|
||||
],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"ai": "^4.2.2",
|
||||
"assert-never": "^1.2.1",
|
||||
"react-aria": "^3.37.0",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
"direction": "^2.0.1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
|
||||
import { HeaderMobileMenuButton } from '@/components/Header/HeaderMobileMenuButton';
|
||||
import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '@/components/layout';
|
||||
import { getSpaceLanguage, t } from '@/intl/server';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
@@ -8,7 +9,6 @@ import { HeaderLink } from './HeaderLink';
|
||||
import { HeaderLinkMore } from './HeaderLinkMore';
|
||||
import { HeaderLinks } from './HeaderLinks';
|
||||
import { HeaderLogo } from './HeaderLogo';
|
||||
import { HeaderMobileMenu } from './HeaderMobileMenu';
|
||||
import { SpacesDropdown } from './SpacesDropdown';
|
||||
|
||||
/**
|
||||
@@ -78,7 +78,7 @@ export function Header(props: {
|
||||
'min-w-0 shrink items-center justify-start gap-2 lg:gap-4'
|
||||
)}
|
||||
>
|
||||
<HeaderMobileMenu
|
||||
<HeaderMobileMenuButton
|
||||
className={tcls(
|
||||
'lg:hidden',
|
||||
'-ml-2',
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { useScrollListener } from '../hooks/useScrollListener';
|
||||
|
||||
const globalClassName = 'navigation-open';
|
||||
|
||||
const SCROLL_DISTANCE = 320;
|
||||
|
||||
/**
|
||||
* Button to show/hide the table of content on mobile.
|
||||
*/
|
||||
export function HeaderMobileMenu(props: Partial<React.ButtonHTMLAttributes<HTMLButtonElement>>) {
|
||||
const language = useLanguage();
|
||||
|
||||
const pathname = usePathname();
|
||||
const hasScrollRef = useRef(false);
|
||||
|
||||
const toggleNavigation = () => {
|
||||
if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) {
|
||||
document.body.classList.remove(globalClassName);
|
||||
} else {
|
||||
document.body.classList.add(globalClassName);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const windowRef = useRef(typeof window === 'undefined' ? null : window);
|
||||
useScrollListener(() => {
|
||||
hasScrollRef.current = window.scrollY >= SCROLL_DISTANCE;
|
||||
}, windowRef);
|
||||
|
||||
// Close the navigation when navigating to a page
|
||||
useEffect(() => {
|
||||
document.body.classList.remove(globalClassName);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
aria-label={tString(language, 'table_of_contents_button_label')}
|
||||
onClick={toggleNavigation}
|
||||
className={tcls(
|
||||
'flex flex-row items-center rounded straight-corners:rounded-sm px-2 py-1',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<Icon icon="bars" className="size-4 text-inherit" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { useMobileMenuSheet } from '@/components/MobileMenu/useMobileMenuSheet';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
/**
|
||||
* Button to show/hide the table of content on mobile.
|
||||
*/
|
||||
export function HeaderMobileMenuButton(
|
||||
props: Partial<React.ButtonHTMLAttributes<HTMLButtonElement>>
|
||||
) {
|
||||
const language = useLanguage();
|
||||
const { open, setOpen } = useMobileMenuSheet();
|
||||
|
||||
const toggleNavigation = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
aria-label={tString(language, 'table_of_contents_button_label')}
|
||||
data-testid="mobile-menu-button"
|
||||
onClick={toggleNavigation}
|
||||
className={tcls(
|
||||
'flex flex-row items-center rounded straight-corners:rounded-sm px-2 py-1',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<Icon icon="bars" className="size-4 text-inherit" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,9 @@ export function SpacesDropdown(props: {
|
||||
siteSpace: SiteSpace;
|
||||
siteSpaces: SiteSpace[];
|
||||
className?: string;
|
||||
withPortal?: boolean;
|
||||
}) {
|
||||
const { context, siteSpace, siteSpaces, className } = props;
|
||||
const { context, siteSpace, siteSpaces, className, withPortal } = props;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
@@ -20,6 +21,7 @@ export function SpacesDropdown(props: {
|
||||
'group-hover/dropdown:invisible', // Prevent hover from opening the dropdown, as it's annoying in this context
|
||||
'group-focus-within/dropdown:group-hover/dropdown:visible' // When the dropdown is already open, it should remain visible when hovered
|
||||
)}
|
||||
withPortal={withPortal}
|
||||
button={
|
||||
<div
|
||||
data-testid="space-dropdown-button"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useMobileMenuSheet } from '@/components/MobileMenu';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { usePreventScroll } from 'react-aria';
|
||||
|
||||
export function MobileMenuScript() {
|
||||
const pathname = usePathname();
|
||||
const { open, setOpen } = useMobileMenuSheet();
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Close the navigation when navigating to a page
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
// Prevent scrolling when the menu is open
|
||||
usePreventScroll({
|
||||
isDisabled: !open,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './useMobileMenuSheet';
|
||||
export * from './MobileMenuScript';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
/**
|
||||
* Hooks to manage the mobile menu sheet state.
|
||||
*/
|
||||
export const useMobileMenuSheet = create<{
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}>((set) => ({
|
||||
open: false,
|
||||
setOpen: (open) => set({ open }),
|
||||
}));
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Header, HeaderLogo } from '@/components/Header';
|
||||
import { TableOfContents } from '@/components/TableOfContents';
|
||||
import { TOCScrollContent, TableOfContents } from '@/components/TableOfContents';
|
||||
import { CONTAINER_STYLE } from '@/components/layout';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -105,7 +105,6 @@ export function SpaceLayout(props: {
|
||||
)}
|
||||
>
|
||||
<TableOfContents
|
||||
context={context}
|
||||
header={
|
||||
withTopHeader ? null : (
|
||||
<div
|
||||
@@ -123,34 +122,43 @@ export function SpaceLayout(props: {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
innerHeader={
|
||||
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
|
||||
<>
|
||||
{!withTopHeader && searchAndAI}
|
||||
{!withTopHeader && withSections && sections && (
|
||||
<SiteSectionList
|
||||
className={tcls('hidden', 'lg:block')}
|
||||
sections={encodeClientSiteSections(
|
||||
context,
|
||||
sections
|
||||
>
|
||||
<TOCScrollContent
|
||||
context={context}
|
||||
innerHeader={
|
||||
(!withTopHeader && !!searchAndAI) ||
|
||||
(!withTopHeader && withSections && !!sections) ||
|
||||
(isMultiVariants && !sections) ? (
|
||||
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
|
||||
<>
|
||||
{!withTopHeader && searchAndAI}
|
||||
{!withTopHeader && withSections && sections && (
|
||||
<SiteSectionList
|
||||
className={tcls('hidden', 'lg:block')}
|
||||
sections={encodeClientSiteSections(
|
||||
context,
|
||||
sections
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isMultiVariants && !sections && (
|
||||
<SpacesDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
className={tcls(
|
||||
'w-full',
|
||||
'page-no-toc:hidden',
|
||||
'site-header-none:page-no-toc:flex'
|
||||
{isMultiVariants && !sections && (
|
||||
<SpacesDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
className={tcls(
|
||||
'w-full',
|
||||
'page-no-toc:hidden',
|
||||
'site-header-none:page-no-toc:flex'
|
||||
)}
|
||||
withPortal={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</TableOfContents>
|
||||
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,8 @@ export function PageGroupItem(props: { page: ClientTOCPageGroup }) {
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
'[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
|
||||
'[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint'
|
||||
'lg:[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
|
||||
'lg:[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint'
|
||||
)}
|
||||
>
|
||||
<TOCPageIcon page={page} />
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { PagesList } from '@/components/TableOfContents';
|
||||
import { Trademark } from '@/components/TableOfContents';
|
||||
import { TOCScrollContainer } from '@/components/TableOfContents/TOCScroller';
|
||||
import { encodeClientTableOfContents } from '@/components/TableOfContents/encodeClientTableOfContents';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
|
||||
|
||||
export async function TOCScrollContent(props: {
|
||||
context: GitBookSiteContext;
|
||||
innerHeader?: React.ReactNode;
|
||||
}) {
|
||||
const { context, innerHeader } = props;
|
||||
const { customization, revision } = context;
|
||||
|
||||
const pages = await encodeClientTableOfContents(context, revision.pages, revision.pages);
|
||||
|
||||
return (
|
||||
<div // The actual sidebar, either shown with a filled bg or transparent.
|
||||
className={tcls(
|
||||
'lg:-ms-5',
|
||||
'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
|
||||
|
||||
'sidebar-filled:bg-tint-subtle',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-gradient_&]:border',
|
||||
'page-no-toc:!bg-transparent',
|
||||
'page-no-toc:!border-none',
|
||||
|
||||
'sidebar-filled:rounded-xl',
|
||||
'straight-corners:rounded-none'
|
||||
)}
|
||||
>
|
||||
{!!innerHeader && (
|
||||
<div className="inline-flex w-full flex-col gap-4 px-2 max-lg:pt-2 max-lg:pr-14 max-lg:pb-2 lg:pr-4 lg:first:mt-4">
|
||||
{innerHeader}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TOCScrollContainer // The scrollview inside the sidebar
|
||||
className={tcls(
|
||||
'flex flex-grow flex-col p-2',
|
||||
innerHeader ? 'mt-0 lg:mt-4' : 'mt-11 lg:mt-0',
|
||||
customization.trademark.enabled && 'pb-[4.5rem]',
|
||||
'gutter-stable overflow-y-auto',
|
||||
'[&::-webkit-scrollbar]:bg-transparent',
|
||||
'[&::-webkit-scrollbar-thumb]:bg-transparent',
|
||||
'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
|
||||
'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
|
||||
'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
|
||||
)}
|
||||
>
|
||||
<PagesList
|
||||
pages={pages}
|
||||
style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
|
||||
/>
|
||||
{customization.trademark.enabled ? (
|
||||
<Trademark
|
||||
space={context.space}
|
||||
customization={customization}
|
||||
placement={SiteInsightsTrademarkPlacement.Sidebar}
|
||||
/>
|
||||
) : null}
|
||||
</TOCScrollContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +1,75 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
|
||||
'use client';
|
||||
|
||||
import { MobileMenuScript, useMobileMenuSheet } from '@/components/MobileMenu';
|
||||
import { TableOfContentsScript } from '@/components/TableOfContents/TableOfContentsScript';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { PagesList } from './PagesList';
|
||||
import { TOCScrollContainer } from './TOCScroller';
|
||||
import { TableOfContentsScript } from './TableOfContentsScript';
|
||||
import { Trademark } from './Trademark';
|
||||
import { encodeClientTableOfContents } from './encodeClientTableOfContents';
|
||||
|
||||
export async function TableOfContents(props: {
|
||||
context: GitBookSiteContext;
|
||||
export function TableOfContents(props: {
|
||||
header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header
|
||||
innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { innerHeader, context, header } = props;
|
||||
const { space, customization, revision } = context;
|
||||
|
||||
const pages = await encodeClientTableOfContents(context, revision.pages, revision.pages);
|
||||
const { header, children } = props;
|
||||
const { open, setOpen } = useMobileMenuSheet();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-tint-12/4 backdrop-blur-lg data-[state=closed]:pointer-events-none data-[state=closed]:invisible data-[state=closed]:animate-fadeOut data-[state=open]:animate-fadeIn lg:hidden dark:bg-tint-1/6"
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
<aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
|
||||
data-testid="table-of-contents"
|
||||
id="table-of-contents"
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
className={tcls(
|
||||
'group',
|
||||
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-4',
|
||||
|
||||
'border-tint-subtle',
|
||||
|
||||
'fixed',
|
||||
'z-50',
|
||||
|
||||
'max-lg:transition',
|
||||
'max-lg:ease-in-out',
|
||||
'max-lg:duration-500',
|
||||
|
||||
'max-lg:rounded-xl',
|
||||
'max-lg:circular-corners:rounded-2xl',
|
||||
'max-lg:straight-corners:rounded-none',
|
||||
'max-lg:bg-tint-base',
|
||||
'max-lg:sidebar-filled:bg-tint-subtle',
|
||||
'max-lg:theme-muted:bg-tint-subtle',
|
||||
'max-lg:[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'max-lg:[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'max-lg:[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
|
||||
'max-lg:pb-2',
|
||||
'max-lg:w-10/12',
|
||||
'max-lg:shadow-lg',
|
||||
'max-lg:depth-flat:shadow-none',
|
||||
'max-lg:inset-1.5',
|
||||
'max-lg:transition-all',
|
||||
'max-lg:duration-300',
|
||||
'max-lg:max-w-sm',
|
||||
'max-lg:data-[state=open]:left-1.5',
|
||||
'max-lg:data-[state=closed]:-left-full',
|
||||
|
||||
'text-sm',
|
||||
|
||||
'grow-0',
|
||||
'shrink-0',
|
||||
'basis-full',
|
||||
'lg:basis-72',
|
||||
'page-no-toc:lg:basis-56',
|
||||
|
||||
'relative',
|
||||
'z-[1]',
|
||||
'lg:sticky',
|
||||
'lg:z-[1]',
|
||||
'lg:mr-12',
|
||||
|
||||
// Server-side static positioning
|
||||
@@ -57,72 +89,36 @@ export async function TableOfContents(props: {
|
||||
'[html[style*="--toc-top-offset"]_&]:lg:!top-[var(--toc-top-offset)]',
|
||||
'[html[style*="--toc-height"]_&]:lg:!h-[var(--toc-height)]',
|
||||
|
||||
'pt-6',
|
||||
'pb-4',
|
||||
'lg:pt-6',
|
||||
'lg:pb-4',
|
||||
'sidebar-filled:lg:pr-6',
|
||||
'page-no-toc:lg:pr-0',
|
||||
|
||||
'hidden',
|
||||
'navigation-open:!flex',
|
||||
'lg:flex',
|
||||
'page-no-toc:lg:hidden',
|
||||
'page-no-toc:xl:flex',
|
||||
'site-header-none:page-no-toc:lg:flex',
|
||||
'flex-col',
|
||||
'gap-4',
|
||||
|
||||
'navigation-open:border-b',
|
||||
'border-tint-subtle'
|
||||
'site-header-none:page-no-toc:lg:flex'
|
||||
)}
|
||||
>
|
||||
{header && header}
|
||||
<div // The actual sidebar, either shown with a filled bg or transparent.
|
||||
className={tcls(
|
||||
'lg:-ms-5',
|
||||
'relative flex flex-grow flex-col overflow-hidden border-tint-subtle',
|
||||
|
||||
'sidebar-filled:bg-tint-subtle',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-gradient_&]:border',
|
||||
'page-no-toc:!bg-transparent',
|
||||
'page-no-toc:!border-none',
|
||||
|
||||
'sidebar-filled:rounded-xl',
|
||||
'straight-corners:rounded-none',
|
||||
'[html.sidebar-filled.circular-corners_&]:page-has-toc:rounded-3xl'
|
||||
)}
|
||||
>
|
||||
{innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
|
||||
<TOCScrollContainer // The scrollview inside the sidebar
|
||||
className={tcls(
|
||||
'flex flex-grow flex-col p-2',
|
||||
customization.trademark.enabled && 'lg:pb-20',
|
||||
'lg:gutter-stable overflow-y-auto',
|
||||
'[&::-webkit-scrollbar]:bg-transparent',
|
||||
'[&::-webkit-scrollbar-thumb]:bg-transparent',
|
||||
'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle',
|
||||
'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7',
|
||||
'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8'
|
||||
)}
|
||||
{open ? (
|
||||
<Button
|
||||
variant="blank"
|
||||
icon="close"
|
||||
iconOnly
|
||||
size="medium"
|
||||
autoFocus={false}
|
||||
className="absolute top-2 right-2 z-50 aspect-square size-[calc(2.25rem+1px)] justify-center bg-transparent text-tint opacity-8 shadow-none ring-transparent lg:hidden"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<PagesList
|
||||
pages={pages}
|
||||
style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
|
||||
/>
|
||||
{customization.trademark.enabled ? (
|
||||
<Trademark
|
||||
space={space}
|
||||
customization={customization}
|
||||
placement={SiteInsightsTrademarkPlacement.Sidebar}
|
||||
/>
|
||||
) : null}
|
||||
</TOCScrollContainer>
|
||||
</div>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{children}
|
||||
</aside>
|
||||
<TableOfContentsScript />
|
||||
<MobileMenuScript />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ export function Trademark(props: {
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'relative',
|
||||
'z-[2]',
|
||||
'lg:absolute',
|
||||
'absolute',
|
||||
|
||||
'left-0',
|
||||
'left-2',
|
||||
'right-2',
|
||||
'bottom-0',
|
||||
'lg:left-0',
|
||||
|
||||
'pointer-events-none',
|
||||
'sidebar-filled:pl-2',
|
||||
@@ -44,8 +44,7 @@ export function Trademark(props: {
|
||||
'straight-corners:rounded-none',
|
||||
'circular-corners:rounded-2xl',
|
||||
|
||||
'before:hidden',
|
||||
'lg:before:block',
|
||||
'before:block',
|
||||
'before:content-[""]',
|
||||
'before:absolute',
|
||||
'before:inset-x-0',
|
||||
@@ -111,8 +110,7 @@ export function TrademarkLink(props: {
|
||||
'hover:bg-tint',
|
||||
'hover:text-tint-strong',
|
||||
|
||||
'ring-2',
|
||||
'lg:ring-1',
|
||||
'ring-1',
|
||||
'ring-inset',
|
||||
'ring-tint-subtle',
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ export { TableOfContents } from './TableOfContents';
|
||||
export { PagesList } from './PagesList';
|
||||
export { TOCScrollContainer } from './TOCScroller';
|
||||
export { Trademark } from './Trademark';
|
||||
export { TOCScrollContent } from './TOCScrollContent';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createContext, useCallback, useContext, useState } from 'react';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
|
||||
import { assert } from 'ts-essentials';
|
||||
import { Link, type LinkInsightsProps } from '.';
|
||||
@@ -34,8 +35,14 @@ export function DropdownMenu(props: {
|
||||
children: React.ReactNode;
|
||||
/** Custom styles */
|
||||
className?: ClassValue;
|
||||
/** Open the dropdown on hover */
|
||||
/** Open the dropdown on hover
|
||||
* @default false
|
||||
*/
|
||||
openOnHover?: boolean;
|
||||
/** Whether to render the dropdown menu in a portal
|
||||
* @default true
|
||||
*/
|
||||
withPortal?: boolean;
|
||||
/**
|
||||
* Side of the dropdown
|
||||
* @default "bottom"
|
||||
@@ -54,12 +61,15 @@ export function DropdownMenu(props: {
|
||||
openOnHover = false,
|
||||
side = 'bottom',
|
||||
align = 'start',
|
||||
withPortal = true,
|
||||
} = props;
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const isOpen = openOnHover ? open || hovered : open;
|
||||
|
||||
const Portal = withPortal ? RadixDropdownMenu.Portal : Slot;
|
||||
|
||||
return (
|
||||
<DropdownMenuContext.Provider value={{ open: isOpen, setOpen }}>
|
||||
<RadixDropdownMenu.Root modal={false} open={isOpen} onOpenChange={setOpen}>
|
||||
@@ -73,7 +83,7 @@ export function DropdownMenu(props: {
|
||||
{button}
|
||||
</RadixDropdownMenu.Trigger>
|
||||
|
||||
<RadixDropdownMenu.Portal>
|
||||
<Portal>
|
||||
<RadixDropdownMenu.Content
|
||||
data-testid="dropdown-menu"
|
||||
hideWhenDetached
|
||||
@@ -93,7 +103,7 @@ export function DropdownMenu(props: {
|
||||
{children}
|
||||
</div>
|
||||
</RadixDropdownMenu.Content>
|
||||
</RadixDropdownMenu.Portal>
|
||||
</Portal>
|
||||
</RadixDropdownMenu.Root>
|
||||
</DropdownMenuContext.Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user