diff --git a/.changeset/tidy-pianos-argue.md b/.changeset/tidy-pianos-argue.md new file mode 100644 index 000000000..b66a1d6d7 --- /dev/null +++ b/.changeset/tidy-pianos-argue.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Load the admin toolbar and its CSS lazily so published pages no longer ship a render-blocking stylesheet for admin-only UI. diff --git a/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx b/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx index 6d933a9cf..889dc5a3c 100644 --- a/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx +++ b/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx @@ -1,4 +1,4 @@ -import { AdminToolbarClient } from './AdminToolbarClient'; +import { AdminToolbarLazy } from './AdminToolbarLazy'; import { getToolbarChangedPages } from './changedPages'; import type { AdminToolbarContext } from './types'; import type { GitBookSiteContext } from '@/lib/context'; @@ -62,5 +62,5 @@ export async function AdminToolbar(props: AdminToolbarProps) { changedPages, }; - return ; + return ; } diff --git a/packages/gitbook/src/components/AdminToolbar/AdminToolbarLazy.tsx b/packages/gitbook/src/components/AdminToolbar/AdminToolbarLazy.tsx new file mode 100644 index 000000000..18d7ce20e --- /dev/null +++ b/packages/gitbook/src/components/AdminToolbar/AdminToolbarLazy.tsx @@ -0,0 +1,17 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +import type { AdminToolbarClientProps } from './types'; + +// The toolbar only ever appears after hydration (`useToolbarVisibility` reads cookies and local +// storage), so nothing is lost by deferring it — and it takes its CSS modules out of the route's +// render-blocking stylesheets, where every published page was paying for them. +const AdminToolbarClient = dynamic( + () => import('./AdminToolbarClient').then((mod) => mod.AdminToolbarClient), + { ssr: false } +); + +export function AdminToolbarLazy(props: AdminToolbarClientProps) { + return ; +} diff --git a/packages/gitbook/src/components/AdminToolbar/index.ts b/packages/gitbook/src/components/AdminToolbar/index.ts index 490e4b6ae..3cbcc3774 100644 --- a/packages/gitbook/src/components/AdminToolbar/index.ts +++ b/packages/gitbook/src/components/AdminToolbar/index.ts @@ -1,6 +1,3 @@ +// Only the server entrypoint: re-exporting the client modules would pull their CSS modules back +// into the route's render-blocking stylesheets, defeating the lazy boundary. export * from './AdminToolbar'; -export * from './AdminToolbarClient'; -export * from './IframeWrapper'; -export * from './Toolbar'; -export * from './transitions'; -export * from './utils'; diff --git a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx index 76a99a229..12f17aab7 100644 --- a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx +++ b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx @@ -15,7 +15,6 @@ import { } from '../primitives/DropdownMenu'; import { SiteAuthLoginDropdownMenuItem } from '../SiteAuth/SiteAuthLoginLink'; import { HeaderLinkMoreDropdown } from './HeaderLinkMoreClient'; -import styles from './headerLinks.module.css'; import { isSiteAuthLoginHref } from '@/lib/auth-login-link'; import type { GitBookSiteContext } from '@/lib/context'; import { resolveContentRef } from '@/lib/references'; @@ -34,7 +33,7 @@ export function HeaderLinkMore(props: { const { label, links, context, socialAccounts } = props; return ( -
+
.button+.button]:-ml-2 z-20 ml-auto flex min-w-9 shrink grow @7xl:grow-0 items-center justify-end @4xl:gap-x-6 gap-x-4', style )} diff --git a/packages/gitbook/src/components/Header/headerLinks.module.css b/packages/gitbook/src/components/Header/headerLinks.module.css deleted file mode 100644 index 46eb0036a..000000000 --- a/packages/gitbook/src/components/Header/headerLinks.module.css +++ /dev/null @@ -1,80 +0,0 @@ -@container header (width < 1280px) { - .containerHeaderlinks { - container-type: inline-size; - container-name: headerlinks; - } -} - -.linkEllipsis { - display: none; - & div > a { - display: none; - } -} - -@container headerlinks (width < 150px) { - .containerHeaderlinks > :nth-child(n + 1) { - display: none; - } - .containerHeaderlinks > :nth-child(n + 1) ~ .linkEllipsis { - display: flex; - & div > *:nth-child(n + 1) { - display: flex; - } - } -} -@container headerlinks (width < 300px) { - .containerHeaderlinks > :nth-child(n + 2) { - display: none; - } - .containerHeaderlinks > :nth-child(n + 2) ~ .linkEllipsis { - display: flex; - & div > *:nth-child(n + 2) { - display: flex; - } - } -} -@container headerlinks (width < 450px) { - .containerHeaderlinks > :nth-child(n + 3) { - display: none; - } - .containerHeaderlinks > :nth-child(n + 3) ~ .linkEllipsis { - display: flex; - & div > *:nth-child(n + 3) { - display: flex; - } - } -} -@container headerlinks (width < 550px) { - .containerHeaderlinks > :nth-child(n + 4) { - display: none; - } - .containerHeaderlinks > :nth-child(n + 4) ~ .linkEllipsis { - display: flex; - & div > *:nth-child(n + 4) { - display: flex; - } - } -} -@container headerlinks (width < 650px) { - .containerHeaderlinks > :nth-child(n + 5) { - display: none; - } - .containerHeaderlinks > :nth-child(n + 5) ~ .linkEllipsis { - display: flex; - & div > *:nth-child(n + 5) { - display: flex; - } - } -} -@container headerlinks (width < 750px) { - .containerHeaderlinks > :nth-child(n + 6) { - display: none; - } - .containerHeaderlinks > :nth-child(n + 6) ~ .linkEllipsis { - display: flex; - & div > *:nth-child(n + 6) { - display: flex; - } - } -} diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css index 1c542fd45..2110b127a 100644 --- a/packages/gitbook/src/components/RootLayout/globals.css +++ b/packages/gitbook/src/components/RootLayout/globals.css @@ -552,3 +552,107 @@ html.dark .highlight-line.diff-deleted .highlight-line-content::before { .button-group > :is(button, a):not(:first-child):has(~ :is(button, a)) { @apply rounded-none!; } + +/* Zoomable images */ +html:has(.zoom-modal) { + /* stylelint-disable-next-line plugin/no-unsupported-browser-features -- single-value overflow is universal; doiuse flags the whole css-overflow feature */ + overflow: hidden; +} + +.zoom-img { + /* stylelint-disable-next-line plugin/no-unsupported-browser-features -- zoom cursors are irrelevant on the touch browsers doiuse flags */ + cursor: zoom-in; +} + +.zoom-image-active { + view-transition-name: zoom-image; +} + +.zoom-modal img { + view-transition-name: zoom-image; + /* stylelint-disable-next-line plugin/no-unsupported-browser-features -- zoom cursors are irrelevant on the touch browsers doiuse flags */ + cursor: zoom-out; +} + +/* Header links collapse into the "more" dropdown one by one as the header narrows. Each breakpoint + hides one more link and reveals the ellipsis holding the ones that no longer fit. */ +@container header (width < 1280px) { + .header-links { + container-type: inline-size; + container-name: headerlinks; + } +} + +.header-links-ellipsis { + display: none; + & div > a { + display: none; + } +} + +@container headerlinks (width < 150px) { + .header-links > :nth-child(n + 1) { + display: none; + } + .header-links > :nth-child(n + 1) ~ .header-links-ellipsis { + display: flex; + & div > *:nth-child(n + 1) { + display: flex; + } + } +} +@container headerlinks (width < 300px) { + .header-links > :nth-child(n + 2) { + display: none; + } + .header-links > :nth-child(n + 2) ~ .header-links-ellipsis { + display: flex; + & div > *:nth-child(n + 2) { + display: flex; + } + } +} +@container headerlinks (width < 450px) { + .header-links > :nth-child(n + 3) { + display: none; + } + .header-links > :nth-child(n + 3) ~ .header-links-ellipsis { + display: flex; + & div > *:nth-child(n + 3) { + display: flex; + } + } +} +@container headerlinks (width < 550px) { + .header-links > :nth-child(n + 4) { + display: none; + } + .header-links > :nth-child(n + 4) ~ .header-links-ellipsis { + display: flex; + & div > *:nth-child(n + 4) { + display: flex; + } + } +} +@container headerlinks (width < 650px) { + .header-links > :nth-child(n + 5) { + display: none; + } + .header-links > :nth-child(n + 5) ~ .header-links-ellipsis { + display: flex; + & div > *:nth-child(n + 5) { + display: flex; + } + } +} +@container headerlinks (width < 750px) { + .header-links > :nth-child(n + 6) { + display: none; + } + .header-links > :nth-child(n + 6) ~ .header-links-ellipsis { + display: flex; + & div > *:nth-child(n + 6) { + display: flex; + } + } +} diff --git a/packages/gitbook/src/components/utils/ZoomImage.module.css b/packages/gitbook/src/components/utils/ZoomImage.module.css deleted file mode 100644 index 324a9a22b..000000000 --- a/packages/gitbook/src/components/utils/ZoomImage.module.css +++ /dev/null @@ -1,20 +0,0 @@ -html:has(.zoomModal) { - overflow: hidden; -} - -.zoomImg { - cursor: zoom-in; -} - -.zoomImageActive { - view-transition-name: zoom-image; -} - -.zoomModal { - /** Unstyled */ -} - -.zoomModal img { - view-transition-name: zoom-image; - cursor: zoom-out; -} diff --git a/packages/gitbook/src/components/utils/ZoomImage.tsx b/packages/gitbook/src/components/utils/ZoomImage.tsx index 0c44fb6f0..4d294f81a 100644 --- a/packages/gitbook/src/components/utils/ZoomImage.tsx +++ b/packages/gitbook/src/components/utils/ZoomImage.tsx @@ -6,7 +6,6 @@ import ReactDOM from 'react-dom'; import { Icon } from '@gitbook/icons'; -import styles from './ZoomImage.module.css'; import { tcls } from '@/lib/tailwind'; const PADDING = 32; // Padding around the image in the modal, in pixels @@ -159,8 +158,8 @@ export function ZoomImage( }} className={classNames( props.className, - zoomable ? styles.zoomImg : null, - active ? styles.zoomImageActive : null + zoomable ? 'zoom-img' : null, + active ? 'zoom-image-active' : null )} /> )} @@ -200,7 +199,7 @@ function ZoomImageModal(props: {