mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 17:57:48 +02:00
refactor(live-codeblock): refactor live code block theme components (#11077)
This commit is contained in:
parent
387157205a
commit
29d19a6884
17 changed files with 354 additions and 180 deletions
|
@ -25,6 +25,7 @@ const plugin: Plugin<unknown[], Root> = function plugin(): Transformer<Root> {
|
|||
node.data.hProperties = node.data.hProperties || {};
|
||||
node.data.hProperties.metastring = node.meta;
|
||||
|
||||
// TODO Docusaurus v4: remove special case
|
||||
// Retrocompatible support for live codeblock metastring
|
||||
// Not really the appropriate place to handle that :s
|
||||
node.data.hProperties.live = node.meta?.split(' ').includes('live');
|
||||
|
|
|
@ -104,6 +104,7 @@ export type TableOfContents = {
|
|||
maxHeadingLevel: number;
|
||||
};
|
||||
|
||||
// TODO Docusaurus v4: use interface + declaration merging to enhance
|
||||
// Theme config after validation/normalization
|
||||
export type ThemeConfig = {
|
||||
docs: {
|
||||
|
|
|
@ -16,6 +16,14 @@ declare module '@docusaurus/theme-live-codeblock' {
|
|||
};
|
||||
}
|
||||
|
||||
declare module '@theme/LiveCodeBlock' {
|
||||
import type {Props as BaseProps} from '@theme/CodeBlock';
|
||||
|
||||
export interface Props extends BaseProps {}
|
||||
|
||||
export default function LiveCodeBlock(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {Props as BaseProps} from '@theme/CodeBlock';
|
||||
|
@ -31,6 +39,64 @@ declare module '@theme/Playground' {
|
|||
export default function Playground(props: LiveProviderProps): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Provider' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {Props as PlaygroundProps} from '@theme/Playground';
|
||||
|
||||
export interface Props extends Omit<PlaygroundProps, 'children'> {
|
||||
code: string | undefined;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function PlaygroundProvider(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Container' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function PlaygroundContainer(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Layout' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Props {}
|
||||
|
||||
export default function PlaygroundLayout(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Preview' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Props {}
|
||||
|
||||
export default function PlaygroundPreview(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Editor' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Props {}
|
||||
|
||||
export default function PlaygroundEditor(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Header' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Props {}
|
||||
|
||||
export default function PlaygroundHeader(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/ReactLiveScope' {
|
||||
type Scope = {
|
||||
[key: string]: unknown;
|
||||
|
|
|
@ -5,21 +5,28 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Playground from '@theme/Playground';
|
||||
import ReactLiveScope from '@theme/ReactLiveScope';
|
||||
import CodeBlock, {type Props} from '@theme-init/CodeBlock';
|
||||
import React, {type ReactNode} from 'react';
|
||||
import type {Props as CodeBlockProps} from '@theme/CodeBlock';
|
||||
import OriginalCodeBlock from '@theme-init/CodeBlock';
|
||||
import LiveCodeBlock from '@theme/LiveCodeBlock';
|
||||
|
||||
const withLiveEditor = (Component: typeof CodeBlock) => {
|
||||
function WrappedComponent(props: Props) {
|
||||
if (props.live) {
|
||||
return <Playground scope={ReactLiveScope} {...props} />;
|
||||
}
|
||||
|
||||
return <Component {...props} />;
|
||||
// TODO Docusaurus v4: remove special case
|
||||
// see packages/docusaurus-mdx-loader/src/remark/mdx1Compat/codeCompatPlugin.ts
|
||||
// we can just use the metastring instead
|
||||
declare module '@theme/CodeBlock' {
|
||||
interface Props {
|
||||
live?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
return WrappedComponent;
|
||||
};
|
||||
function isLiveCodeBlock(props: CodeBlockProps): boolean {
|
||||
return !!props.live;
|
||||
}
|
||||
|
||||
export default withLiveEditor(CodeBlock);
|
||||
export default function CodeBlockEnhancer(props: CodeBlockProps): ReactNode {
|
||||
return isLiveCodeBlock(props) ? (
|
||||
<LiveCodeBlock {...props} />
|
||||
) : (
|
||||
<OriginalCodeBlock {...props} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import Playground from '@theme/Playground';
|
||||
import ReactLiveScope from '@theme/ReactLiveScope';
|
||||
import type {Props} from '@theme/LiveCodeBlock';
|
||||
|
||||
export default function LiveCodeBlock(props: Props): ReactNode {
|
||||
return <Playground scope={ReactLiveScope} {...props} />;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
|
||||
import type {Props} from '@theme/Playground/Container';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function PlaygroundContainer({children}: Props): ReactNode {
|
||||
return <div className={styles.playgroundContainer}>{children}</div>;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.playgroundContainer {
|
||||
margin-bottom: var(--ifm-leading);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: var(--ifm-global-shadow-lw);
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import {LiveEditor} from 'react-live';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import PlaygroundHeader from '@theme/Playground/Header';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function PlaygroundEditor(): ReactNode {
|
||||
const isBrowser = useIsBrowser();
|
||||
return (
|
||||
<>
|
||||
<PlaygroundHeader>
|
||||
<Translate
|
||||
id="theme.Playground.liveEditor"
|
||||
description="The live editor label of the live codeblocks">
|
||||
Live Editor
|
||||
</Translate>
|
||||
</PlaygroundHeader>
|
||||
<LiveEditor
|
||||
// We force remount the editor on hydration,
|
||||
// otherwise dark prism theme is not applied
|
||||
key={String(isBrowser)}
|
||||
className={styles.playgroundEditor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.playgroundEditor {
|
||||
font: var(--ifm-code-font-size) / var(--ifm-pre-line-height)
|
||||
var(--ifm-font-family-monospace) !important;
|
||||
/* rtl:ignore */
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.playgroundEditor pre {
|
||||
border-radius: 0;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function PlaygroundHeader({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): ReactNode {
|
||||
return <div className={clsx(styles.playgroundHeader)}>{children}</div>;
|
||||
}
|
|
@ -5,13 +5,6 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.playgroundContainer {
|
||||
margin-bottom: var(--ifm-leading);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: var(--ifm-global-shadow-lw);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playgroundHeader {
|
||||
letter-spacing: 0.08rem;
|
||||
padding: 0.75rem;
|
||||
|
@ -26,19 +19,3 @@
|
|||
background: var(--ifm-color-emphasis-600);
|
||||
color: var(--ifm-color-content-inverse);
|
||||
}
|
||||
|
||||
.playgroundEditor {
|
||||
font: var(--ifm-code-font-size) / var(--ifm-pre-line-height)
|
||||
var(--ifm-font-family-monospace) !important;
|
||||
/* rtl:ignore */
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.playgroundEditor pre {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.playgroundPreview {
|
||||
padding: 1rem;
|
||||
background-color: var(--ifm-pre-background);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import {useThemeConfig} from '@docusaurus/theme-common';
|
||||
import PlaygroundPreview from '@theme/Playground/Preview';
|
||||
import PlaygroundEditor from '@theme/Playground/Editor';
|
||||
|
||||
import type {ThemeConfig} from '@docusaurus/theme-live-codeblock';
|
||||
|
||||
function useLiveCodeBlockThemeConfig() {
|
||||
const themeConfig = useThemeConfig() as unknown as ThemeConfig;
|
||||
return themeConfig.liveCodeBlock;
|
||||
}
|
||||
|
||||
export default function PlaygroundLayout(): ReactNode {
|
||||
const {playgroundPosition} = useLiveCodeBlockThemeConfig();
|
||||
return (
|
||||
<>
|
||||
{playgroundPosition === 'top' ? (
|
||||
<>
|
||||
<PlaygroundPreview />
|
||||
<PlaygroundEditor />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlaygroundEditor />
|
||||
<PlaygroundPreview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import {LiveError, LivePreview} from 'react-live';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import {ErrorBoundaryErrorMessageFallback} from '@docusaurus/theme-common';
|
||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import PlaygroundHeader from '@theme/Playground/Header';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function Loader() {
|
||||
// Is it worth improving/translating?
|
||||
// eslint-disable-next-line @docusaurus/no-untranslated-text
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
function PlaygroundLivePreview(): ReactNode {
|
||||
// No SSR for the live preview
|
||||
// See https://github.com/facebook/docusaurus/issues/5747
|
||||
return (
|
||||
<BrowserOnly fallback={<Loader />}>
|
||||
{() => (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
fallback={(params) => (
|
||||
<ErrorBoundaryErrorMessageFallback {...params} />
|
||||
)}>
|
||||
<LivePreview />
|
||||
</ErrorBoundary>
|
||||
<LiveError />
|
||||
</>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlaygroundPreview(): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<PlaygroundHeader>
|
||||
<Translate
|
||||
id="theme.Playground.result"
|
||||
description="The result label of the live codeblocks">
|
||||
Result
|
||||
</Translate>
|
||||
</PlaygroundHeader>
|
||||
<div className={styles.playgroundPreview}>
|
||||
<PlaygroundLivePreview />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.playgroundPreview {
|
||||
padding: 1rem;
|
||||
background-color: var(--ifm-pre-background);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import {LiveProvider} from 'react-live';
|
||||
import {usePrismTheme} from '@docusaurus/theme-common';
|
||||
|
||||
import type {Props} from '@theme/Playground/Provider';
|
||||
|
||||
// this should rather be a stable function
|
||||
// see https://github.com/facebook/docusaurus/issues/9630#issuecomment-1855682643
|
||||
const DEFAULT_TRANSFORM_CODE = (code: string) => `${code};`;
|
||||
|
||||
export default function PlaygroundProvider({
|
||||
code,
|
||||
children,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const prismTheme = usePrismTheme();
|
||||
const noInline = props.metastring?.includes('noInline') ?? false;
|
||||
return (
|
||||
<LiveProvider
|
||||
noInline={noInline}
|
||||
theme={prismTheme}
|
||||
{...props}
|
||||
code={code?.replace(/\n$/, '')}
|
||||
transformCode={props.transformCode ?? DEFAULT_TRANSFORM_CODE}>
|
||||
{children}
|
||||
</LiveProvider>
|
||||
);
|
||||
}
|
|
@ -6,137 +6,22 @@
|
|||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import {
|
||||
ErrorBoundaryErrorMessageFallback,
|
||||
usePrismTheme,
|
||||
} from '@docusaurus/theme-common';
|
||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||
import PlaygroundProvider from '@theme/Playground/Provider';
|
||||
import PlaygroundContainer from '@theme/Playground/Container';
|
||||
import PlaygroundLayout from '@theme/Playground/Layout';
|
||||
|
||||
import type {Props} from '@theme/Playground';
|
||||
import type {ThemeConfig} from '@docusaurus/theme-live-codeblock';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function Header({children}: {children: ReactNode}) {
|
||||
return <div className={clsx(styles.playgroundHeader)}>{children}</div>;
|
||||
}
|
||||
|
||||
function LivePreviewLoader() {
|
||||
// Is it worth improving/translating?
|
||||
// eslint-disable-next-line @docusaurus/no-untranslated-text
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
function Preview() {
|
||||
// No SSR for the live preview
|
||||
// See https://github.com/facebook/docusaurus/issues/5747
|
||||
return (
|
||||
<BrowserOnly fallback={<LivePreviewLoader />}>
|
||||
{() => (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
fallback={(params) => (
|
||||
<ErrorBoundaryErrorMessageFallback {...params} />
|
||||
)}>
|
||||
<LivePreview />
|
||||
</ErrorBoundary>
|
||||
<LiveError />
|
||||
</>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
function ResultWithHeader() {
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<Translate
|
||||
id="theme.Playground.result"
|
||||
description="The result label of the live codeblocks">
|
||||
Result
|
||||
</Translate>
|
||||
</Header>
|
||||
{/* https://github.com/facebook/docusaurus/issues/5747 */}
|
||||
<div className={styles.playgroundPreview}>
|
||||
<Preview />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ThemedLiveEditor() {
|
||||
const isBrowser = useIsBrowser();
|
||||
return (
|
||||
<LiveEditor
|
||||
// We force remount the editor on hydration,
|
||||
// otherwise dark prism theme is not applied
|
||||
key={String(isBrowser)}
|
||||
className={styles.playgroundEditor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EditorWithHeader() {
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<Translate
|
||||
id="theme.Playground.liveEditor"
|
||||
description="The live editor label of the live codeblocks">
|
||||
Live Editor
|
||||
</Translate>
|
||||
</Header>
|
||||
<ThemedLiveEditor />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// this should rather be a stable function
|
||||
// see https://github.com/facebook/docusaurus/issues/9630#issuecomment-1855682643
|
||||
const DEFAULT_TRANSFORM_CODE = (code: string) => `${code};`;
|
||||
|
||||
export default function Playground({
|
||||
children,
|
||||
transformCode,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const {
|
||||
siteConfig: {themeConfig},
|
||||
} = useDocusaurusContext();
|
||||
const {
|
||||
liveCodeBlock: {playgroundPosition},
|
||||
} = themeConfig as ThemeConfig;
|
||||
const prismTheme = usePrismTheme();
|
||||
|
||||
const noInline = props.metastring?.includes('noInline') ?? false;
|
||||
|
||||
return (
|
||||
<div className={styles.playgroundContainer}>
|
||||
<LiveProvider
|
||||
code={children?.replace(/\n$/, '')}
|
||||
noInline={noInline}
|
||||
transformCode={transformCode ?? DEFAULT_TRANSFORM_CODE}
|
||||
theme={prismTheme}
|
||||
{...props}>
|
||||
{playgroundPosition === 'top' ? (
|
||||
<>
|
||||
<ResultWithHeader />
|
||||
<EditorWithHeader />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EditorWithHeader />
|
||||
<ResultWithHeader />
|
||||
</>
|
||||
)}
|
||||
</LiveProvider>
|
||||
</div>
|
||||
<PlaygroundContainer>
|
||||
<PlaygroundProvider code={children} {...props}>
|
||||
<PlaygroundLayout />
|
||||
</PlaygroundProvider>
|
||||
</PlaygroundContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/// <reference types="@docusaurus/theme-classic" />
|
||||
/// <reference types="@docusaurus/module-type-aliases" />
|
||||
|
||||
declare module '@theme-init/CodeBlock' {
|
||||
import type CodeBlock from '@theme/CodeBlock';
|
||||
import type {Props as BaseProps} from '@theme/CodeBlock';
|
||||
|
||||
export interface Props extends BaseProps {
|
||||
live?: boolean;
|
||||
}
|
||||
const CodeBlockComp: typeof CodeBlock;
|
||||
export default CodeBlockComp;
|
||||
}
|
Loading…
Add table
Reference in a new issue