From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Introduction
  • Installation
    • Plate UI
      • Next.js
      • React
    • Manual
    • RSC
    • Node.js
    • Local Docs
    • MCP
  • Releases

Code Drawing

PreviousNext
Code Drawing Node
Loading…
Code BlockColumn

On This Page

FeaturesKit UsageInstallationAdd KitManual UsageInstallationAdd PluginConfigure PluginAdd Toolbar ButtonPluginsCodeDrawingPluginAPIeditor.plugin(BaseCodeDrawingPlugin).update.insertTypesCodeDrawingElementCodeDrawingLanguageCodeDrawingViewUtilitiesrenderCodeDrawingdownloadImage
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Support for PlantUML, Graphviz, Flowchart, and Mermaid diagram languages
  • Inline code editing with code-block-like styling
  • Real-time preview with debounced rendering (500ms)
  • Three persisted views: split, code, and preview
  • Responsive layout: horizontal on desktop (code left, preview right), vertical on mobile (preview top, code bottom)
  • Border separator between code editor and preview in Both mode
  • Toolbar controls: language selector and view mode selector (always visible on mobile, hover to show on desktop)
  • Download rendered diagrams as images
  • Floating toolbar with delete and download actions
Report an issue

Kit Usage

Installation

The fastest way to add code drawing functionality is with the CodeDrawingKit, which includes the pre-configured CodeDrawingPlugin with its Plate UI components.

'use client';
 
import {
  type CodeDrawingLanguage,
  type CodeDrawingView,
  CODE_DRAWING_LANGUAGES,
  DEFAULT_MIN_HEIGHT,
  DOWNLOAD_FILENAME,
  RENDER_DEBOUNCE_DELAY,
  CODE_DRAWING_VIEWS,
  downloadImage,
  renderCodeDrawing,
} from '@platejs/code-drawing';
import { CodeDrawingPlugin } from '@platejs/code-drawing/react';
import { DownloadIcon, Trash2 } from 'lucide-react';
import {
  type PlateElementProps,
  PlateElement,
  useEditor,
  useEditorReadOnly,
  useEditorSelector,
  useElement,
  useElementSelected,
  useFocusedLast,
  usePath,
} from 'platejs/react';
import * as React from 'react';
 
import { Button } from '@/components/ui/button';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { useIsMobile } from '@/hooks/use-mobile';
import {
  FloatingPopover,
  FloatingPopoverAnchor,
  FloatingPopoverContent,
} from '@/components/editor/floating-popover';
 
const languageLabels: Record<CodeDrawingLanguage, string> = {
  flowchart: 'Flowchart',
  graphviz: 'Graphviz',
  mermaid: 'Mermaid',
  plantuml: 'PlantUML',
};
 
const viewLabels: Record<CodeDrawingView, string> = {
  code: 'Code',
  preview: 'Preview',
  split: 'Split',
};
 
const isCodeDrawingLanguage = (
  value: string | null
): value is CodeDrawingLanguage =>
  CODE_DRAWING_LANGUAGES.some((language) => language === value);
 
const isCodeDrawingView = (value: string | null): value is CodeDrawingView =>
  CODE_DRAWING_VIEWS.some((view) => view === value);
 
export function CodeDrawingElement(
  props: PlateElementProps<typeof CodeDrawingPlugin>
) {
  const { children } = props;
  const isMobile = useIsMobile();
  const editor = useEditor();
  const readOnly = useEditorReadOnly();
  const selected = useElementSelected();
  const isFocusedLast = useFocusedLast();
  const element = useElement(CodeDrawingPlugin);
  const path = usePath();
  const [loading, setLoading] = React.useState(false);
  const [image, setImage] = React.useState('');
  const [renderError, setRenderError] = React.useState<string | null>(null);
 
  React.useEffect(() => {
    let cancelled = false;
    const render = async () => {
      if (!element.code.trim()) {
        setImage('');
        setRenderError(null);
        setLoading(false);
        return;
      }
 
      setRenderError(null);
      setLoading(true);
 
      try {
        const imageData = await renderCodeDrawing(
          element.language,
          element.code
        );
 
        if (!cancelled) setImage(imageData);
      } catch (error) {
        if (!cancelled) {
          const message =
            error instanceof Error ? error.message : 'Rendering failed';
 
          console.error(message);
          setImage('');
          setRenderError(message);
        }
      } finally {
        if (!cancelled) setLoading(false);
      }
    };
    const timeout = window.setTimeout(() => {
      void render();
    }, RENDER_DEBOUNCE_DELAY);
 
    return () => {
      cancelled = true;
      window.clearTimeout(timeout);
    };
  }, [element.code, element.language]);
 
  const handleDownload = React.useCallback(() => {
    if (!image) return;
    downloadImage(image, DOWNLOAD_FILENAME);
  }, [image]);
 
  const handleCodeChange = React.useCallback(
    (code: string) => {
      if (path) {
        editor.update.nodes.set({ code }, { at: path });
      }
    },
    [editor, path]
  );
 
  const handleLanguageChange = React.useCallback(
    (language: CodeDrawingLanguage) => {
      if (path) {
        editor.update.nodes.set({ language }, { at: path });
      }
    },
    [editor, path]
  );
 
  const handleViewChange = React.useCallback(
    (view: CodeDrawingView) => {
      if (path) {
        editor.update.nodes.set({ view }, { at: path });
      }
    },
    [editor, path]
  );
 
  const { code, language, view } = element;
 
  const selectionCollapsed = useEditorSelector((innerEditor) =>
    innerEditor.read.selection.isCollapsed()
  );
 
  const open = isFocusedLast && !readOnly && selected && selectionCollapsed;
 
  const content = (
    <PlateElement {...props}>
      <div contentEditable={false}>
        <div>
          <CodeDrawingPreview
            code={code}
            language={language}
            view={view}
            image={image}
            loading={loading}
            renderError={renderError}
            onCodeChange={handleCodeChange}
            onLanguageChange={handleLanguageChange}
            onViewChange={handleViewChange}
            readOnly={readOnly}
            isMobile={isMobile}
          />
        </div>
      </div>
      {children}
    </PlateElement>
  );
 
  if (readOnly) {
    return content;
  }
 
  return (
    <FloatingPopover open={open} modal={false}>
      <FloatingPopoverAnchor element={content} />
      <FloatingPopoverContent
        className="w-auto p-1"
        contentEditable={false}
        onInitialFocus={(e) => {
          e.preventDefault();
        }}
      >
        <div className="flex items-center gap-1">
          {image && (
            <Button
              size="icon"
              variant="ghost"
              className="size-8"
              onClick={handleDownload}
              title="Export"
            >
              <DownloadIcon className="size-4" />
            </Button>
          )}
          <Button
            size="icon"
            variant="ghost"
            className="size-8"
            onClick={() => {
              if (!readOnly && path) editor.update.nodes.remove({ at: path });
            }}
            title="Delete"
          >
            <Trash2 className="size-4" />
          </Button>
        </div>
      </FloatingPopoverContent>
    </FloatingPopover>
  );
}
 
function CodeDrawingPreview({
  code,
  language,
  view,
  image,
  loading,
  renderError,
  onCodeChange,
  onLanguageChange,
  onViewChange,
  readOnly = false,
  isMobile = false,
}: {
  code: string;
  language: CodeDrawingLanguage;
  view: CodeDrawingView;
  image: string;
  loading: boolean;
  renderError: string | null;
  onCodeChange: (code: string) => void;
  onLanguageChange: (language: CodeDrawingLanguage) => void;
  onViewChange: (view: CodeDrawingView) => void;
  readOnly?: boolean;
  isMobile?: boolean;
}) {
  const viewMode = view;
  const showCode = viewMode === 'split' || viewMode === 'code';
  const showBorder = viewMode === 'split';
 
  const handleCodeChange = React.useCallback(
    (e: React.ChangeEvent<HTMLTextAreaElement>) => {
      onCodeChange(e.target.value);
    },
    [onCodeChange]
  );
 
  const toolbar = readOnly ? null : (
    <CodeDrawingToolbar
      language={language}
      viewMode={viewMode}
      readOnly={readOnly}
      isMobile={isMobile}
      onLanguageChange={onLanguageChange}
      onViewChange={onViewChange}
    />
  );
 
  return (
    <div
      className={`flex ${
        isMobile ? 'flex-col-reverse' : 'flex-col'
      } group my-4 w-full items-stretch border bg-muted/50 md:flex-row`}
      style={{
        minHeight: `${DEFAULT_MIN_HEIGHT}px`,
      }}
    >
      {showCode && (
        <CodeDrawingTextarea
          code={code}
          viewMode={viewMode}
          readOnly={readOnly}
          isMobile={isMobile}
          showBorder={showBorder}
          onCodeChange={handleCodeChange}
          toolbar={viewMode === 'code' ? toolbar : null}
        />
      )}
 
      {viewMode !== 'code' && (
        <CodeDrawingPreviewArea
          image={image}
          loading={loading}
          renderError={renderError}
          code={code}
          viewMode={viewMode}
          readOnly={readOnly}
          isMobile={isMobile}
          showBorder={showBorder}
          toolbar={toolbar}
        />
      )}
    </div>
  );
}
 
function CodeDrawingToolbar({
  language,
  viewMode,
  readOnly = false,
  isMobile = false,
  onLanguageChange,
  onViewChange,
}: {
  language: CodeDrawingLanguage;
  viewMode: CodeDrawingView;
  readOnly?: boolean;
  isMobile?: boolean;
  onLanguageChange: (language: CodeDrawingLanguage) => void;
  onViewChange: (view: CodeDrawingView) => void;
}) {
  const [toolbarVisible, setToolbarVisible] = React.useState(false);
  const [languageSelectOpen, setLanguageSelectOpen] = React.useState(false);
  const [viewSelectOpen, setViewSelectOpen] = React.useState(false);
 
  const opacityClass =
    isMobile || toolbarVisible || languageSelectOpen || viewSelectOpen
      ? 'opacity-100'
      : 'opacity-0 group-hover:opacity-100';
 
  const positionClass = isMobile
    ? 'flex items-center gap-2'
    : 'absolute right-2 z-10 flex items-center gap-2';
 
  return (
    <div
      role="toolbar"
      tabIndex={-1}
      className={`${positionClass} transition-opacity ${opacityClass}`}
      onMouseEnter={() => {
        setToolbarVisible(true);
      }}
      onMouseLeave={() => {
        if (!languageSelectOpen && !viewSelectOpen) {
          setToolbarVisible(false);
        }
      }}
    >
      {!readOnly && (
        <Select
          value={language}
          onValueChange={(nextLanguage) => {
            if (isCodeDrawingLanguage(nextLanguage)) {
              onLanguageChange(nextLanguage);
            }
          }}
          open={languageSelectOpen}
          onOpenChange={setLanguageSelectOpen}
        >
          <SelectTrigger
            className={`h-8 w-[120px] border-0 bg-muted/50 text-xs shadow-none ${
              isMobile ? '' : 'transition-colors hover:bg-zinc-200'
            }`}
          >
            <SelectValue />
          </SelectTrigger>
          <SelectContent className="z-[100]">
            {CODE_DRAWING_LANGUAGES.map((innerLanguage) => (
              <SelectItem key={innerLanguage} value={innerLanguage}>
                {languageLabels[innerLanguage]}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      )}
 
      {!readOnly && (
        <Select
          value={viewMode}
          onValueChange={(nextView) => {
            if (isCodeDrawingView(nextView)) {
              onViewChange(nextView);
            }
          }}
          open={viewSelectOpen}
          onOpenChange={setViewSelectOpen}
        >
          <SelectTrigger
            className={`h-8 w-[80px] border-0 bg-muted/50 text-xs shadow-none ${
              isMobile ? '' : 'transition-colors hover:bg-zinc-200'
            }`}
          >
            <SelectValue />
          </SelectTrigger>
          <SelectContent className="z-[100]">
            {CODE_DRAWING_VIEWS.map((view) => (
              <SelectItem key={view} value={view}>
                {viewLabels[view]}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      )}
    </div>
  );
}
 
function CodeDrawingTextarea({
  code,
  viewMode,
  readOnly = false,
  isMobile = false,
  showBorder = false,
  onCodeChange,
  toolbar,
}: {
  code: string;
  viewMode: CodeDrawingView;
  readOnly?: boolean;
  isMobile?: boolean;
  showBorder?: boolean;
  onCodeChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
  toolbar?: React.ReactNode;
}) {
  const textareaRef = React.useRef<HTMLTextAreaElement>(null);
  const isCodeOnlyMode = viewMode === 'code';
 
  const [internalCode, setInternalCode] = React.useState(code);
  const lastExternalCodeRef = React.useRef(code);
 
  React.useEffect(() => {
    if (code !== lastExternalCodeRef.current) {
      lastExternalCodeRef.current = code;
      setInternalCode(code);
    }
  }, [code]);
 
  const handleChange = React.useCallback(
    (e: React.ChangeEvent<HTMLTextAreaElement>) => {
      const newValue = e.target.value;
      setInternalCode(newValue);
      onCodeChange(e);
    },
    [onCodeChange]
  );
 
  return (
    <div
      className={`${
        isCodeOnlyMode ? 'w-full' : 'min-w-0 flex-1'
      } flex flex-col ${isCodeOnlyMode && !isMobile ? 'relative' : ''} ${
        showBorder && !isMobile ? 'border-r' : ''
      }`}
    >
      {toolbar && isCodeOnlyMode && (
        <div
          className={
            isMobile
              ? 'mt-2 mb-2 flex justify-end px-2'
              : 'absolute right-2 z-10 mt-2'
          }
        >
          {toolbar}
        </div>
      )}
 
      <div className="relative flex-1 rounded-md">
        <pre
          className="m-0 overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid"
          style={{ minHeight: `${DEFAULT_MIN_HEIGHT}px`, height: '100%' }}
        >
          <code className="block h-full w-full">
            <textarea
              ref={textareaRef}
              value={internalCode}
              onChange={handleChange}
              readOnly={readOnly}
              className="m-0 h-full w-full resize-none overflow-auto border-0 bg-transparent p-0 font-mono text-sm outline-none"
              style={{ minHeight: `${DEFAULT_MIN_HEIGHT}px` }}
              placeholder="Enter your code here..."
              spellCheck={false}
            />
          </code>
        </pre>
      </div>
    </div>
  );
}
 
function CodeDrawingPreviewArea({
  image,
  loading,
  renderError,
  code,
  viewMode,
  readOnly: _readOnly = false,
  isMobile = false,
  showBorder = false,
  toolbar,
}: {
  image: string;
  loading: boolean;
  renderError: string | null;
  code: string;
  viewMode: CodeDrawingView;
  readOnly?: boolean;
  isMobile?: boolean;
  showBorder?: boolean;
  toolbar?: React.ReactNode;
}) {
  const showPreview = viewMode === 'split' || viewMode === 'preview';
 
  return (
    <div
      className={`flex min-w-0 flex-1 flex-col ${isMobile ? '' : 'relative'} ${
        showBorder && isMobile ? 'border-b' : ''
      }`}
    >
      {toolbar && (
        <div
          className={
            isMobile
              ? 'mt-2 mb-2 flex justify-end px-2'
              : 'absolute right-2 z-10 mt-2'
          }
        >
          {toolbar}
        </div>
      )}
 
      {showPreview ? (
        <div className="flex flex-1 items-center justify-center rounded-md bg-muted/30 p-4">
          {loading && <div className="text-muted-foreground">Loading...</div>}
          {!loading &&
            image && (
              // oxlint-disable-next-line nextjs/no-img-element -- [P1 local-invariant] The renderer returns an ephemeral preview data URL; optimization and remote loading do not apply.
              <img
                src={image}
                alt="code drawing"
                className="max-h-full max-w-full object-contain"
              />
            )}
          {!loading && !image && renderError && (
            <div className="text-destructive" title={renderError}>
              Could not render preview. Edit the source to retry.
            </div>
          )}
          {!loading && !image && !renderError && (
            <div className="text-muted-foreground">
              {code.trim() ? 'Rendering...' : 'Preview will appear here'}
            </div>
          )}
        </div>
      ) : (
        <div className="pointer-events-none flex flex-1 items-center justify-center rounded-md border bg-muted/30 p-4 opacity-0">
          {/* Placeholder to maintain height */}
        </div>
      )}
    </div>
  );
}
 
export const CodeDrawingKit = [
  CodeDrawingPlugin.configure({ component: CodeDrawingElement }),
];
'use client';
 
import {
  type CodeDrawingLanguage,
  type CodeDrawingView,
  CODE_DRAWING_LANGUAGES,
  DEFAULT_MIN_HEIGHT,
  DOWNLOAD_FILENAME,
  RENDER_DEBOUNCE_DELAY,
  CODE_DRAWING_VIEWS,
  downloadImage,
  renderCodeDrawing,
} from '@platejs/code-drawing';
import { CodeDrawingPlugin } from '@platejs/code-drawing/react';
import { DownloadIcon, Trash2 } from 'lucide-react';
import {
  type PlateElementProps,
  PlateElement,
  useEditor,
  useEditorReadOnly,
  useEditorSelector,
  useElement,
  useElementSelected,
  useFocusedLast,



































































































































































































































































































































































































































































































































































  • CodeDrawingElement: Renders code drawing elements with inline editing and preview.

Add Kit

Add the kit to your plugins:

import { createPlateEditor } from 'platejs/react';
import { CodeDrawingKit } from '@/components/editor/code-drawing';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    ...CodeDrawingKit,
  ],
});
import { createPlateEditor } from 'platejs/react';
import { CodeDrawingKit } from '@/components/editor/code-drawing';
 
const editor = createPlateEditor




Manual Usage

Installation

pnpm add @platejs/code-drawing
pnpm add @platejs/code-drawing

Add Plugin

Include CodeDrawingPlugin in your Plate plugins array when creating the editor.

import { CodeDrawingPlugin } from '@platejs/code-drawing/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    CodeDrawingPlugin,
  ],
});
import { CodeDrawingPlugin } from '@platejs/code-drawing/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    CodeDrawingPlugin,
  ],
});

Configure Plugin

Configure the code drawing plugin with custom components.

import { CodeDrawingPlugin } from '@platejs/code-drawing/react';
import { createPlateEditor } from 'platejs/react';
import { CodeDrawingElement } from '@/components/editor/code-drawing';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    CodeDrawingPlugin.configure({ component: CodeDrawingElement }),
  ],
});
import { CodeDrawingPlugin } from '@platejs/code-drawing/react';








  • component: Assigns CodeDrawingElement to render code drawing elements.

Add Toolbar Button

You can add a toolbar button to insert code drawing elements. Add this item to the Insert Toolbar Button:

import { PLUGINS } from 'platejs';
 
const insertItem = {
  icon: <Code2Icon />,
  label: 'Code Drawing',
  value: PLUGINS.codeDrawing,
};
import { PLUGINS } from 'platejs';
 
const insertItem = {
  icon: <Code2Icon />,
  label: 'Code Drawing',
  value: PLUGINS.codeDrawing,
};

Plugins

CodeDrawingPlugin

Plugin for rendering code drawings with inline editing and preview capabilities.

API

editor.plugin(BaseCodeDrawingPlugin).update.insert

Inserts a code drawing element at the current selection.

Parameters

    Construction values for the code drawing element. Schema defaults make every canonical field present.

    Use after?: NodeTarget to choose a reference block and select?: boolean to select the drawing. Without a reference or selection, the command appends the drawing to the document.

Types

CodeDrawingElement

The code drawing element type.

Attributes

    The serialized element type. Its default is codeDrawing; read the installed value from editor.plugin(BaseCodeDrawingPlugin).schema.type.

    The diagram source. Defaults to an empty string.

    The lowercase diagram-language identifier. Defaults to mermaid.

    The persisted presentation view. Defaults to split.

CodeDrawingLanguage

The type of diagram supported.

Returns

    The persisted diagram language.

CodeDrawingView

The view mode for code drawing elements.

Returns

    The view mode.

    • 'split': Shows code and preview together.
    • 'code': Shows only the code editor.
    • 'preview': Shows only the rendered preview.

Utilities

renderCodeDrawing

Renders a diagram from code based on its language.

Parameters

    The diagram language to render.

    The code content for the diagram.

Returns

    A promise that resolves to the rendered image data URL.

downloadImage

Downloads an image from a data URL.

Parameters

    The image data URL to download.

    The filename for the downloaded image.

    • Default: 'code-drawing.png'
usePath,
} from 'platejs/react';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useIsMobile } from '@/hooks/use-mobile';
import {
FloatingPopover,
FloatingPopoverAnchor,
FloatingPopoverContent,
} from '@/components/editor/floating-popover';
const languageLabels: Record<CodeDrawingLanguage, string> = {
flowchart: 'Flowchart',
graphviz: 'Graphviz',
mermaid: 'Mermaid',
plantuml: 'PlantUML',
};
const viewLabels: Record<CodeDrawingView, string> = {
code: 'Code',
preview: 'Preview',
split: 'Split',
};
const isCodeDrawingLanguage = (
value: string | null
): value is CodeDrawingLanguage =>
CODE_DRAWING_LANGUAGES.some((language) => language === value);
const isCodeDrawingView = (value: string | null): value is CodeDrawingView =>
CODE_DRAWING_VIEWS.some((view) => view === value);
export function CodeDrawingElement(
props: PlateElementProps<typeof CodeDrawingPlugin>
) {
const { children } = props;
const isMobile = useIsMobile();
const editor = useEditor();
const readOnly = useEditorReadOnly();
const selected = useElementSelected();
const isFocusedLast = useFocusedLast();
const element = useElement(CodeDrawingPlugin);
const path = usePath();
const [loading, setLoading] = React.useState(false);
const [image, setImage] = React.useState('');
const [renderError, setRenderError] = React.useState<string | null>(null);
React.useEffect(() => {
let cancelled = false;
const render = async () => {
if (!element.code.trim()) {
setImage('');
setRenderError(null);
setLoading(false);
return;
}
setRenderError(null);
setLoading(true);
try {
const imageData = await renderCodeDrawing(
element.language,
element.code
);
if (!cancelled) setImage(imageData);
} catch (error) {
if (!cancelled) {
const message =
error instanceof Error ? error.message : 'Rendering failed';
console.error(message);
setImage('');
setRenderError(message);
}
} finally {
if (!cancelled) setLoading(false);
}
};
const timeout = window.setTimeout(() => {
void render();
}, RENDER_DEBOUNCE_DELAY);
return () => {
cancelled = true;
window.clearTimeout(timeout);
};
}, [element.code, element.language]);
const handleDownload = React.useCallback(() => {
if (!image) return;
downloadImage(image, DOWNLOAD_FILENAME);
}, [image]);
const handleCodeChange = React.useCallback(
(code: string) => {
if (path) {
editor.update.nodes.set({ code }, { at: path });
}
},
[editor, path]
);
const handleLanguageChange = React.useCallback(
(language: CodeDrawingLanguage) => {
if (path) {
editor.update.nodes.set({ language }, { at: path });
}
},
[editor, path]
);
const handleViewChange = React.useCallback(
(view: CodeDrawingView) => {
if (path) {
editor.update.nodes.set({ view }, { at: path });
}
},
[editor, path]
);
const { code, language, view } = element;
const selectionCollapsed = useEditorSelector((innerEditor) =>
innerEditor.read.selection.isCollapsed()
);
const open = isFocusedLast && !readOnly && selected && selectionCollapsed;
const content = (
<PlateElement {...props}>
<div contentEditable={false}>
<div>
<CodeDrawingPreview
code={code}
language={language}
view={view}
image={image}
loading={loading}
renderError={renderError}
onCodeChange={handleCodeChange}
onLanguageChange={handleLanguageChange}
onViewChange={handleViewChange}
readOnly={readOnly}
isMobile={isMobile}
/>
</div>
</div>
{children}
</PlateElement>
);
if (readOnly) {
return content;
}
return (
<FloatingPopover open={open} modal={false}>
<FloatingPopoverAnchor element={content} />
<FloatingPopoverContent
className="w-auto p-1"
contentEditable={false}
onInitialFocus={(e) => {
e.preventDefault();
}}
>
<div className="flex items-center gap-1">
{image && (
<Button
size="icon"
variant="ghost"
className="size-8"
onClick={handleDownload}
title="Export"
>
<DownloadIcon className="size-4" />
</Button>
)}
<Button
size="icon"
variant="ghost"
className="size-8"
onClick={() => {
if (!readOnly && path) editor.update.nodes.remove({ at: path });
}}
title="Delete"
>
<Trash2 className="size-4" />
</Button>
</div>
</FloatingPopoverContent>
</FloatingPopover>
);
}
function CodeDrawingPreview({
code,
language,
view,
image,
loading,
renderError,
onCodeChange,
onLanguageChange,
onViewChange,
readOnly = false,
isMobile = false,
}: {
code: string;
language: CodeDrawingLanguage;
view: CodeDrawingView;
image: string;
loading: boolean;
renderError: string | null;
onCodeChange: (code: string) => void;
onLanguageChange: (language: CodeDrawingLanguage) => void;
onViewChange: (view: CodeDrawingView) => void;
readOnly?: boolean;
isMobile?: boolean;
}) {
const viewMode = view;
const showCode = viewMode === 'split' || viewMode === 'code';
const showBorder = viewMode === 'split';
const handleCodeChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
onCodeChange(e.target.value);
},
[onCodeChange]
);
const toolbar = readOnly ? null : (
<CodeDrawingToolbar
language={language}
viewMode={viewMode}
readOnly={readOnly}
isMobile={isMobile}
onLanguageChange={onLanguageChange}
onViewChange={onViewChange}
/>
);
return (
<div
className={`flex ${
isMobile ? 'flex-col-reverse' : 'flex-col'
} group my-4 w-full items-stretch border bg-muted/50 md:flex-row`}
style={{
minHeight: `${DEFAULT_MIN_HEIGHT}px`,
}}
>
{showCode && (
<CodeDrawingTextarea
code={code}
viewMode={viewMode}
readOnly={readOnly}
isMobile={isMobile}
showBorder={showBorder}
onCodeChange={handleCodeChange}
toolbar={viewMode === 'code' ? toolbar : null}
/>
)}
{viewMode !== 'code' && (
<CodeDrawingPreviewArea
image={image}
loading={loading}
renderError={renderError}
code={code}
viewMode={viewMode}
readOnly={readOnly}
isMobile={isMobile}
showBorder={showBorder}
toolbar={toolbar}
/>
)}
</div>
);
}
function CodeDrawingToolbar({
language,
viewMode,
readOnly = false,
isMobile = false,
onLanguageChange,
onViewChange,
}: {
language: CodeDrawingLanguage;
viewMode: CodeDrawingView;
readOnly?: boolean;
isMobile?: boolean;
onLanguageChange: (language: CodeDrawingLanguage) => void;
onViewChange: (view: CodeDrawingView) => void;
}) {
const [toolbarVisible, setToolbarVisible] = React.useState(false);
const [languageSelectOpen, setLanguageSelectOpen] = React.useState(false);
const [viewSelectOpen, setViewSelectOpen] = React.useState(false);
const opacityClass =
isMobile || toolbarVisible || languageSelectOpen || viewSelectOpen
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100';
const positionClass = isMobile
? 'flex items-center gap-2'
: 'absolute right-2 z-10 flex items-center gap-2';
return (
<div
role="toolbar"
tabIndex={-1}
className={`${positionClass} transition-opacity ${opacityClass}`}
onMouseEnter={() => {
setToolbarVisible(true);
}}
onMouseLeave={() => {
if (!languageSelectOpen && !viewSelectOpen) {
setToolbarVisible(false);
}
}}
>
{!readOnly && (
<Select
value={language}
onValueChange={(nextLanguage) => {
if (isCodeDrawingLanguage(nextLanguage)) {
onLanguageChange(nextLanguage);
}
}}
open={languageSelectOpen}
onOpenChange={setLanguageSelectOpen}
>
<SelectTrigger
className={`h-8 w-[120px] border-0 bg-muted/50 text-xs shadow-none ${
isMobile ? '' : 'transition-colors hover:bg-zinc-200'
}`}
>
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[100]">
{CODE_DRAWING_LANGUAGES.map((innerLanguage) => (
<SelectItem key={innerLanguage} value={innerLanguage}>
{languageLabels[innerLanguage]}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{!readOnly && (
<Select
value={viewMode}
onValueChange={(nextView) => {
if (isCodeDrawingView(nextView)) {
onViewChange(nextView);
}
}}
open={viewSelectOpen}
onOpenChange={setViewSelectOpen}
>
<SelectTrigger
className={`h-8 w-[80px] border-0 bg-muted/50 text-xs shadow-none ${
isMobile ? '' : 'transition-colors hover:bg-zinc-200'
}`}
>
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[100]">
{CODE_DRAWING_VIEWS.map((view) => (
<SelectItem key={view} value={view}>
{viewLabels[view]}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
);
}
function CodeDrawingTextarea({
code,
viewMode,
readOnly = false,
isMobile = false,
showBorder = false,
onCodeChange,
toolbar,
}: {
code: string;
viewMode: CodeDrawingView;
readOnly?: boolean;
isMobile?: boolean;
showBorder?: boolean;
onCodeChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
toolbar?: React.ReactNode;
}) {
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const isCodeOnlyMode = viewMode === 'code';
const [internalCode, setInternalCode] = React.useState(code);
const lastExternalCodeRef = React.useRef(code);
React.useEffect(() => {
if (code !== lastExternalCodeRef.current) {
lastExternalCodeRef.current = code;
setInternalCode(code);
}
}, [code]);
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setInternalCode(newValue);
onCodeChange(e);
},
[onCodeChange]
);
return (
<div
className={`${
isCodeOnlyMode ? 'w-full' : 'min-w-0 flex-1'
} flex flex-col ${isCodeOnlyMode && !isMobile ? 'relative' : ''} ${
showBorder && !isMobile ? 'border-r' : ''
}`}
>
{toolbar && isCodeOnlyMode && (
<div
className={
isMobile
? 'mt-2 mb-2 flex justify-end px-2'
: 'absolute right-2 z-10 mt-2'
}
>
{toolbar}
</div>
)}
<div className="relative flex-1 rounded-md">
<pre
className="m-0 overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid"
style={{ minHeight: `${DEFAULT_MIN_HEIGHT}px`, height: '100%' }}
>
<code className="block h-full w-full">
<textarea
ref={textareaRef}
value={internalCode}
onChange={handleChange}
readOnly={readOnly}
className="m-0 h-full w-full resize-none overflow-auto border-0 bg-transparent p-0 font-mono text-sm outline-none"
style={{ minHeight: `${DEFAULT_MIN_HEIGHT}px` }}
placeholder="Enter your code here..."
spellCheck={false}
/>
</code>
</pre>
</div>
</div>
);
}
function CodeDrawingPreviewArea({
image,
loading,
renderError,
code,
viewMode,
readOnly: _readOnly = false,
isMobile = false,
showBorder = false,
toolbar,
}: {
image: string;
loading: boolean;
renderError: string | null;
code: string;
viewMode: CodeDrawingView;
readOnly?: boolean;
isMobile?: boolean;
showBorder?: boolean;
toolbar?: React.ReactNode;
}) {
const showPreview = viewMode === 'split' || viewMode === 'preview';
return (
<div
className={`flex min-w-0 flex-1 flex-col ${isMobile ? '' : 'relative'} ${
showBorder && isMobile ? 'border-b' : ''
}`}
>
{toolbar && (
<div
className={
isMobile
? 'mt-2 mb-2 flex justify-end px-2'
: 'absolute right-2 z-10 mt-2'
}
>
{toolbar}
</div>
)}
{showPreview ? (
<div className="flex flex-1 items-center justify-center rounded-md bg-muted/30 p-4">
{loading && <div className="text-muted-foreground">Loading...</div>}
{!loading &&
image && (
// oxlint-disable-next-line nextjs/no-img-element -- [P1 local-invariant] The renderer returns an ephemeral preview data URL; optimization and remote loading do not apply.
<img
src={image}
alt="code drawing"
className="max-h-full max-w-full object-contain"
/>
)}
{!loading && !image && renderError && (
<div className="text-destructive" title={renderError}>
Could not render preview. Edit the source to retry.
</div>
)}
{!loading && !image && !renderError && (
<div className="text-muted-foreground">
{code.trim() ? 'Rendering...' : 'Preview will appear here'}
</div>
)}
</div>
) : (
<div className="pointer-events-none flex flex-1 items-center justify-center rounded-md border bg-muted/30 p-4 opacity-0">
{/* Placeholder to maintain height */}
</div>
)}
</div>
);
}
export const CodeDrawingKit = [
CodeDrawingPlugin.configure({ component: CodeDrawingElement }),
];
({
plugins: [
// ...otherPlugins,
...CodeDrawingKit,
],
});
import { createPlateEditor } from 'platejs/react';
import { CodeDrawingElement } from '@/components/editor/code-drawing';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
CodeDrawingPlugin.configure({ component: CodeDrawingElement }),
],
});