mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-13 14:57:54 +02:00
stable live code block refactor
This commit is contained in:
parent
02ec2dc1ae
commit
17a586c28d
10 changed files with 229 additions and 130 deletions
|
@ -104,6 +104,7 @@ export type TableOfContents = {
|
||||||
maxHeadingLevel: number;
|
maxHeadingLevel: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO Docusaurus v4: use interface + declaration merging to enhance
|
||||||
// Theme config after validation/normalization
|
// Theme config after validation/normalization
|
||||||
export type ThemeConfig = {
|
export type ThemeConfig = {
|
||||||
docs: {
|
docs: {
|
||||||
|
|
|
@ -39,6 +39,33 @@ declare module '@theme/Playground' {
|
||||||
export default function Playground(props: LiveProviderProps): ReactNode;
|
export default function Playground(props: LiveProviderProps): 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' {
|
declare module '@theme/ReactLiveScope' {
|
||||||
type Scope = {
|
type Scope = {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* 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 styles from './styles.module.css';
|
||||||
|
|
||||||
|
export default function PlaygroundEditor(): ReactNode {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.playgroundHeader {
|
||||||
|
letter-spacing: 0.08rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
background: var(--ifm-color-emphasis-200);
|
||||||
|
color: var(--ifm-color-content);
|
||||||
|
font-size: var(--ifm-code-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playgroundHeader:first-of-type {
|
||||||
|
background: var(--ifm-color-emphasis-600);
|
||||||
|
color: var(--ifm-color-content-inverse);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -6,137 +6,74 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {type ReactNode} from 'react';
|
import React, {type ReactNode} from 'react';
|
||||||
import clsx from 'clsx';
|
import {LiveProvider} from 'react-live';
|
||||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
import {usePrismTheme, useThemeConfig} from '@docusaurus/theme-common';
|
||||||
import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live';
|
import PlaygroundPreview from '@theme/Playground/Preview';
|
||||||
import Translate from '@docusaurus/Translate';
|
import PlaygroundEditor from '@theme/Playground/Editor';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
||||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
|
||||||
import {
|
|
||||||
ErrorBoundaryErrorMessageFallback,
|
|
||||||
usePrismTheme,
|
|
||||||
} from '@docusaurus/theme-common';
|
|
||||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
|
||||||
|
|
||||||
import type {Props} from '@theme/Playground';
|
import type {Props} from '@theme/Playground';
|
||||||
import type {ThemeConfig} from '@docusaurus/theme-live-codeblock';
|
import type {ThemeConfig} from '@docusaurus/theme-live-codeblock';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
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
|
// this should rather be a stable function
|
||||||
// see https://github.com/facebook/docusaurus/issues/9630#issuecomment-1855682643
|
// see https://github.com/facebook/docusaurus/issues/9630#issuecomment-1855682643
|
||||||
const DEFAULT_TRANSFORM_CODE = (code: string) => `${code};`;
|
const DEFAULT_TRANSFORM_CODE = (code: string) => `${code};`;
|
||||||
|
|
||||||
|
type PlaygroundProviderProps = Omit<Props, 'children'> & {
|
||||||
|
code: string | undefined;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function PlaygroundProvider(props: PlaygroundProviderProps): ReactNode {
|
||||||
|
const prismTheme = usePrismTheme();
|
||||||
|
const noInline = props.metastring?.includes('noInline') ?? false;
|
||||||
|
return (
|
||||||
|
<LiveProvider
|
||||||
|
noInline={noInline}
|
||||||
|
theme={prismTheme}
|
||||||
|
{...props}
|
||||||
|
code={props.code?.replace(/\n$/, '')}
|
||||||
|
transformCode={props.transformCode ?? DEFAULT_TRANSFORM_CODE}>
|
||||||
|
<PlaygroundContent />
|
||||||
|
</LiveProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useLiveCodeBlockThemeConfig() {
|
||||||
|
const themeConfig = useThemeConfig() as unknown as ThemeConfig;
|
||||||
|
return themeConfig.liveCodeBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PlaygroundContent(): ReactNode {
|
||||||
|
const {playgroundPosition} = useLiveCodeBlockThemeConfig();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{playgroundPosition === 'top' ? (
|
||||||
|
<>
|
||||||
|
<PlaygroundPreview />
|
||||||
|
<PlaygroundEditor />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PlaygroundEditor />
|
||||||
|
<PlaygroundPreview />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Playground({
|
export default function Playground({
|
||||||
children,
|
children,
|
||||||
transformCode,
|
transformCode,
|
||||||
...props
|
...props
|
||||||
}: Props): ReactNode {
|
}: Props): ReactNode {
|
||||||
const {
|
|
||||||
siteConfig: {themeConfig},
|
|
||||||
} = useDocusaurusContext();
|
|
||||||
const {
|
|
||||||
liveCodeBlock: {playgroundPosition},
|
|
||||||
} = themeConfig as ThemeConfig;
|
|
||||||
const prismTheme = usePrismTheme();
|
|
||||||
|
|
||||||
const noInline = props.metastring?.includes('noInline') ?? false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.playgroundContainer}>
|
<div className={styles.playgroundContainer}>
|
||||||
<LiveProvider
|
<PlaygroundProvider code={children} {...props}>
|
||||||
code={children?.replace(/\n$/, '')}
|
<PlaygroundContent />
|
||||||
noInline={noInline}
|
</PlaygroundProvider>
|
||||||
transformCode={transformCode ?? DEFAULT_TRANSFORM_CODE}
|
|
||||||
theme={prismTheme}
|
|
||||||
{...props}>
|
|
||||||
{playgroundPosition === 'top' ? (
|
|
||||||
<>
|
|
||||||
<ResultWithHeader />
|
|
||||||
<EditorWithHeader />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<EditorWithHeader />
|
|
||||||
<ResultWithHeader />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</LiveProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,19 +26,3 @@
|
||||||
background: var(--ifm-color-emphasis-600);
|
background: var(--ifm-color-emphasis-600);
|
||||||
color: var(--ifm-color-content-inverse);
|
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);
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue