Files
docmost/apps/client/src/features/public-space/components/docs/docs-shell.tsx
T
Philip Okugbe 27fa9af959 fix: public page print (#2476)
* fix pdf-print for public pages

* fix print in dark mode
2026-09-05 17:28:45 +01:00

224 lines
7.4 KiB
TypeScript

import React from "react";
import { ActionIcon, Drawer, Tooltip } from "@mantine/core";
import { Link } from "react-router-dom";
import { useAtom } from "jotai";
import { useTranslation } from "react-i18next";
import { IconList, IconMenu2 } from "@tabler/icons-react";
import clsx from "clsx";
import {
docsMobileSidebarAtom,
docsMobileTocAtom,
} from "@/features/public-space/atoms/public-space-atoms.ts";
import {
DocsSurface,
DocsSurfaceProvider,
} from "@/features/public-space/components/docs/docs-surface-context.tsx";
import DocsSidebarTree from "@/features/public-space/components/docs/docs-sidebar-tree.tsx";
import DocsToc from "@/features/public-space/components/docs/docs-toc.tsx";
import DocsEditPage from "@/features/public-space/components/docs/docs-edit-page.tsx";
import DocsCopyPage from "@/features/public-space/components/docs/docs-copy-page.tsx";
import DocsSearchButton from "@/features/public-space/components/docs/docs-search-button.tsx";
import DocsThemeToggle from "@/features/public-space/components/docs/docs-theme-toggle.tsx";
import DocsFooterBranding from "@/features/public-space/components/docs/docs-footer-branding.tsx";
import { MAIN_CONTENT_ID, SkipToMain } from "@/components/ui/skip-to-main.tsx";
import { SearchMobileControl } from "@/features/search/components/search-control.tsx";
import styles from "./docs.module.css";
const MemoizedDocsSidebarTree = React.memo(DocsSidebarTree);
const MANTINE_COLOR_SCHEME_ATTRIBUTE = "data-mantine-color-scheme";
const DOCS_PRINT_COLOR_SCHEME_ATTRIBUTE = "data-docs-print-color-scheme";
type DocsShellProps = {
surface: DocsSurface;
onSearchOpen?: () => void;
searchSpotlight?: React.ReactNode;
children: React.ReactNode;
};
export default function DocsShell({
surface,
onSearchOpen,
searchSpotlight,
children,
}: DocsShellProps) {
const { t } = useTranslation();
const { hasSidebar, siteName, homeUrl, showBranding, showEditPage } = surface;
const [mobileSidebarOpen, setMobileSidebarOpen] = useAtom(
docsMobileSidebarAtom,
);
const [mobileTocOpen, setMobileTocOpen] = useAtom(docsMobileTocAtom);
React.useEffect(() => {
const root = document.documentElement;
let previousColorScheme: string | null = null;
let isPrinting = false;
const restoreColorScheme = () => {
if (!isPrinting) return;
if (previousColorScheme === null) {
root.removeAttribute(MANTINE_COLOR_SCHEME_ATTRIBUTE);
} else {
root.setAttribute(MANTINE_COLOR_SCHEME_ATTRIBUTE, previousColorScheme);
}
root.removeAttribute(DOCS_PRINT_COLOR_SCHEME_ATTRIBUTE);
isPrinting = false;
};
const useLightPrintTheme = () => {
if (isPrinting) return;
previousColorScheme = root.getAttribute(MANTINE_COLOR_SCHEME_ATTRIBUTE);
root.setAttribute(
DOCS_PRINT_COLOR_SCHEME_ATTRIBUTE,
previousColorScheme ?? "light",
);
root.setAttribute(MANTINE_COLOR_SCHEME_ATTRIBUTE, "light");
isPrinting = true;
};
window.addEventListener("beforeprint", useLightPrintTheme);
window.addEventListener("afterprint", restoreColorScheme);
return () => {
window.removeEventListener("beforeprint", useLightPrintTheme);
window.removeEventListener("afterprint", restoreColorScheme);
restoreColorScheme();
};
}, []);
return (
<DocsSurfaceProvider value={surface}>
<div className={clsx(styles.root, "public-typography")}>
<SkipToMain />
<header className={styles.header}>
<div className={styles.headerInner}>
<div className={styles.headerLeft}>
{hasSidebar && (
<Tooltip label={t("Toggle sidebar")}>
<ActionIcon
variant="subtle"
className={clsx(styles.headerAction, styles.sidebarToggle)}
size="md"
onClick={() => setMobileSidebarOpen((value) => !value)}
aria-label={t("Toggle sidebar")}
aria-expanded={mobileSidebarOpen}
>
<IconMenu2 size={18} stroke={2} />
</ActionIcon>
</Tooltip>
)}
{!hasSidebar && siteName && homeUrl && (
<Link to={homeUrl} className={styles.headerSpaceName}>
{siteName}
</Link>
)}
</div>
<div className={styles.headerCenter}>
{onSearchOpen && (
<div className={styles.searchSlot}>
<DocsSearchButton onClick={onSearchOpen} />
</div>
)}
</div>
<div className={styles.headerRight}>
{onSearchOpen && (
<span className={styles.mobileOnly}>
<SearchMobileControl onSearch={onSearchOpen} />
</span>
)}
<DocsThemeToggle />
</div>
</div>
</header>
<div className={styles.body}>
<nav
className={styles.sidebar}
data-hidden={!hasSidebar || undefined}
aria-label={t("Pages")}
aria-hidden={!hasSidebar || undefined}
>
{hasSidebar && (
<>
{siteName && homeUrl && (
<>
<Link to={homeUrl} className={styles.sidebarTitle}>
{siteName}
</Link>
<div className={styles.sidebarDivider} aria-hidden />
</>
)}
<div className={styles.sidebarScroll}>
<MemoizedDocsSidebarTree />
</div>
</>
)}
</nav>
<main className={styles.main} id={MAIN_CONTENT_ID} tabIndex={-1}>
<div className={styles.article}>
<div className={styles.articleActions}>
<DocsCopyPage />
<span className={styles.tocOverlayControl}>
<Tooltip label={t("Table of contents")} withArrow>
<ActionIcon
variant="subtle"
className={styles.headerAction}
onClick={() => setMobileTocOpen(true)}
size="md"
aria-label={t("Table of contents")}
>
<IconList size={18} stroke={2} />
</ActionIcon>
</Tooltip>
</span>
</div>
{children}
{showBranding && (
<DocsFooterBranding refSource={surface.brandingRef} />
)}
</div>
</main>
<aside className={styles.toc} aria-label={t("On this page")}>
<DocsToc />
{showEditPage && <DocsEditPage />}
</aside>
</div>
<Drawer
opened={mobileSidebarOpen}
onClose={() => setMobileSidebarOpen(false)}
title={siteName}
size={300}
padding="sm"
>
<div className={styles.drawerTree}>
{hasSidebar && <MemoizedDocsSidebarTree />}
</div>
</Drawer>
<Drawer
opened={mobileTocOpen}
onClose={() => setMobileTocOpen(false)}
position="right"
size={300}
padding="md"
>
<DocsToc />
{showEditPage && <DocsEditPage />}
</Drawer>
{searchSpotlight}
</div>
</DocsSurfaceProvider>
);
}