Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • HTML Export
  • Export
  • Server-Side
  • Version History
  • Editable Voids
  • Huge Document
  • Hundreds Editors
  • Markdown Streaming
  • Preview Markdown
  • Collaboration Demo
  • Table Nomerge Demo
  • Excalidraw Demo
  • Code Drawing Demo
  • Single Block Demo
  • Find Demo
  • AI Demo
  • Align Demo
  • Autoformat Demo
  • Basic Nodes Demo
  • Block Menu Demo
  • Node Selection Demo
  • Column Demo
  • Code Block Demo
  • Callout Demo
  • Discussion Demo
  • Date Demo
  • Footnote Demo
  • Drag & Drop Demo
  • Emoji Demo
  • Equation Demo
  • Exit Break Demo
  • Floating Toolbar Demo
  • Font Demo
  • Indent Demo
  • List Demo
  • Line Height Demo
  • Link Demo
  • Media Demo
  • Mention Demo
  • Block Placeholder Demo
  • Serializing CSV Demo
  • Serializing Docx Demo
  • Serializing HTML Demo
  • Serializing Markdown Demo
  • Slash Command Demo
  • Plugin Rules Demo
  • Table Demo
  • Table of Contents Demo
  • Details Demo

Export

PreviousNext

Export a Plate document to HTML, Markdown, or Word.

Plus

The registry export-toolbar-button downloads the current document as HTML, Markdown, or Word from the browser. The demo mounts it in the fixed toolbar.

Demo

Loading…
HTML ExportServer-Side

On This Page

DemoToolbar sourceExport formatsSuggestionsDOCX presentationPlus exportRelated
Build your editor
Production-ready AI template and reusable components.
Get all-access

Toolbar source

Install the Export Toolbar Button component to add the menu to an editor toolbar.

'use client';
 
import { ArrowDownToLineIcon } from 'lucide-react';
import { createEditorView } from 'platejs';
import { DefaultAuthoredPlugin, isAuthoredEditor } from 'platejs/authored';
import { CommentsPlugin } from 'platejs/comments/react';
import { exportDocx, type DocxComment } from 'platejs/docx/export';
import { MarkdownPlugin } from 'platejs/markdown';
import { useEditor, useEditorSelector, useModelEditor } from 'platejs/react';
import { renderStaticHtml } from 'platejs/static';
import * as React from 'react';
import { toast } from 'sonner';
 
import { DOCX_EXPORT_STYLES } from '@/components/editor/docx-export';
import { useDocxSource } from '@/components/editor/docx-source';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import { ToolbarButton } from '@/components/editor/toolbar';
 
import { EditorStatic } from './editor-static';
 
type CleanProjection = 'accepted' | 'proposed';
 
const downloadBlob = (blob: Blob, filename: string) => {
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
 
  link.href = url;
  link.download = filename;
  document.body.append(link);
  link.click();
  link.remove();
  URL.revokeObjectURL(url);
};
 
const absoluteCssUrls = (css: string, base: string) =>
  css.replaceAll(
    /url\((['"]?)([^'")]+)\1\)/g,
    (match, quote: string, url: string) =>
      url.startsWith('data:') || url.startsWith('#')
        ? match
        : `url(${quote}${new URL(url, base).href}${quote})`
  );
 
// Inline the app's own CSS so the file renders with the same styles without
// depending on a stylesheet URL that changes between deployments.
const readAppStyles = () =>
  Array.from(document.styleSheets, (sheet) => {
    try {
      const css = Array.from(sheet.cssRules, (rule) => rule.cssText).join('\n');
 
      return `<style>${absoluteCssUrls(css, sheet.href ?? document.baseURI)}</style>`;
    } catch {
      // Cross-origin rules are unreadable; keep the app's link instead.
      return sheet.href ? `<link rel="stylesheet" href="${sheet.href}" />` : '';
    }
  }).join('\n');
 
const toastWarnings = (
  diagnostics: ReadonlyArray<Readonly<{ severity: 'error' | 'warning' }>>
) => {
  const warningCount = diagnostics.filter(
    ({ severity }) => severity === 'warning'
  ).length;
 
  if (warningCount > 0) {
    toast.warning(
      `Exported with ${warningCount} warning${warningCount === 1 ? '' : 's'}.`
    );
  }
};
 
export function ExportToolbarButton() {
  const editor = useEditor();
  const model = useModelEditor();
  const commentsInstalled = editor.plugin(CommentsPlugin).installed;
  const docxSource = useDocxSource();
  const [open, setOpen] = React.useState(false);
  const [projectionChoice, setProjectionChoice] =
    React.useState<CleanProjection>();
 
  const authoredState = useEditorSelector((current) => {
    const authored = current.plugin(DefaultAuthoredPlugin);
 
    if (!authored.installed) return 'clean:proposed';
 
    const unresolved = (['pending', 'conflicted'] as const).some(
      (status) =>
        authored.read.changes({
          limit: 1,
          status,
        }).items.length > 0
    );
 
    return `${unresolved ? 'unresolved' : 'clean'}:${authored.read.view().projection}`;
  });
  const [authoredStatus, mountedProjection] = authoredState.split(':');
  const unresolved = authoredStatus === 'unresolved';
  const projection =
    projectionChoice ??
    (mountedProjection === 'accepted' ? 'accepted' : 'proposed');
 
  const exportToHtml = async () => {
    const result = await renderStaticHtml(model, {
      component: EditorStatic,
      projection,
      props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } },
    });
 
    const html = `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    ${readAppStyles()}
  </head>
  <body class="${document.body.className}">
    ${result.data}
  </body>
</html>`;
 
    downloadBlob(new Blob([html], { type: 'text/html' }), 'plate.html');
    toastWarnings(result.diagnostics);
  };
 
  const exportToMarkdown = () => {
    const result = model.plugin(MarkdownPlugin).api.serialize({ projection });
 
    if (!result.ok) {
      toast.error(
        result.diagnostics.find(({ severity }) => severity === 'error')
          ?.message ?? 'Markdown export failed.'
      );
 
      return;
    }
    downloadBlob(
      new Blob([result.data], { type: 'text/markdown' }),
      'plate.md'
    );
    toastWarnings(result.diagnostics);
  };
 
  const exportToWord = async (
    requestedProjection?: CleanProjection | 'review'
  ) => {
    let omittedCommentCount = 0;
    const docxComments: DocxComment[] = [];
 
    if (commentsInstalled) {
      const commentEditor = isAuthoredEditor(model)
        ? createEditorView(model, {
            authored: { intent: 'edit', projection: 'proposed' },
          })
        : model;
      const comments = commentEditor.plugin(CommentsPlugin);
      const users = comments.store.get('users');
 
      comments.api.getThreads().forEach((thread) => {
        if (thread.status !== 'published') return;
        const attachment = comments.api.attachment(thread.id);
 
        if (attachment?.type !== 'range' || attachment.status !== 'attached') {
          omittedCommentCount += 1;
 
          return;
        }
        const parentId = thread.messages[0]
          ? `${thread.id}:${thread.messages[0].id}`
          : null;
 
        thread.messages.forEach((message, index) => {
          const user = users[message.userId];
          const name = user?.name ?? 'Unknown';
          const initials = name
            .split(/\s+/)
            .filter(Boolean)
            .map((part) => part[0])
            .join('')
            .slice(0, 4);
 
          docxComments.push({
            author: { ...(initials ? { initials } : {}), name },
            body: message.body,
            createdAt: message.createdAt,
            durableId: null,
            id: `${thread.id}:${message.id}`,
            parentId: index === 0 ? null : parentId,
            resolved: index === 0 ? thread.resolution !== null : null,
            target: { range: attachment.range },
          });
        });
      });
    }
    const wordProjection =
      requestedProjection ?? (unresolved ? projection : ('review' as const));
    const result = await exportDocx(model, {
      comments: docxComments,
      component: EditorStatic,
      // The download still completes; the toasts below report what was lost.
      lossPolicy: 'allow',
      projection: wordProjection,
      source: docxSource?.source,
      stylesheet: DOCX_EXPORT_STYLES,
    });
 
    if (!result.ok) {
      toast.error(
        result.diagnostics.find(({ severity }) => severity === 'error')
          ?.message ?? 'The Word document could not be exported.'
      );
 
      return;
    }
 
    downloadBlob(result.blob, 'plate.docx');
    toastWarnings(result.diagnostics);
    if (omittedCommentCount > 0) {
      toast.warning(
        `${omittedCommentCount} comment thread${omittedCommentCount === 1 ? '' : 's'} could not be attached to the Word document.`
      );
    }
  };
 
  return (
    <DropdownMenu
      open={open}
      onOpenChange={(nextOpen) => {
        if (nextOpen) setProjectionChoice(undefined);
        setOpen(nextOpen);
      }}
      modal={false}
    >
      <DropdownMenuTrigger>
        <ToolbarButton
          aria-label="Export"
          pressed={open}
          tooltip="Export"
          isDropdown
        >
          <ArrowDownToLineIcon className="size-4" />
        </ToolbarButton>
      </DropdownMenuTrigger>
 
      <DropdownMenuContent align="start">
        {unresolved && (
          <>
            <DropdownMenuLabel>Unresolved suggestions</DropdownMenuLabel>
            <DropdownMenuRadioGroup
              value={projection}
              onValueChange={(value) =>
                setProjectionChoice(
                  value === 'accepted' ? 'accepted' : 'proposed'
                )
              }
            >
              <DropdownMenuRadioItem
                value="proposed"
                onSelect={(event) => event.preventDefault()}
              >
                Include suggested changes
              </DropdownMenuRadioItem>
              <DropdownMenuRadioItem
                value="accepted"
                onSelect={(event) => event.preventDefault()}
              >
                Exclude suggested changes
              </DropdownMenuRadioItem>
            </DropdownMenuRadioGroup>
            <DropdownMenuSeparator />
          </>
        )}
        <DropdownMenuGroup>
          <DropdownMenuItem onSelect={exportToHtml}>
            Export as HTML
          </DropdownMenuItem>
          <DropdownMenuItem onSelect={exportToMarkdown}>
            Export as Markdown
          </DropdownMenuItem>
          <DropdownMenuItem onSelect={() => exportToWord()}>
            Export as Word
          </DropdownMenuItem>
          {unresolved && (
            <DropdownMenuItem onSelect={() => exportToWord('review')}>
              Export as Word with tracked changes
            </DropdownMenuItem>
          )}
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
'use client';
 
import { ArrowDownToLineIcon } from 'lucide-react';
import { createEditorView } from 'platejs';
import { DefaultAuthoredPlugin, isAuthoredEditor } from 'platejs/authored';
import { CommentsPlugin } from 'platejs/comments/react';
import { exportDocx, type DocxComment } from 'platejs/docx/export';
import { MarkdownPlugin } from 'platejs/markdown';
import { useEditor, useEditorSelector, useModelEditor } from 'platejs/react';
import { renderStaticHtml } from 'platejs/static';
import * as React from 'react';
import { toast } from
































































































































































































































































































Export formats

FormatImplementationOutput
HTMLrenderStaticHtml(model, { component: EditorStatic, projection }) from platejs/staticplate.html
Markdownmodel.api.markdown.serialize({ projection })plate.md
WordexportDocx(model, { component: EditorStatic, projection, stylesheet: DOCX_EXPORT_STYLES }) from platejs/docx/exportplate.docx

Every format captures the same complete document and explicit authored projection from useModelEditor(). Export reads model content rather than the DOM, so selection, placeholders, and review decorations do not enter the file. The current editor supplies its compiled schema, format mappings, and static component bindings; export does not create a second conversion editor.

The HTML file inlines the app's own stylesheets and copies the page's body classes. Cross-origin stylesheets stay as links.

Suggestions

When the document has unresolved suggestions, the menu shows an Unresolved suggestions choice:

ChoiceProjectionResult
Include suggested changesproposedThe document with pending suggestions applied
Exclude suggested changesacceptedThe document without pending suggestions

The initial choice follows the mounted view. HTML, Markdown, and Word use the same choice. Export as Word with tracked changes uses review, which writes pending suggestions as Word revisions. A conflict has no faithful Word revision, so review export returns an error until the conflict is resolved.

Without unresolved suggestions, Word export uses review. A retained, unchanged imported DOCX can then be returned byte for byte. Each exporter returns diagnostics, which the menu presents as warning or error toasts.

DOCX presentation

DOCX owns semantic mappings for Plate code blocks, columns, equations, callouts, headings, and tables of contents. The registry keeps only the copied visual stylesheet:

export const DOCX_EXPORT_STYLES = `
body {
  font-family: 'Calibri', 'Arial', sans-serif;
  font-size: 11pt;
  line-height: 1.5;
  color: #000;
  margin: 0;
  padding: 20px;
}
h1 { font-size: 24pt; font-weight: bold; margin: 0 0 12pt 0; }
h2 { font-size: 18pt; font-weight: bold; margin: 0 0 10pt 0; }
h3 { font-size: 14pt; font-weight: bold; margin: 0 0 8pt 0; }
h4 { font-size: 12pt; font-weight: bold; margin: 0 0 6pt 0; }
h5 { font-size: 11pt; font-weight: bold; margin: 0 0 6pt 0; }
h6 { font-size: 10pt; font-weight: bold; margin: 0 0 6pt 0; }
p { margin: 0 0 8pt 0; }
ul, ol { margin: 0 0 8pt 0; padding-left: 20pt; }
li { margin: 0 0 4pt 0; }
strong, b { font-weight: bold; }
em, i { font-style: italic; }
u { text-decoration: underline; }
s, strike, del { text-decoration: line-through; }
code {
  font-family: 'Courier New', Consolas, monospace;
  background-color: #f5f5f5;
  padding: 2px 4px;
  border-radius: 3px;
}
pre {
  font-family: 'Courier New', Consolas, monospace;
  background-color: #f5f5f5;
  padding: 10px;
  margin: 0 0 8pt 0;
  white-space: pre-wrap;
  border-radius: 4px;
}
.hljs-addition, .hljs-name, .hljs-quote, .hljs-selector-pseudo, .hljs-selector-tag { color: #22863a; }
.hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator,
.hljs-section, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-variable { color: #005cc5; }
.hljs-built_in, .hljs-symbol { color: #e36209; }
.hljs-bullet { color: #735c0f; }
.hljs-comment, .hljs-formula { color: #6a737d; }
.hljs-deletion { color: #b31d28; }
.hljs-doctag, .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type { color: #d73a49; }
.hljs-regexp, .hljs-string { color: #032f62; }
.hljs-title { color: #6f42c1; }
.hljs-emphasis { font-style: italic; }
.hljs-section, .hljs-strong { font-weight: bold; }
blockquote {
  border-left: 3px solid #ccc;
  margin: 0 0 8pt 0;
  padding-left: 10pt;
  color: #666;
  font-style: italic;
}
table {
  border-collapse: collapse;
  width: 100%;
  margin: 0 0 8pt 0;
}
th, td {
  border: 1px solid #ccc;
  padding: 6pt;
  text-align: left;
}
th {
  background-color: #f5f5f5;
  font-weight: bold;
}
a {
  color: #0066cc;
  text-decoration: underline;
}
img {
  max-width: 100%;
  height: auto;
}
hr {
  border: none;
  border-top: 1px solid #ccc;
  margin: 12pt 0;
}
sup { vertical-align: super; font-size: 8pt; }
sub { vertical-align: sub; font-size: 8pt; }
mark { background-color: #ffff00; }
`.trim();
export const DOCX_EXPORT_STYLES = `
body {
  font-family: 'Calibri', 'Arial', sans-serif;
  font-size: 11pt;
  line-height: 1.5;
  color: #000;
  margin: 0;
  padding: 20px;
}
h1 { font-size: 24pt; font-weight: bold; margin: 0 0 12pt 0; }
h2 { font-size: 18pt; font-weight: bold; margin: 0 0 10pt 0; }
h3 { font-size: 14pt; font-weight: bold; margin: 0 0 8pt 0; }
h4 { font-size: 12pt; font-weight: bold; margin: 0 0 6pt 0; }
h5 { font-size: 11pt; font-weight: bold; margin: 0 0 6pt 0; }
h6 { font-size: 10pt; font-weight: bold; margin: 0 0 6pt 0; }
p { margin: 0 0 8pt 0; }
ul, ol { margin: 0 0 8pt 0; padding-left: 20pt; }
li { margin: 0 0 4pt 0; }
strong, b { font-weight: bold; }
em, i { font-style: italic; }
u { text-decoration: underline; }
s, strike, del { text-decoration: line-through; }
code {
  font-family: 'Courier New', Consolas, monospace;
  background-color: #f5f5f5;




























































Plus export

Plate Plus includes a server-side export flow for PDF output with page settings.

Get the code

Related

  • DOCX covers DOCX paste, file import, and export APIs.
  • Markdown covers Markdown serialization.
  • Static Rendering covers static editor rendering and HTML serialization.
  • Authored Changes covers visible HTML/Markdown projections, authored JSON, and DOCX review.
'sonner'
;
import { DOCX_EXPORT_STYLES } from '@/components/editor/docx-export';
import { useDocxSource } from '@/components/editor/docx-source';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import { ToolbarButton } from '@/components/editor/toolbar';
import { EditorStatic } from './editor-static';
type CleanProjection = 'accepted' | 'proposed';
const downloadBlob = (blob: Blob, filename: string) => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.append(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
};
const absoluteCssUrls = (css: string, base: string) =>
css.replaceAll(
/url\((['"]?)([^'")]+)\1\)/g,
(match, quote: string, url: string) =>
url.startsWith('data:') || url.startsWith('#')
? match
: `url(${quote}${new URL(url, base).href}${quote})`
);
// Inline the app's own CSS so the file renders with the same styles without
// depending on a stylesheet URL that changes between deployments.
const readAppStyles = () =>
Array.from(document.styleSheets, (sheet) => {
try {
const css = Array.from(sheet.cssRules, (rule) => rule.cssText).join('\n');
return `<style>${absoluteCssUrls(css, sheet.href ?? document.baseURI)}</style>`;
} catch {
// Cross-origin rules are unreadable; keep the app's link instead.
return sheet.href ? `<link rel="stylesheet" href="${sheet.href}" />` : '';
}
}).join('\n');
const toastWarnings = (
diagnostics: ReadonlyArray<Readonly<{ severity: 'error' | 'warning' }>>
) => {
const warningCount = diagnostics.filter(
({ severity }) => severity === 'warning'
).length;
if (warningCount > 0) {
toast.warning(
`Exported with ${warningCount} warning${warningCount === 1 ? '' : 's'}.`
);
}
};
export function ExportToolbarButton() {
const editor = useEditor();
const model = useModelEditor();
const commentsInstalled = editor.plugin(CommentsPlugin).installed;
const docxSource = useDocxSource();
const [open, setOpen] = React.useState(false);
const [projectionChoice, setProjectionChoice] =
React.useState<CleanProjection>();
const authoredState = useEditorSelector((current) => {
const authored = current.plugin(DefaultAuthoredPlugin);
if (!authored.installed) return 'clean:proposed';
const unresolved = (['pending', 'conflicted'] as const).some(
(status) =>
authored.read.changes({
limit: 1,
status,
}).items.length > 0
);
return `${unresolved ? 'unresolved' : 'clean'}:${authored.read.view().projection}`;
});
const [authoredStatus, mountedProjection] = authoredState.split(':');
const unresolved = authoredStatus === 'unresolved';
const projection =
projectionChoice ??
(mountedProjection === 'accepted' ? 'accepted' : 'proposed');
const exportToHtml = async () => {
const result = await renderStaticHtml(model, {
component: EditorStatic,
projection,
props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } },
});
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
${readAppStyles()}
</head>
<body class="${document.body.className}">
${result.data}
</body>
</html>`;
downloadBlob(new Blob([html], { type: 'text/html' }), 'plate.html');
toastWarnings(result.diagnostics);
};
const exportToMarkdown = () => {
const result = model.plugin(MarkdownPlugin).api.serialize({ projection });
if (!result.ok) {
toast.error(
result.diagnostics.find(({ severity }) => severity === 'error')
?.message ?? 'Markdown export failed.'
);
return;
}
downloadBlob(
new Blob([result.data], { type: 'text/markdown' }),
'plate.md'
);
toastWarnings(result.diagnostics);
};
const exportToWord = async (
requestedProjection?: CleanProjection | 'review'
) => {
let omittedCommentCount = 0;
const docxComments: DocxComment[] = [];
if (commentsInstalled) {
const commentEditor = isAuthoredEditor(model)
? createEditorView(model, {
authored: { intent: 'edit', projection: 'proposed' },
})
: model;
const comments = commentEditor.plugin(CommentsPlugin);
const users = comments.store.get('users');
comments.api.getThreads().forEach((thread) => {
if (thread.status !== 'published') return;
const attachment = comments.api.attachment(thread.id);
if (attachment?.type !== 'range' || attachment.status !== 'attached') {
omittedCommentCount += 1;
return;
}
const parentId = thread.messages[0]
? `${thread.id}:${thread.messages[0].id}`
: null;
thread.messages.forEach((message, index) => {
const user = users[message.userId];
const name = user?.name ?? 'Unknown';
const initials = name
.split(/\s+/)
.filter(Boolean)
.map((part) => part[0])
.join('')
.slice(0, 4);
docxComments.push({
author: { ...(initials ? { initials } : {}), name },
body: message.body,
createdAt: message.createdAt,
durableId: null,
id: `${thread.id}:${message.id}`,
parentId: index === 0 ? null : parentId,
resolved: index === 0 ? thread.resolution !== null : null,
target: { range: attachment.range },
});
});
});
}
const wordProjection =
requestedProjection ?? (unresolved ? projection : ('review' as const));
const result = await exportDocx(model, {
comments: docxComments,
component: EditorStatic,
// The download still completes; the toasts below report what was lost.
lossPolicy: 'allow',
projection: wordProjection,
source: docxSource?.source,
stylesheet: DOCX_EXPORT_STYLES,
});
if (!result.ok) {
toast.error(
result.diagnostics.find(({ severity }) => severity === 'error')
?.message ?? 'The Word document could not be exported.'
);
return;
}
downloadBlob(result.blob, 'plate.docx');
toastWarnings(result.diagnostics);
if (omittedCommentCount > 0) {
toast.warning(
`${omittedCommentCount} comment thread${omittedCommentCount === 1 ? '' : 's'} could not be attached to the Word document.`
);
}
};
return (
<DropdownMenu
open={open}
onOpenChange={(nextOpen) => {
if (nextOpen) setProjectionChoice(undefined);
setOpen(nextOpen);
}}
modal={false}
>
<DropdownMenuTrigger>
<ToolbarButton
aria-label="Export"
pressed={open}
tooltip="Export"
isDropdown
>
<ArrowDownToLineIcon className="size-4" />
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{unresolved && (
<>
<DropdownMenuLabel>Unresolved suggestions</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={projection}
onValueChange={(value) =>
setProjectionChoice(
value === 'accepted' ? 'accepted' : 'proposed'
)
}
>
<DropdownMenuRadioItem
value="proposed"
onSelect={(event) => event.preventDefault()}
>
Include suggested changes
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
value="accepted"
onSelect={(event) => event.preventDefault()}
>
Exclude suggested changes
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuGroup>
<DropdownMenuItem onSelect={exportToHtml}>
Export as HTML
</DropdownMenuItem>
<DropdownMenuItem onSelect={exportToMarkdown}>
Export as Markdown
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => exportToWord()}>
Export as Word
</DropdownMenuItem>
{unresolved && (
<DropdownMenuItem onSelect={() => exportToWord('review')}>
Export as Word with tracked changes
</DropdownMenuItem>
)}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
padding: 2px 4px;
border-radius: 3px;
}
pre {
font-family: 'Courier New', Consolas, monospace;
background-color: #f5f5f5;
padding: 10px;
margin: 0 0 8pt 0;
white-space: pre-wrap;
border-radius: 4px;
}
.hljs-addition, .hljs-name, .hljs-quote, .hljs-selector-pseudo, .hljs-selector-tag { color: #22863a; }
.hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator,
.hljs-section, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-variable { color: #005cc5; }
.hljs-built_in, .hljs-symbol { color: #e36209; }
.hljs-bullet { color: #735c0f; }
.hljs-comment, .hljs-formula { color: #6a737d; }
.hljs-deletion { color: #b31d28; }
.hljs-doctag, .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type { color: #d73a49; }
.hljs-regexp, .hljs-string { color: #032f62; }
.hljs-title { color: #6f42c1; }
.hljs-emphasis { font-style: italic; }
.hljs-section, .hljs-strong { font-weight: bold; }
blockquote {
border-left: 3px solid #ccc;
margin: 0 0 8pt 0;
padding-left: 10pt;
color: #666;
font-style: italic;
}
table {
border-collapse: collapse;
width: 100%;
margin: 0 0 8pt 0;
}
th, td {
border: 1px solid #ccc;
padding: 6pt;
text-align: left;
}
th {
background-color: #f5f5f5;
font-weight: bold;
}
a {
color: #0066cc;
text-decoration: underline;
}
img {
max-width: 100%;
height: auto;
}
hr {
border: none;
border-top: 1px solid #ccc;
margin: 12pt 0;
}
sup { vertical-align: super; font-size: 8pt; }
sub { vertical-align: sub; font-size: 8pt; }
mark { background-color: #ffff00; }
`.trim();