From e39a4882473a1505f83276d4993cb21bd91a4315 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Thu, 11 Jul 2024 14:48:22 +0200 Subject: [PATCH] [EUI+] Implement document content styles (#7872) --- packages/docusaurus-theme/package.json | 1 + .../src/theme/DocItem/Content/index.tsx | 72 +++++++++++----- .../src/theme/DocItem/Footer/index.tsx | 51 ++++++++++++ .../src/theme/DocItem/Layout/index.tsx | 82 +++++++++++++++++++ .../src/theme/DocItem/Metadata/index.tsx | 15 ++++ .../src/theme/DocItem/Paginator/index.tsx | 12 +++ .../src/theme/DocItem/TOC/Desktop/index.tsx | 17 ++++ .../src/theme/DocItem/TOC/Mobile/index.tsx | 33 ++++++++ .../src/theme/DocItem/index.tsx | 22 +++++ .../src/theme/DocPaginator/index.tsx | 55 +++++++++++++ .../src/theme/EditThisPage/index.tsx | 44 ++++++++++ .../src/theme/PaginatorNavLink/index.tsx | 63 ++++++++++++++ .../docusaurus-theme/src/theme/Root.styles.ts | 31 ++++++- .../docusaurus-theme/src/theme/theme.d.ts | 43 ++++++++++ yarn.lock | 3 +- 15 files changed, 521 insertions(+), 23 deletions(-) create mode 100644 packages/docusaurus-theme/src/theme/DocItem/Footer/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/DocItem/Layout/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/DocItem/Metadata/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/DocItem/Paginator/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/DocItem/TOC/Desktop/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/DocItem/TOC/Mobile/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/DocItem/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/DocPaginator/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/EditThisPage/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/PaginatorNavLink/index.tsx create mode 100644 packages/docusaurus-theme/src/theme/theme.d.ts diff --git a/packages/docusaurus-theme/package.json b/packages/docusaurus-theme/package.json index 19e71834cdf..2388cf0e71e 100644 --- a/packages/docusaurus-theme/package.json +++ b/packages/docusaurus-theme/package.json @@ -30,6 +30,7 @@ "dependencies": { "@docusaurus/core": "^3.4.0", "@docusaurus/module-type-aliases": "^3.4.0", + "@docusaurus/theme-common": "^3.4.0", "@docusaurus/utils-validation": "^3.4.0", "@elastic/datemath": "^5.0.3", "@elastic/eui": "94.5.0", diff --git a/packages/docusaurus-theme/src/theme/DocItem/Content/index.tsx b/packages/docusaurus-theme/src/theme/DocItem/Content/index.tsx index b5cf7bfe378..0663381b932 100644 --- a/packages/docusaurus-theme/src/theme/DocItem/Content/index.tsx +++ b/packages/docusaurus-theme/src/theme/DocItem/Content/index.tsx @@ -1,33 +1,67 @@ -import OriginalContent from '@theme-init/DocItem/Content'; -import type ContentType from '@theme-init/DocItem/Content'; -import type { WrapperProps } from '@docusaurus/types'; -import { useEuiMemoizedStyles, UseEuiTheme } from '@elastic/eui'; +import { ReactNode } from 'react'; +import clsx from 'clsx'; import { css } from '@emotion/react'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import { useDoc } from '@docusaurus/theme-common/internal'; +import MDXContent from '@theme-original/MDXContent'; +import { + EuiHorizontalRule, + useEuiMemoizedStyles, + UseEuiTheme, +} from '@elastic/eui'; +import { Props } from '@theme-original/DocItem/Content'; -type Props = WrapperProps; +import Heading from '../../MDXComponents/Heading'; + +/** + Title can be declared inside md content or declared through + front matter and added manually. To make both cases consistent, + the added title is added under the same div.markdown block + See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120 + + We render a "synthetic title" if: + - user doesn't ask to hide it with front matter + - the markdown content does not already contain a top-level h1 heading +*/ +function useSyntheticTitle(): string | null { + const { metadata, frontMatter, contentTitle } = useDoc(); + const shouldRender = + !frontMatter.hide_title && typeof contentTitle === 'undefined'; + if (!shouldRender) { + return null; + } + return metadata.title; +} const getContentStyles = ({ euiTheme }: UseEuiTheme) => { return { - content: css` - // required specificity to apply styles - .markdown header > h1 { - --ifm-h1-font-size: 2.86rem; - --ifm-h1-vertical-rhythm-bottom: 1.486; + header: css` + // required specificity to override docusaurus styles + & > h1.euiTitle { + --ifm-h1-font-size: var(--eui-font-size-xxl); + --ifm-h1-vertical-rhythm-bottom: 1.2; + + line-height: 2.8rem; } `, }; }; -/* OriginalContent holds the document title and markdown content -NOTE: ejecting this results in an error due to using useDoc() hook outside of DocProvider -https://github.com/facebook/docusaurus/blob/main/packages/docusaurus-theme-classic/src/theme/DocItem/Content/index.tsx */ -const Content = (props: Props): JSX.Element => { +export default function DocItemContent({ children }: Props): JSX.Element { + const syntheticTitle = useSyntheticTitle(); const styles = useEuiMemoizedStyles(getContentStyles); + return ( -
- +
+ {syntheticTitle && ( + <> +
+ {syntheticTitle} +
+ + + )} + {children}
); -}; - -export default Content; +} diff --git a/packages/docusaurus-theme/src/theme/DocItem/Footer/index.tsx b/packages/docusaurus-theme/src/theme/DocItem/Footer/index.tsx new file mode 100644 index 00000000000..893c6371c59 --- /dev/null +++ b/packages/docusaurus-theme/src/theme/DocItem/Footer/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import clsx from 'clsx'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import { useDoc } from '@docusaurus/theme-common/internal'; +import TagsListInline from '@theme-original/TagsListInline'; + +import EditMetaRow from '@theme-original/EditMetaRow'; + +export default function DocItemFooter(): JSX.Element | null { + const { metadata } = useDoc(); + const { editUrl, lastUpdatedAt, lastUpdatedBy, tags } = metadata; + + const canDisplayTagsRow = tags.length > 0; + const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy); + + const canDisplayFooter = canDisplayTagsRow || canDisplayEditMetaRow; + + if (!canDisplayFooter) { + return null; + } + + return ( +
+ {canDisplayTagsRow && ( +
+
+ +
+
+ )} + {canDisplayEditMetaRow && ( + + )} +
+ ); +} diff --git a/packages/docusaurus-theme/src/theme/DocItem/Layout/index.tsx b/packages/docusaurus-theme/src/theme/DocItem/Layout/index.tsx new file mode 100644 index 00000000000..151fd4dc30f --- /dev/null +++ b/packages/docusaurus-theme/src/theme/DocItem/Layout/index.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { useWindowSize } from '@docusaurus/theme-common'; +import { useDoc } from '@docusaurus/theme-common/internal'; +import DocItemPaginator from '@theme-original/DocItem/Paginator'; +import DocVersionBanner from '@theme-original/DocVersionBanner'; +import DocVersionBadge from '@theme-original/DocVersionBadge'; +import DocBreadcrumbs from '@theme-original/DocBreadcrumbs'; +import Unlisted from '@theme-original/Unlisted'; +import * as Props from '@theme-original/DocItem/Layout'; +import { EuiHorizontalRule } from '@elastic/eui'; + +import DocItemContent from '../Content'; +import DocItemTOCMobile from '../TOC/Mobile'; +import DocItemTOCDesktop from '../TOC/Desktop'; +import DocItemFooter from '../Footer'; + +// converted from css modules to emotion +const layoutStyles = { + docItemContainer: css` + & header + *, + & article > *:first-child { + margin-top: 0; + } + `, + docItemCol: css` + @media (min-width: 997px) { + max-width: 75% !important; + } + `, +}; + +/** + * Decide if the toc should be rendered, on mobile or desktop viewports + */ +function useDocTOC() { + const { frontMatter, toc } = useDoc(); + const windowSize = useWindowSize(); + + const hidden = frontMatter.hide_table_of_contents ?? false; + const canRender = !hidden && toc.length > 0; + + const mobile = canRender ? : undefined; + + const desktop = + canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? ( + + ) : undefined; + + return { + hidden, + mobile, + desktop, + }; +} + +export default function DocItemLayout({ children }: typeof Props): JSX.Element { + const docTOC = useDocTOC(); + const { + metadata: { unlisted }, + } = useDoc(); + return ( +
+
+ {unlisted && } + +
+
+ + + {docTOC.mobile} + {children} + +
+ + +
+
+ {docTOC.desktop &&
{docTOC.desktop}
} +
+ ); +} diff --git a/packages/docusaurus-theme/src/theme/DocItem/Metadata/index.tsx b/packages/docusaurus-theme/src/theme/DocItem/Metadata/index.tsx new file mode 100644 index 00000000000..cd7b9b5cbcf --- /dev/null +++ b/packages/docusaurus-theme/src/theme/DocItem/Metadata/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import {PageMetadata} from '@docusaurus/theme-common'; +import {useDoc} from '@docusaurus/theme-common/internal'; + +export default function DocItemMetadata(): JSX.Element { + const {metadata, frontMatter, assets} = useDoc(); + return ( + + ); +} diff --git a/packages/docusaurus-theme/src/theme/DocItem/Paginator/index.tsx b/packages/docusaurus-theme/src/theme/DocItem/Paginator/index.tsx new file mode 100644 index 00000000000..994cf035e19 --- /dev/null +++ b/packages/docusaurus-theme/src/theme/DocItem/Paginator/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { useDoc } from '@docusaurus/theme-common/internal'; +import DocPaginator from '@theme-original/DocPaginator'; + +/** + * This extra component is needed, because should remain generic. + * DocPaginator is used in non-docs contexts too: generated-index pages... + */ +export default function DocItemPaginator(): JSX.Element { + const { metadata } = useDoc(); + return ; +} diff --git a/packages/docusaurus-theme/src/theme/DocItem/TOC/Desktop/index.tsx b/packages/docusaurus-theme/src/theme/DocItem/TOC/Desktop/index.tsx new file mode 100644 index 00000000000..abee5787f78 --- /dev/null +++ b/packages/docusaurus-theme/src/theme/DocItem/TOC/Desktop/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import { useDoc } from '@docusaurus/theme-common/internal'; + +import TOC from '@theme-original/TOC'; + +export default function DocItemTOCDesktop(): JSX.Element { + const { toc, frontMatter } = useDoc(); + return ( + + ); +} diff --git a/packages/docusaurus-theme/src/theme/DocItem/TOC/Mobile/index.tsx b/packages/docusaurus-theme/src/theme/DocItem/TOC/Mobile/index.tsx new file mode 100644 index 00000000000..233033c74b3 --- /dev/null +++ b/packages/docusaurus-theme/src/theme/DocItem/TOC/Mobile/index.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import { useDoc } from '@docusaurus/theme-common/internal'; + +import TOCCollapsible from '@theme-original/TOCCollapsible'; + +// converted from css modules to emotion +const tocStyles = { + tocMobile: css` + @media (min-width: 997px) { + /* Prevent hydration FOUC, as the mobile TOC needs to be server-rendered */ + display: none; + } + + @media print { + display: none; + } + `, +}; + +export default function DocItemTOCMobile(): JSX.Element { + const { toc, frontMatter } = useDoc(); + return ( + + ); +} diff --git a/packages/docusaurus-theme/src/theme/DocItem/index.tsx b/packages/docusaurus-theme/src/theme/DocItem/index.tsx new file mode 100644 index 00000000000..872730a0cb6 --- /dev/null +++ b/packages/docusaurus-theme/src/theme/DocItem/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { HtmlClassNameProvider } from '@docusaurus/theme-common'; +import { DocProvider } from '@docusaurus/theme-common/internal'; +import DocItemMetadata from '@theme-original/DocItem/Metadata'; +import type { Props } from '@theme/DocItem'; + +import DocItemLayout from './Layout'; + +export default function DocItem(props: Props): JSX.Element { + const docHtmlClassName = `docs-doc-id-${props.content.metadata.id}`; + const MDXComponent = props.content; + return ( + + + + + + + + + ); +} diff --git a/packages/docusaurus-theme/src/theme/DocPaginator/index.tsx b/packages/docusaurus-theme/src/theme/DocPaginator/index.tsx new file mode 100644 index 00000000000..197646ae70f --- /dev/null +++ b/packages/docusaurus-theme/src/theme/DocPaginator/index.tsx @@ -0,0 +1,55 @@ +import { css } from '@emotion/react'; +import Translate, { translate } from '@docusaurus/Translate'; +import PaginatorNavLink from '@theme-original/PaginatorNavLink'; +import type { Props } from '@theme-original/DocPaginator'; + +const styles = { + pagination: css` + // docusaurus reset, we add spacing via the + // horizontal rule in Layout instead + margin-top: 0; + `, +}; + +export default function DocPaginator(props: Props): JSX.Element { + const { previous, next } = props; + return ( + + ); +} diff --git a/packages/docusaurus-theme/src/theme/EditThisPage/index.tsx b/packages/docusaurus-theme/src/theme/EditThisPage/index.tsx new file mode 100644 index 00000000000..15ad94fc05f --- /dev/null +++ b/packages/docusaurus-theme/src/theme/EditThisPage/index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { css } from '@emotion/react'; +import Translate from '@docusaurus/Translate'; +import type { Props } from '@theme-original/EditThisPage'; +import { EuiButton, useEuiMemoizedStyles, UseEuiTheme } from '@elastic/eui'; +// @ts-ignore - eui only has module declarations for '@elastic/eui/src/themes/amsterdam/global_styling/mixins/button' +// but importing from /src results in "Module not found" error +import { euiButtonColor } from '@elastic/eui/lib/themes/amsterdam/global_styling/mixins/button'; + +const getStyles = (theme: UseEuiTheme) => { + const { euiTheme } = theme; + const buttonColor = euiButtonColor(theme, 'primary'); + + return { + editPage: css` + // overriding Docusaurus link hover styles to preserve button styles + --ifm-link-hover-color: ${buttonColor.color}; + + border: ${euiTheme.border.thin}; + border-color: ${euiTheme.colors.primary}; + `, + }; +}; + +export default function EditThisPage({ editUrl, ...rest }: Props): JSX.Element { + const styles = useEuiMemoizedStyles(getStyles); + + return ( + + + Edit this page + + + ); +} diff --git a/packages/docusaurus-theme/src/theme/PaginatorNavLink/index.tsx b/packages/docusaurus-theme/src/theme/PaginatorNavLink/index.tsx new file mode 100644 index 00000000000..bbd57819afc --- /dev/null +++ b/packages/docusaurus-theme/src/theme/PaginatorNavLink/index.tsx @@ -0,0 +1,63 @@ +import clsx from 'clsx'; +import { css } from '@emotion/react'; +import Link from '@docusaurus/Link'; +import type { Props } from '@theme-original/PaginatorNavLink'; +import { useEuiMemoizedStyles, UseEuiTheme, EuiIcon } from '@elastic/eui'; + +const getStyles = ({ euiTheme }: UseEuiTheme) => { + return { + link: css` + --ifm-pagination-nav-color-hover: ${euiTheme.colors.primary}; + + border: ${euiTheme.border.thin}; + + .pagination-nav__label { + display: flex; + align-items: center; + + font-size: var(--eui-font-size-m); + line-height: var(--eui-line-height-l); + } + + &.pagination-nav__link--next .pagination-nav__label { + justify-content: flex-end; + } + + .pagination-nav__label::before, + .pagination-nav__label::after { + content: ''; + } + + .pagination-nav__sublabel { + margin-block-end: ${euiTheme.size.xs}; + font-size: var(--eui-font-size-s); + line-height: var(--eui-line-height-l); + } + `, + }; +}; + +export default function PaginatorNavLink(props: Props): JSX.Element { + const { permalink, title, subLabel, isNext } = props; + const isPrev = !isNext; + const styles = useEuiMemoizedStyles(getStyles); + + return ( + + {subLabel &&
{subLabel}
} + +
+ {isPrev && } + {title} + {isNext && } +
+ + ); +} diff --git a/packages/docusaurus-theme/src/theme/Root.styles.ts b/packages/docusaurus-theme/src/theme/Root.styles.ts index d64c75ef10c..94883648bda 100644 --- a/packages/docusaurus-theme/src/theme/Root.styles.ts +++ b/packages/docusaurus-theme/src/theme/Root.styles.ts @@ -1,10 +1,13 @@ import { + euiFontSizeFromScale, euiLineHeightFromBaseline, useEuiBackgroundColor, UseEuiTheme, } from '@elastic/eui'; // override docusaurus variables as needed +// NOTE: we use define variables with style calculations here +// on the global level to reduce how often they are called export const getGlobalStyles = ({ euiTheme }: UseEuiTheme) => { const { font, base, colors, size } = euiTheme; const fontBodyScale = font.scale[font.body.scale]; @@ -16,7 +19,16 @@ export const getGlobalStyles = ({ euiTheme }: UseEuiTheme) => { fontWeight: font.weight[font.body.weight], }; - const lineHeightL = '1.75rem'; + const fontSizeXXL = euiFontSizeFromScale('xxl', euiTheme); + const fontSizeXL = euiFontSizeFromScale('xl', euiTheme); + const fontSizeL = euiFontSizeFromScale('l', euiTheme); + const fontSizeM = euiFontSizeFromScale('m', euiTheme); + const fontSizeS = euiFontSizeFromScale('s', euiTheme); + const fontSizeXS = euiFontSizeFromScale('xs', euiTheme); + const fontSizeXXS = euiFontSizeFromScale('xxs', euiTheme); + + const lineHeightXL = '1.75rem'; + const lineHeightL = '1.5rem'; const lineHeightM = euiLineHeightFromBaseline('s', euiTheme); const lineHeightS = euiLineHeightFromBaseline('xs', euiTheme); const lineHeightXS = '1.33rem'; @@ -34,18 +46,31 @@ export const getGlobalStyles = ({ euiTheme }: UseEuiTheme) => { /* Docusaurus theme variables */ --ifm-background-color: ${colors.body}; --ifm-font-color-base: ${colors.text}; + --ifm-link-color: ${colors.link}; + --ifm-link-hover-color: ${colors.link}; } :root { /* EUI theme variables */ - --eui-line-height-base: ${lineHeightL}; + --eui-font-size-base: ${fontBase.fontSize}; + --eui-font-size-xxl: ${fontSizeXXL}; + --eui-font-size-xl: ${fontSizeXL}; + --eui-font-size-l: ${fontSizeL}; + --eui-font-size-m: ${fontSizeM}; + --eui-font-size-s: ${fontSizeS}; + --eui-font-size-xs: ${fontSizeXS}; + --eui-font-size-xxs: ${fontSizeXXS}; + + --eui-line-height-base: ${lineHeightXL}; + --eui-line-height-xl: ${lineHeightXL}; + --eui-line-height-l: ${lineHeightL}; --eui-line-height-m: ${lineHeightM}; --eui-line-height-s: ${lineHeightS}; --eui-line-height-xs: ${lineHeightXS}; /* Docusaurus theme variables */ --ifm-font-family-base: ${fontBase.fontFamily}; - --ifm-font-size-base: ${fontBase.fontSize}; + --ifm-font-size-base: var(--eui-font-size-base); --ifm-font-weight-base: ${fontBase.fontWeight}; --ifm-line-height-base: var(--eui-line-height-base); diff --git a/packages/docusaurus-theme/src/theme/theme.d.ts b/packages/docusaurus-theme/src/theme/theme.d.ts new file mode 100644 index 00000000000..dbc0c6d4142 --- /dev/null +++ b/packages/docusaurus-theme/src/theme/theme.d.ts @@ -0,0 +1,43 @@ +// Prop types are declared for @theme scope in @docusaurus/theme-classic +// but when using swizzle --eject the type import is not available. +// we re-declare the needed types here to ensure type checking +// NOTE: when using swizzle --wrap there is the approach to do the following: +// type Props = WrapperProps but it will result in `any` type + +// original: https://github.com/facebook/docusaurus/blob/d2bb74a8fd4ee92fc7ff806657dbb35de1de845f/packages/docusaurus-theme-classic/src/theme-classic.d.ts#L560 +declare module '@theme-original/DocItem/Content' { + export interface Props { + readonly children: JSX.Element; + } +} + +// original: https://github.com/facebook/docusaurus/blob/d2bb74a8fd4ee92fc7ff806657dbb35de1de845f/packages/docusaurus-theme-classic/src/theme-classic.d.ts#L554 +declare module '@theme-original/DocPaginator' { + import type { PropNavigation } from '@docusaurus/plugin-content-docs'; + + export interface Props extends PropNavigation {} + + export default function DocPaginator(props: Props): JSX.Element; +} + +// original: https://github.com/facebook/docusaurus/blob/d2bb74a8fd4ee92fc7ff806657dbb35de1de845f/packages/docusaurus-theme-classic/src/theme-classic.d.ts#L1252 +declare module '@theme-original/PaginatorNavLink' { + import type { ReactNode } from 'react'; + import type { PropNavigationLink } from '@docusaurus/plugin-content-docs'; + + export interface Props extends Omit { + readonly title: ReactNode; + readonly subLabel?: JSX.Element; + readonly isNext?: boolean; + } + + export default function PaginatorNavLink(props: Props): JSX.Element; +} + +// original: https://github.com/facebook/docusaurus/blob/fa743c81defd24e22eae45c81bd79eb8ec2c4ef0/packages/docusaurus-theme-classic/src/theme-classic.d.ts#L702 +declare module '@theme-original/EditThisPage' { + export interface Props { + readonly editUrl: string; + } + export default function EditThisPage(props: Props): JSX.Element; +} diff --git a/yarn.lock b/yarn.lock index 6e8bdd9b103..f31cfbcf261 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5477,7 +5477,7 @@ __metadata: languageName: node linkType: hard -"@docusaurus/theme-common@npm:3.4.0": +"@docusaurus/theme-common@npm:3.4.0, @docusaurus/theme-common@npm:^3.4.0": version: 3.4.0 resolution: "@docusaurus/theme-common@npm:3.4.0" dependencies: @@ -5691,6 +5691,7 @@ __metadata: dependencies: "@docusaurus/core": "npm:^3.4.0" "@docusaurus/module-type-aliases": "npm:^3.4.0" + "@docusaurus/theme-common": "npm:^3.4.0" "@docusaurus/types": "npm:^3.4.0" "@docusaurus/utils-validation": "npm:^3.4.0" "@elastic/datemath": "npm:^5.0.3"