From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Discussion
    • Comments
    • Suggestion
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • List Classic
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Toggle
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Cursor Overlay
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Toolbar

PreviousNext

Fixed and floating toolbars for your editor.

PlusFixed ToolbarFloating Toolbar
Loading…
TabbableYjs

On This Page

FeaturesKit UsageInstallationAdd KitManual UsageCreate PluginsCustomizing Fixed Toolbar ButtonsCustomizing Floating Toolbar ButtonsCreating Custom ButtonCreating Mark ButtonTurn Into Toolbar ButtonInsert Toolbar ButtonPlate PlusPluginsFixedToolbarKitFloatingToolbarKit
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Fixed Toolbar: Persistent toolbar that sticks to the top of the editor
  • Floating Toolbar: Contextual toolbar that appears on text selection
  • Customizable Buttons: Easily add, remove, and reorder toolbar buttons
  • Responsive Design: Adapts to different screen sizes and content
  • Plugin Integration: Seamless integration with Plate plugins and UI components
Report an issue

Kit Usage

Installation

The fastest way to add toolbar functionality is with the FixedToolbarKit and FloatingToolbarKit, which include pre-configured toolbar plugins along with their Plate UI components.

'use client';
 
import {
  BoldPlugin,
  CodePlugin,
  HighlightPlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  FontBackgroundColorPlugin,
  FontColorPlugin,
} from '@platejs/basic-styles/react';
import {
  BaseAudioPlugin,
  BaseFilePlugin,
  BaseImagePlugin,
  BaseVideoPlugin,
} from '@platejs/media';
import {
  BaselineIcon,
  BoldIcon,
  Code2Icon,
  HighlighterIcon,
  ItalicIcon,
  PaintBucketIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import { useEditorReadOnly, definePlatePlugin } from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
 
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
  DEFAULT_COLORS,
  FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
  IndentToolbarButton,
  OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
  BulletedListToolbarButton,
  NumberedListToolbarButton,
  TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { ToggleToolbarButton } from './toggle-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
 
export function FixedToolbarButtons({
  children,
}: {
  children?: React.ReactNode;
} = {}) {
  const readOnly = useEditorReadOnly();
 
  return (
    <div className="flex w-full">
      {!readOnly && (
        <>
          <ToolbarGroup>
            <UndoToolbarButton />
            <RedoToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <AIToolbarButton tooltip="AI commands">
              <WandSparklesIcon />
            </AIToolbarButton>
          </ToolbarGroup>
 
          {children}
 
          <ToolbarGroup>
            <InsertToolbarButton />
            <TurnIntoToolbarButton />
            <FontSizeToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
              <BoldIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
              <ItalicIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={UnderlinePlugin}
              tooltip="Underline (⌘+U)"
            >
              <UnderlineIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={StrikethroughPlugin}
              tooltip="Strikethrough (⌘+⇧+M)"
            >
              <StrikethroughIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
              <Code2Icon />
            </MarkToolbarButton>
 
            <FontColorToolbarButton
              colors={DEFAULT_COLORS}
              plugin={FontColorPlugin}
              tooltip="Text color"
            >
              <BaselineIcon />
            </FontColorToolbarButton>
 
            <FontColorToolbarButton
              colors={DEFAULT_COLORS}
              plugin={FontBackgroundColorPlugin}
              tooltip="Background color"
            >
              <PaintBucketIcon />
            </FontColorToolbarButton>
          </ToolbarGroup>
 
          <ToolbarGroup>
            <AlignToolbarButton />
 
            <NumberedListToolbarButton />
            <BulletedListToolbarButton />
            <TodoListToolbarButton />
            <ToggleToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <LinkToolbarButton />
            <TableToolbarButton />
            <EmojiToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MediaToolbarButton plugin={BaseImagePlugin} />
            <MediaToolbarButton plugin={BaseVideoPlugin} />
            <MediaToolbarButton plugin={BaseAudioPlugin} />
            <MediaToolbarButton plugin={BaseFilePlugin} />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <LineHeightToolbarButton />
            <OutdentToolbarButton />
            <IndentToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MoreToolbarButton />
          </ToolbarGroup>
        </>
      )}
 
      <div className="grow" />
 
      <ToolbarGroup>
        <MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
          <HighlighterIcon />
        </MarkToolbarButton>
        <CommentToolbarButton />
      </ToolbarGroup>
 
      <ToolbarGroup>
        <ModeToolbarButton />
      </ToolbarGroup>
    </div>
  );
}
 
export function FixedToolbar(props: React.ComponentProps<typeof Toolbar>) {
  return (
    <Toolbar
      {...props}
      className={cn(
        'scrollbar-hide sticky top-0 left-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
        props.className
      )}
    />
  );
}
 
export const FixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
  render: {
    beforeEditable: () => (
      <FixedToolbar>
        <FixedToolbarButtons />
      </FixedToolbar>
    ),
  },
});
 
export const FixedToolbarKit = [FixedToolbarPlugin] as const;
'use client';
 
import {
  BoldPlugin,
  CodePlugin,
  HighlightPlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  FontBackgroundColorPlugin,
  FontColorPlugin,
} from '@platejs/basic-styles/react';
import {
  BaseAudioPlugin,
  BaseFilePlugin,
  BaseImagePlugin,
  BaseVideoPlugin,
} from '@platejs/media';
import {
  BaselineIcon,
  BoldIcon,
  Code2Icon,
  HighlighterIcon,
  ItalicIcon,



























































































































































































'use client';
 
import { AIChatPlugin } from '@platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  type UseVirtualFloatingOptions,
  flip,
  getSelectionBoundingClientRect,
  offset,
  useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils'











































































































































































































































































  • FixedToolbar: Renders a persistent toolbar above the editor
  • FixedToolbarButtons: Pre-configured button set for the fixed toolbar
  • FloatingToolbar: Renders a contextual toolbar on text selection
  • FloatingToolbarButtons: Pre-configured button set for the floating toolbar

Add Kit

import { createPlateEditor } from 'platejs/react';
import { FixedToolbarKit } from '@/components/editor/fixed-toolbar';
import { FloatingToolbarKit } from '@/components/editor/floating-toolbar';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    ...FixedToolbarKit,
    ...FloatingToolbarKit,
  ],
});
import { createPlateEditor } from 'platejs/react'









Manual Usage

Create Plugins

import { definePlatePlugin } from 'platejs/react';
import { FixedToolbar } from '@/components/editor/fixed-toolbar';
import { FixedToolbarButtons } from '@/components/editor/fixed-toolbar';
import { FloatingToolbar } from '@/components/editor/floating-toolbar';
import { FloatingToolbarButtons } from '@/components/editor/floating-toolbar';
 
const fixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
  render: {
    beforeEditable: () => (
      <FixedToolbar>
        <FixedToolbarButtons />
      </FixedToolbar>
    ),
  },
});
 
const floatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
  render: {
    afterEditable: () => (
      <FloatingToolbar>
        <FloatingToolbarButtons />
      </FloatingToolbar>
    ),
  },
});
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    fixedToolbarPlugin,
    floatingToolbarPlugin,
  ],
});
import { definePlatePlugin } from 'platejs/react';
import { FixedToolbar } from '@/components/editor/fixed-toolbar';
import { FixedToolbarButtons } from '@/components/editor/fixed-toolbar';
import { FloatingToolbar } from '@/components/editor/floating-toolbar';
import { FloatingToolbarButtons } from '@/components/editor/floating-toolbar';
 
const fixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
  render: {
    beforeEditable: () => (
      <FixedToolbar>
        <FixedToolbarButtons />
      </FixedToolbar>
    ),
  },
});
 
















  • render.beforeEditable: Renders FixedToolbar above the editor content
  • render.afterEditable: Renders FloatingToolbar as an overlay after the editor

Customizing Fixed Toolbar Buttons

The FixedToolbarButtons component contains the default set of buttons for the fixed toolbar.

'use client';
 
import {
  BoldPlugin,
  CodePlugin,
  HighlightPlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  FontBackgroundColorPlugin,
  FontColorPlugin,
} from '@platejs/basic-styles/react';
import {
  BaseAudioPlugin,
  BaseFilePlugin,
  BaseImagePlugin,
  BaseVideoPlugin,
} from '@platejs/media';
import {
  BaselineIcon,
  BoldIcon,
  Code2Icon,
  HighlighterIcon,




























































































































































































To customize it, you can edit components/editor/fixed-toolbar.tsx.

Customizing Floating Toolbar Buttons

Similarly, you can customize the floating toolbar by editing components/editor/floating-toolbar.tsx.

'use client';
 
import { AIChatPlugin } from '@platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  type UseVirtualFloatingOptions,
  flip,
  getSelectionBoundingClientRect,
  offset,
  useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils'











































































































































































































































































Creating Custom Button

This example shows a button that inserts custom text into the editor.

import { useEditor } from 'platejs/react';
import { CustomIcon } from 'lucide-react';
import { ToolbarButton } from '@/components/editor/toolbar';
 
export function CustomToolbarButton() {
  const editor = useEditor();
 
  return (
    <ToolbarButton
      onClick={() => {
        // Custom action
        editor.update((tx) => {
          tx.text.insert('Custom text');
        });
      }}
      tooltip="Custom Action"




Creating Mark Button

For toggling marks like bold or italic, you can use the MarkToolbarButton component. It simplifies the process by handling the toggle state and action automatically.

This example creates a "Bold" button.

import { BoldIcon } from 'lucide-react';
 
import { MarkToolbarButton } from '@/components/editor/mark-toolbar-button';
 
export function BoldToolbarButton() {
  return (
    <MarkToolbarButton nodeType="bold" tooltip="Bold (⌘+B)">
      <BoldIcon />
    </MarkToolbarButton>
  );
}
import { BoldIcon } from









  • nodeType: Specifies the mark to toggle (e.g., bold, italic).
  • tooltip: Provides a helpful tooltip for the button.
  • MarkToolbarButton reads mark state and handles toggling directly in the component.

Turn Into Toolbar Button

The TurnIntoToolbarButton provides a dropdown menu to convert the current block into different types (headings, lists, quotes, etc.).

'use client';
import { ElementApi } from '@platejs/plite';
import { PLUGINS } from '@platejs/utils';
import {
  ChevronRightIcon,
  Code2Icon,
  Columns3Icon,
  FileCodeIcon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  Heading4Icon,
  Heading5Icon,
  Heading6Icon,
  ListIcon,
  ListOrderedIcon,
  PilcrowIcon,
  QuoteIcon,
  SquareIcon,
} from 'lucide-react';
import { useEditor, useSelectionFragmentProp } from 'platejs/react';
import * as React from





































































































































































































To add a new block type to the turn-into options, edit the turnIntoItems array:

const turnIntoItems = [
  // ... existing items
  {
    icon: <CustomIcon />,
    keywords: ['custom', 'special'],
    label: 'Custom Block',
    value: 'custom-block',
  },
];
const turnIntoItems = [
  // ... existing items
  {
    icon: <CustomIcon />,
    keywords: ['custom', 'special'],



Insert Toolbar Button

The InsertToolbarButton provides a dropdown menu to insert various elements (blocks, lists, media, inline elements).

'use client';
 
import {
  CalendarIcon,
  ChevronRightIcon,
  Code2,
  Columns3Icon,
  FileCodeIcon,
  FilmIcon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  ImageIcon,
  Link2Icon,
  ListIcon,
  ListOrderedIcon,
  MinusIcon,
  PenToolIcon,
  PilcrowIcon,
  PlusIcon,
  QuoteIcon,
  RadicalIcon,
  SquareIcon,
  SuperscriptIcon,
  TableIcon,
  TableOfContentsIcon,
} from 'lucide-react';
import { PLUGINS } from 'platejs';















































































































































































































































To add a new insertable item, add it to the appropriate group in the groups array:

{
  group: 'Basic blocks',
  items: [
    // ... existing items
    {
      icon: <CustomIcon />,
      label: 'Custom Block',
      value: 'custom-block',
    },
  ].map((item) => ({
    ...item,
    onSelect: (editor, value) => {
      insertBlock(editor, value);
    },
  })),
}

Plate Plus

  • Color picker
  • Mark as equation
  • Beautifully crafted UI
Get the code

Plugins

FixedToolbarKit

Plugin that renders a fixed toolbar above the editor content.

Options

    Renders the fixed toolbar before the editor content. Contains FixedToolbarButtons by default.

FloatingToolbarKit

Plugin that renders a floating toolbar that appears on text selection.

Options

    Renders the floating toolbar as an overlay after the editor. Contains FloatingToolbarButtons by default.

PaintBucketIcon,
StrikethroughIcon,
UnderlineIcon,
WandSparklesIcon,
} from 'lucide-react';
import { useEditorReadOnly, definePlatePlugin } from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
DEFAULT_COLORS,
FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
IndentToolbarButton,
OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
BulletedListToolbarButton,
NumberedListToolbarButton,
TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { ToggleToolbarButton } from './toggle-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FixedToolbarButtons({
children,
}: {
children?: React.ReactNode;
} = {}) {
const readOnly = useEditorReadOnly();
return (
<div className="flex w-full">
{!readOnly && (
<>
<ToolbarGroup>
<UndoToolbarButton />
<RedoToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
</AIToolbarButton>
</ToolbarGroup>
{children}
<ToolbarGroup>
<InsertToolbarButton />
<TurnIntoToolbarButton />
<FontSizeToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontColorPlugin}
tooltip="Text color"
>
<BaselineIcon />
</FontColorToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontBackgroundColorPlugin}
tooltip="Background color"
>
<PaintBucketIcon />
</FontColorToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<AlignToolbarButton />
<NumberedListToolbarButton />
<BulletedListToolbarButton />
<TodoListToolbarButton />
<ToggleToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<LinkToolbarButton />
<TableToolbarButton />
<EmojiToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MediaToolbarButton plugin={BaseImagePlugin} />
<MediaToolbarButton plugin={BaseVideoPlugin} />
<MediaToolbarButton plugin={BaseAudioPlugin} />
<MediaToolbarButton plugin={BaseFilePlugin} />
</ToolbarGroup>
<ToolbarGroup>
<LineHeightToolbarButton />
<OutdentToolbarButton />
<IndentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MoreToolbarButton />
</ToolbarGroup>
</>
)}
<div className="grow" />
<ToolbarGroup>
<MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
<HighlighterIcon />
</MarkToolbarButton>
<CommentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<ModeToolbarButton />
</ToolbarGroup>
</div>
);
}
export function FixedToolbar(props: React.ComponentProps<typeof Toolbar>) {
return (
<Toolbar
{...props}
className={cn(
'scrollbar-hide sticky top-0 left-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
props.className
)}
/>
);
}
export const FixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
render: {
beforeEditable: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
});
export const FixedToolbarKit = [FixedToolbarPlugin] as const;
;
import {
BoldIcon,
Code2Icon,
ItalicIcon,
StrikethroughIcon,
UnderlineIcon,
WandSparklesIcon,
} from 'lucide-react';
import {
useEditorReadOnly,
definePlatePlugin,
useEditor,
useEditorId,
useEditorSelector,
useEventEditorValue,
usePluginStore,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FloatingToolbarButtons() {
const readOnly = useEditorReadOnly();
return (
<>
{!readOnly && (
<>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
Ask AI
</AIToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<TurnIntoToolbarButton />
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<InlineEquationToolbarButton />
<LinkToolbarButton />
</ToolbarGroup>
</>
)}
<ToolbarGroup>
<CommentToolbarButton />
<SuggestionToolbarButton />
{!readOnly && <MoreToolbarButton />}
</ToolbarGroup>
</>
);
}
type FloatingToolbarOptions = {
floatingOptions?: UseVirtualFloatingOptions;
hideToolbar?: boolean;
showWhenReadOnly?: boolean;
};
type FloatingToolbarProps = React.ComponentProps<typeof Toolbar> & {
options?: FloatingToolbarOptions;
};
export function FloatingToolbar(props: FloatingToolbarProps) {
const editorId = useEditorId();
const hasNodeSelection = useEditorSelector(
(editor) => editor.read.selection.nodes().length > 0,
{ id: editorId }
);
if (hasNodeSelection) return null;
return <TextFloatingToolbar {...props} />;
}
function TextFloatingToolbar({
children,
className,
options,
...props
}: FloatingToolbarProps) {
const editorId = useEditorId();
const focusedEditorId = useEventEditorValue('focus');
const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
const editor = useEditor({ id: editorId });
const selectionExpanded = useEditorSelector(
(innerEditor) => innerEditor.read.selection.isExpanded(),
{ id: editorId }
);
const selectionText = useEditorSelector(
(innerEditor2) => innerEditor2.read.text.string(),
{ id: editorId }
);
const selectionRange = useEditorSelector(
(innerEditor3) => innerEditor3.read.selection(),
{ id: editorId }
);
const waitForCollapsedSelection = useEditorSelector(
(innerEditor4, previous = false) => {
if (!innerEditor4.read.selection.isExpanded()) return false;
if (editorId !== focusedEditorId) return true;
return previous;
},
{ id: editorId }
);
const readOnly = useEditorReadOnly();
const [dismissedSelection, setDismissedSelection] =
React.useState<typeof selectionRange>(null);
const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
null
);
const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
const open =
selectionExpanded &&
!!selectionText &&
(editorId === focusedEditorId || ownedOverlayOpen) &&
!isFloatingLinkOpen &&
!isAIChatOpen &&
!options?.hideToolbar &&
(!readOnly || !!options?.showWhenReadOnly) &&
(!waitForCollapsedSelection || readOnly || ownedOverlayOpen) &&
mouseDownOpen !== false &&
dismissedSelection !== selectionRange;
const floating = useVirtualFloating(
mergeProps<UseVirtualFloatingOptions>(
{
open,
getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
onOpenChange: (nextOpen) => {
setDismissedSelection(nextOpen ? null : selectionRange);
},
},
{
middleware: [
offset(12),
flip({
fallbackPlacements: [
'top-start',
'top-end',
'bottom-start',
'bottom-end',
],
padding: 12,
}),
],
placement: 'top',
...options?.floatingOptions,
}
)
);
const openStateRef = React.useRef(open);
React.useEffect(() => {
openStateRef.current = open;
}, [open]);
React.useEffect(() => {
const onMouseUp = () => {
setMouseDownOpen(null);
};
const onMouseDown = () => {
setMouseDownOpen(openStateRef.current);
};
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousedown', onMouseDown);
return () => {
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('mousedown', onMouseDown);
};
}, []);
const editorVersion = useEditorSelector(
(innerEditor5) => innerEditor5.read.lastCommit()?.version ?? 0,
{ id: editorId }
);
const updateFloating = floating.update;
React.useEffect(() => {
if (open) updateFloating?.();
}, [editorVersion, open, updateFloating]);
const clickOutsideRef = useOnClickOutside(
() => {
setDismissedSelection(selectionRange);
},
{ ignoreClass: 'ignore-click-outside/toolbar' }
);
const ref = useComposedRef<HTMLDivElement>(
props.ref,
floating.refs.setFloating
);
if (!open) return null;
return (
<div ref={clickOutsideRef}>
<Toolbar
{...props}
ref={ref}
onOverlayOpenChange={setOwnedOverlayOpen}
style={floating.style}
className={cn(
'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
'max-w-[80vw]',
className
)}
>
{children}
</Toolbar>
</div>
);
}
export const FloatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
render: {
afterEditable: () => (
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
),
},
});
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;
'use client';
 
import { AIChatPlugin } from '@platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  type UseVirtualFloatingOptions,
  flip,
  getSelectionBoundingClientRect,
  offset,
  useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils';
import {
  BoldIcon,
  Code2Icon,
  ItalicIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import {
  useEditorReadOnly,
  definePlatePlugin,
  useEditor,
  useEditorId,
  useEditorSelector,
  useEventEditorValue,
  usePluginStore,
} from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
 
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
 
export function FloatingToolbarButtons() {
  const readOnly = useEditorReadOnly();
 
  return (
    <>
      {!readOnly && (
        <>
          <ToolbarGroup>
            <AIToolbarButton tooltip="AI commands">
              <WandSparklesIcon />
              Ask AI
            </AIToolbarButton>
          </ToolbarGroup>
 
          <ToolbarGroup>
            <TurnIntoToolbarButton />
 
            <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
              <BoldIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
              <ItalicIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={UnderlinePlugin}
              tooltip="Underline (⌘+U)"
            >
              <UnderlineIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={StrikethroughPlugin}
              tooltip="Strikethrough (⌘+⇧+M)"
            >
              <StrikethroughIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
              <Code2Icon />
            </MarkToolbarButton>
 
            <InlineEquationToolbarButton />
 
            <LinkToolbarButton />
          </ToolbarGroup>
        </>
      )}
 
      <ToolbarGroup>
        <CommentToolbarButton />
        <SuggestionToolbarButton />
 
        {!readOnly && <MoreToolbarButton />}
      </ToolbarGroup>
    </>
  );
}
 
type FloatingToolbarOptions = {
  floatingOptions?: UseVirtualFloatingOptions;
  hideToolbar?: boolean;
  showWhenReadOnly?: boolean;
};
 
type FloatingToolbarProps = React.ComponentProps<typeof Toolbar> & {
  options?: FloatingToolbarOptions;
};
 
export function FloatingToolbar(props: FloatingToolbarProps) {
  const editorId = useEditorId();
  const hasNodeSelection = useEditorSelector(
    (editor) => editor.read.selection.nodes().length > 0,
    { id: editorId }
  );
 
  if (hasNodeSelection) return null;
 
  return <TextFloatingToolbar {...props} />;
}
 
function TextFloatingToolbar({
  children,
  className,
  options,
  ...props
}: FloatingToolbarProps) {
  const editorId = useEditorId();
  const focusedEditorId = useEventEditorValue('focus');
  const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
  const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
  const editor = useEditor({ id: editorId });
  const selectionExpanded = useEditorSelector(
    (innerEditor) => innerEditor.read.selection.isExpanded(),
    { id: editorId }
  );
  const selectionText = useEditorSelector(
    (innerEditor2) => innerEditor2.read.text.string(),
    { id: editorId }
  );
  const selectionRange = useEditorSelector(
    (innerEditor3) => innerEditor3.read.selection(),
    { id: editorId }
  );
  const waitForCollapsedSelection = useEditorSelector(
    (innerEditor4, previous = false) => {
      if (!innerEditor4.read.selection.isExpanded()) return false;
      if (editorId !== focusedEditorId) return true;
 
      return previous;
    },
    { id: editorId }
  );
  const readOnly = useEditorReadOnly();
  const [dismissedSelection, setDismissedSelection] =
    React.useState<typeof selectionRange>(null);
  const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
    null
  );
  const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
  const open =
    selectionExpanded &&
    !!selectionText &&
    (editorId === focusedEditorId || ownedOverlayOpen) &&
    !isFloatingLinkOpen &&
    !isAIChatOpen &&
    !options?.hideToolbar &&
    (!readOnly || !!options?.showWhenReadOnly) &&
    (!waitForCollapsedSelection || readOnly || ownedOverlayOpen) &&
    mouseDownOpen !== false &&
    dismissedSelection !== selectionRange;
  const floating = useVirtualFloating(
    mergeProps<UseVirtualFloatingOptions>(
      {
        open,
        getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
        onOpenChange: (nextOpen) => {
          setDismissedSelection(nextOpen ? null : selectionRange);
        },
      },
      {
        middleware: [
          offset(12),
          flip({
            fallbackPlacements: [
              'top-start',
              'top-end',
              'bottom-start',
              'bottom-end',
            ],
            padding: 12,
          }),
        ],
        placement: 'top',
        ...options?.floatingOptions,
      }
    )
  );
  const openStateRef = React.useRef(open);
 
  React.useEffect(() => {
    openStateRef.current = open;
  }, [open]);
 
  React.useEffect(() => {
    const onMouseUp = () => {
      setMouseDownOpen(null);
    };
    const onMouseDown = () => {
      setMouseDownOpen(openStateRef.current);
    };
 
    document.addEventListener('mouseup', onMouseUp);
    document.addEventListener('mousedown', onMouseDown);
 
    return () => {
      document.removeEventListener('mouseup', onMouseUp);
      document.removeEventListener('mousedown', onMouseDown);
    };
  }, []);
 
  const editorVersion = useEditorSelector(
    (innerEditor5) => innerEditor5.read.lastCommit()?.version ?? 0,
    { id: editorId }
  );
  const updateFloating = floating.update;
 
  React.useEffect(() => {
    if (open) updateFloating?.();
  }, [editorVersion, open, updateFloating]);
 
  const clickOutsideRef = useOnClickOutside(
    () => {
      setDismissedSelection(selectionRange);
    },
    { ignoreClass: 'ignore-click-outside/toolbar' }
  );
 
  const ref = useComposedRef<HTMLDivElement>(
    props.ref,
    floating.refs.setFloating
  );
 
  if (!open) return null;
 
  return (
    <div ref={clickOutsideRef}>
      <Toolbar
        {...props}
        ref={ref}
        onOverlayOpenChange={setOwnedOverlayOpen}
        style={floating.style}
        className={cn(
          'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
          'max-w-[80vw]',
          className
        )}
      >
        {children}
      </Toolbar>
    </div>
  );
}
 
export const FloatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
  render: {
    afterEditable: () => (
      <FloatingToolbar>
        <FloatingToolbarButtons />
      </FloatingToolbar>
    ),
  },
});
 
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;
;
import { FixedToolbarKit } from '@/components/editor/fixed-toolbar';
import { FloatingToolbarKit } from '@/components/editor/floating-toolbar';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...FixedToolbarKit,
...FloatingToolbarKit,
],
});
const
floatingToolbarPlugin
=
definePlatePlugin
(
'floatingToolbar'
, {
render: {
afterEditable: () => (
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
),
},
});
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
fixedToolbarPlugin,
floatingToolbarPlugin,
],
});
ItalicIcon,
PaintBucketIcon,
StrikethroughIcon,
UnderlineIcon,
WandSparklesIcon,
} from 'lucide-react';
import { useEditorReadOnly, definePlatePlugin } from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
DEFAULT_COLORS,
FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
IndentToolbarButton,
OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
BulletedListToolbarButton,
NumberedListToolbarButton,
TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { ToggleToolbarButton } from './toggle-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FixedToolbarButtons({
children,
}: {
children?: React.ReactNode;
} = {}) {
const readOnly = useEditorReadOnly();
return (
<div className="flex w-full">
{!readOnly && (
<>
<ToolbarGroup>
<UndoToolbarButton />
<RedoToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
</AIToolbarButton>
</ToolbarGroup>
{children}
<ToolbarGroup>
<InsertToolbarButton />
<TurnIntoToolbarButton />
<FontSizeToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontColorPlugin}
tooltip="Text color"
>
<BaselineIcon />
</FontColorToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontBackgroundColorPlugin}
tooltip="Background color"
>
<PaintBucketIcon />
</FontColorToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<AlignToolbarButton />
<NumberedListToolbarButton />
<BulletedListToolbarButton />
<TodoListToolbarButton />
<ToggleToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<LinkToolbarButton />
<TableToolbarButton />
<EmojiToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MediaToolbarButton plugin={BaseImagePlugin} />
<MediaToolbarButton plugin={BaseVideoPlugin} />
<MediaToolbarButton plugin={BaseAudioPlugin} />
<MediaToolbarButton plugin={BaseFilePlugin} />
</ToolbarGroup>
<ToolbarGroup>
<LineHeightToolbarButton />
<OutdentToolbarButton />
<IndentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MoreToolbarButton />
</ToolbarGroup>
</>
)}
<div className="grow" />
<ToolbarGroup>
<MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
<HighlighterIcon />
</MarkToolbarButton>
<CommentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<ModeToolbarButton />
</ToolbarGroup>
</div>
);
}
export function FixedToolbar(props: React.ComponentProps<typeof Toolbar>) {
return (
<Toolbar
{...props}
className={cn(
'scrollbar-hide sticky top-0 left-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
props.className
)}
/>
);
}
export const FixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
render: {
beforeEditable: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
});
export const FixedToolbarKit = [FixedToolbarPlugin] as const;
'use client';
 
import {
  BoldPlugin,
  CodePlugin,
  HighlightPlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  FontBackgroundColorPlugin,
  FontColorPlugin,
} from '@platejs/basic-styles/react';
import {
  BaseAudioPlugin,
  BaseFilePlugin,
  BaseImagePlugin,
  BaseVideoPlugin,
} from '@platejs/media';
import {
  BaselineIcon,
  BoldIcon,
  Code2Icon,
  HighlighterIcon,
  ItalicIcon,
  PaintBucketIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import { useEditorReadOnly, definePlatePlugin } from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
 
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
  DEFAULT_COLORS,
  FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
  IndentToolbarButton,
  OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
  BulletedListToolbarButton,
  NumberedListToolbarButton,
  TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { ToggleToolbarButton } from './toggle-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
 
export function FixedToolbarButtons({
  children,
}: {
  children?: React.ReactNode;
} = {}) {
  const readOnly = useEditorReadOnly();
 
  return (
    <div className="flex w-full">
      {!readOnly && (
        <>
          <ToolbarGroup>
            <UndoToolbarButton />
            <RedoToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <AIToolbarButton tooltip="AI commands">
              <WandSparklesIcon />
            </AIToolbarButton>
          </ToolbarGroup>
 
          {children}
 
          <ToolbarGroup>
            <InsertToolbarButton />
            <TurnIntoToolbarButton />
            <FontSizeToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
              <BoldIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
              <ItalicIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={UnderlinePlugin}
              tooltip="Underline (⌘+U)"
            >
              <UnderlineIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={StrikethroughPlugin}
              tooltip="Strikethrough (⌘+⇧+M)"
            >
              <StrikethroughIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
              <Code2Icon />
            </MarkToolbarButton>
 
            <FontColorToolbarButton
              colors={DEFAULT_COLORS}
              plugin={FontColorPlugin}
              tooltip="Text color"
            >
              <BaselineIcon />
            </FontColorToolbarButton>
 
            <FontColorToolbarButton
              colors={DEFAULT_COLORS}
              plugin={FontBackgroundColorPlugin}
              tooltip="Background color"
            >
              <PaintBucketIcon />
            </FontColorToolbarButton>
          </ToolbarGroup>
 
          <ToolbarGroup>
            <AlignToolbarButton />
 
            <NumberedListToolbarButton />
            <BulletedListToolbarButton />
            <TodoListToolbarButton />
            <ToggleToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <LinkToolbarButton />
            <TableToolbarButton />
            <EmojiToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MediaToolbarButton plugin={BaseImagePlugin} />
            <MediaToolbarButton plugin={BaseVideoPlugin} />
            <MediaToolbarButton plugin={BaseAudioPlugin} />
            <MediaToolbarButton plugin={BaseFilePlugin} />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <LineHeightToolbarButton />
            <OutdentToolbarButton />
            <IndentToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MoreToolbarButton />
          </ToolbarGroup>
        </>
      )}
 
      <div className="grow" />
 
      <ToolbarGroup>
        <MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
          <HighlighterIcon />
        </MarkToolbarButton>
        <CommentToolbarButton />
      </ToolbarGroup>
 
      <ToolbarGroup>
        <ModeToolbarButton />
      </ToolbarGroup>
    </div>
  );
}
 
export function FixedToolbar(props: React.ComponentProps<typeof Toolbar>) {
  return (
    <Toolbar
      {...props}
      className={cn(
        'scrollbar-hide sticky top-0 left-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
        props.className
      )}
    />
  );
}
 
export const FixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
  render: {
    beforeEditable: () => (
      <FixedToolbar>
        <FixedToolbarButtons />
      </FixedToolbar>
    ),
  },
});
 
export const FixedToolbarKit = [FixedToolbarPlugin] as const;
;
import {
BoldIcon,
Code2Icon,
ItalicIcon,
StrikethroughIcon,
UnderlineIcon,
WandSparklesIcon,
} from 'lucide-react';
import {
useEditorReadOnly,
definePlatePlugin,
useEditor,
useEditorId,
useEditorSelector,
useEventEditorValue,
usePluginStore,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FloatingToolbarButtons() {
const readOnly = useEditorReadOnly();
return (
<>
{!readOnly && (
<>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
Ask AI
</AIToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<TurnIntoToolbarButton />
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<InlineEquationToolbarButton />
<LinkToolbarButton />
</ToolbarGroup>
</>
)}
<ToolbarGroup>
<CommentToolbarButton />
<SuggestionToolbarButton />
{!readOnly && <MoreToolbarButton />}
</ToolbarGroup>
</>
);
}
type FloatingToolbarOptions = {
floatingOptions?: UseVirtualFloatingOptions;
hideToolbar?: boolean;
showWhenReadOnly?: boolean;
};
type FloatingToolbarProps = React.ComponentProps<typeof Toolbar> & {
options?: FloatingToolbarOptions;
};
export function FloatingToolbar(props: FloatingToolbarProps) {
const editorId = useEditorId();
const hasNodeSelection = useEditorSelector(
(editor) => editor.read.selection.nodes().length > 0,
{ id: editorId }
);
if (hasNodeSelection) return null;
return <TextFloatingToolbar {...props} />;
}
function TextFloatingToolbar({
children,
className,
options,
...props
}: FloatingToolbarProps) {
const editorId = useEditorId();
const focusedEditorId = useEventEditorValue('focus');
const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
const editor = useEditor({ id: editorId });
const selectionExpanded = useEditorSelector(
(innerEditor) => innerEditor.read.selection.isExpanded(),
{ id: editorId }
);
const selectionText = useEditorSelector(
(innerEditor2) => innerEditor2.read.text.string(),
{ id: editorId }
);
const selectionRange = useEditorSelector(
(innerEditor3) => innerEditor3.read.selection(),
{ id: editorId }
);
const waitForCollapsedSelection = useEditorSelector(
(innerEditor4, previous = false) => {
if (!innerEditor4.read.selection.isExpanded()) return false;
if (editorId !== focusedEditorId) return true;
return previous;
},
{ id: editorId }
);
const readOnly = useEditorReadOnly();
const [dismissedSelection, setDismissedSelection] =
React.useState<typeof selectionRange>(null);
const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
null
);
const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
const open =
selectionExpanded &&
!!selectionText &&
(editorId === focusedEditorId || ownedOverlayOpen) &&
!isFloatingLinkOpen &&
!isAIChatOpen &&
!options?.hideToolbar &&
(!readOnly || !!options?.showWhenReadOnly) &&
(!waitForCollapsedSelection || readOnly || ownedOverlayOpen) &&
mouseDownOpen !== false &&
dismissedSelection !== selectionRange;
const floating = useVirtualFloating(
mergeProps<UseVirtualFloatingOptions>(
{
open,
getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
onOpenChange: (nextOpen) => {
setDismissedSelection(nextOpen ? null : selectionRange);
},
},
{
middleware: [
offset(12),
flip({
fallbackPlacements: [
'top-start',
'top-end',
'bottom-start',
'bottom-end',
],
padding: 12,
}),
],
placement: 'top',
...options?.floatingOptions,
}
)
);
const openStateRef = React.useRef(open);
React.useEffect(() => {
openStateRef.current = open;
}, [open]);
React.useEffect(() => {
const onMouseUp = () => {
setMouseDownOpen(null);
};
const onMouseDown = () => {
setMouseDownOpen(openStateRef.current);
};
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousedown', onMouseDown);
return () => {
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('mousedown', onMouseDown);
};
}, []);
const editorVersion = useEditorSelector(
(innerEditor5) => innerEditor5.read.lastCommit()?.version ?? 0,
{ id: editorId }
);
const updateFloating = floating.update;
React.useEffect(() => {
if (open) updateFloating?.();
}, [editorVersion, open, updateFloating]);
const clickOutsideRef = useOnClickOutside(
() => {
setDismissedSelection(selectionRange);
},
{ ignoreClass: 'ignore-click-outside/toolbar' }
);
const ref = useComposedRef<HTMLDivElement>(
props.ref,
floating.refs.setFloating
);
if (!open) return null;
return (
<div ref={clickOutsideRef}>
<Toolbar
{...props}
ref={ref}
onOverlayOpenChange={setOwnedOverlayOpen}
style={floating.style}
className={cn(
'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
'max-w-[80vw]',
className
)}
>
{children}
</Toolbar>
</div>
);
}
export const FloatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
render: {
afterEditable: () => (
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
),
},
});
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;
'use client';
 
import { AIChatPlugin } from '@platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  type UseVirtualFloatingOptions,
  flip,
  getSelectionBoundingClientRect,
  offset,
  useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils';
import {
  BoldIcon,
  Code2Icon,
  ItalicIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import {
  useEditorReadOnly,
  definePlatePlugin,
  useEditor,
  useEditorId,
  useEditorSelector,
  useEventEditorValue,
  usePluginStore,
} from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
 
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
 
export function FloatingToolbarButtons() {
  const readOnly = useEditorReadOnly();
 
  return (
    <>
      {!readOnly && (
        <>
          <ToolbarGroup>
            <AIToolbarButton tooltip="AI commands">
              <WandSparklesIcon />
              Ask AI
            </AIToolbarButton>
          </ToolbarGroup>
 
          <ToolbarGroup>
            <TurnIntoToolbarButton />
 
            <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
              <BoldIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
              <ItalicIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={UnderlinePlugin}
              tooltip="Underline (⌘+U)"
            >
              <UnderlineIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={StrikethroughPlugin}
              tooltip="Strikethrough (⌘+⇧+M)"
            >
              <StrikethroughIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
              <Code2Icon />
            </MarkToolbarButton>
 
            <InlineEquationToolbarButton />
 
            <LinkToolbarButton />
          </ToolbarGroup>
        </>
      )}
 
      <ToolbarGroup>
        <CommentToolbarButton />
        <SuggestionToolbarButton />
 
        {!readOnly && <MoreToolbarButton />}
      </ToolbarGroup>
    </>
  );
}
 
type FloatingToolbarOptions = {
  floatingOptions?: UseVirtualFloatingOptions;
  hideToolbar?: boolean;
  showWhenReadOnly?: boolean;
};
 
type FloatingToolbarProps = React.ComponentProps<typeof Toolbar> & {
  options?: FloatingToolbarOptions;
};
 
export function FloatingToolbar(props: FloatingToolbarProps) {
  const editorId = useEditorId();
  const hasNodeSelection = useEditorSelector(
    (editor) => editor.read.selection.nodes().length > 0,
    { id: editorId }
  );
 
  if (hasNodeSelection) return null;
 
  return <TextFloatingToolbar {...props} />;
}
 
function TextFloatingToolbar({
  children,
  className,
  options,
  ...props
}: FloatingToolbarProps) {
  const editorId = useEditorId();
  const focusedEditorId = useEventEditorValue('focus');
  const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
  const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
  const editor = useEditor({ id: editorId });
  const selectionExpanded = useEditorSelector(
    (innerEditor) => innerEditor.read.selection.isExpanded(),
    { id: editorId }
  );
  const selectionText = useEditorSelector(
    (innerEditor2) => innerEditor2.read.text.string(),
    { id: editorId }
  );
  const selectionRange = useEditorSelector(
    (innerEditor3) => innerEditor3.read.selection(),
    { id: editorId }
  );
  const waitForCollapsedSelection = useEditorSelector(
    (innerEditor4, previous = false) => {
      if (!innerEditor4.read.selection.isExpanded()) return false;
      if (editorId !== focusedEditorId) return true;
 
      return previous;
    },
    { id: editorId }
  );
  const readOnly = useEditorReadOnly();
  const [dismissedSelection, setDismissedSelection] =
    React.useState<typeof selectionRange>(null);
  const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
    null
  );
  const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
  const open =
    selectionExpanded &&
    !!selectionText &&
    (editorId === focusedEditorId || ownedOverlayOpen) &&
    !isFloatingLinkOpen &&
    !isAIChatOpen &&
    !options?.hideToolbar &&
    (!readOnly || !!options?.showWhenReadOnly) &&
    (!waitForCollapsedSelection || readOnly || ownedOverlayOpen) &&
    mouseDownOpen !== false &&
    dismissedSelection !== selectionRange;
  const floating = useVirtualFloating(
    mergeProps<UseVirtualFloatingOptions>(
      {
        open,
        getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
        onOpenChange: (nextOpen) => {
          setDismissedSelection(nextOpen ? null : selectionRange);
        },
      },
      {
        middleware: [
          offset(12),
          flip({
            fallbackPlacements: [
              'top-start',
              'top-end',
              'bottom-start',
              'bottom-end',
            ],
            padding: 12,
          }),
        ],
        placement: 'top',
        ...options?.floatingOptions,
      }
    )
  );
  const openStateRef = React.useRef(open);
 
  React.useEffect(() => {
    openStateRef.current = open;
  }, [open]);
 
  React.useEffect(() => {
    const onMouseUp = () => {
      setMouseDownOpen(null);
    };
    const onMouseDown = () => {
      setMouseDownOpen(openStateRef.current);
    };
 
    document.addEventListener('mouseup', onMouseUp);
    document.addEventListener('mousedown', onMouseDown);
 
    return () => {
      document.removeEventListener('mouseup', onMouseUp);
      document.removeEventListener('mousedown', onMouseDown);
    };
  }, []);
 
  const editorVersion = useEditorSelector(
    (innerEditor5) => innerEditor5.read.lastCommit()?.version ?? 0,
    { id: editorId }
  );
  const updateFloating = floating.update;
 
  React.useEffect(() => {
    if (open) updateFloating?.();
  }, [editorVersion, open, updateFloating]);
 
  const clickOutsideRef = useOnClickOutside(
    () => {
      setDismissedSelection(selectionRange);
    },
    { ignoreClass: 'ignore-click-outside/toolbar' }
  );
 
  const ref = useComposedRef<HTMLDivElement>(
    props.ref,
    floating.refs.setFloating
  );
 
  if (!open) return null;
 
  return (
    <div ref={clickOutsideRef}>
      <Toolbar
        {...props}
        ref={ref}
        onOverlayOpenChange={setOwnedOverlayOpen}
        style={floating.style}
        className={cn(
          'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
          'max-w-[80vw]',
          className
        )}
      >
        {children}
      </Toolbar>
    </div>
  );
}
 
export const FloatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
  render: {
    afterEditable: () => (
      <FloatingToolbar>
        <FloatingToolbarButtons />
      </FloatingToolbar>
    ),
  },
});
 
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;
>
<CustomIcon />
</ToolbarButton>
);
}
import { useEditor } from 'platejs/react';
import { CustomIcon } from 'lucide-react';
import { ToolbarButton } from '@/components/editor/toolbar';
 
export function CustomToolbarButton() {
  const editor = useEditor();
 
  return (
    <ToolbarButton
      onClick={() => {
        // Custom action
        editor.update((tx) => {
          tx.text.insert('Custom text');
        });
      }}
      tooltip="Custom Action"
    >
      <CustomIcon />
    </ToolbarButton>
  );
}
'lucide-react'
;
import { MarkToolbarButton } from '@/components/editor/mark-toolbar-button';
export function BoldToolbarButton() {
return (
<MarkToolbarButton nodeType="bold" tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
);
}
'react'
;
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import {
ToolbarButton,
ToolbarMenuGroup,
} from '@/components/editor/toolbar';
import {
getBlockType,
applyBlockAction,
} from '@/components/editor/transforms';
const ACTION_THREE_COLUMNS = 'action_three_columns';
function getTurnIntoLabel(value: string) {
switch (value) {
case 'disc': {
return 'Bulleted list';
}
case 'decimal': {
return 'Numbered list';
}
case 'todo': {
return 'To-do list';
}
case 'heading-1': {
return 'Heading 1';
}
case 'heading-2': {
return 'Heading 2';
}
case 'heading-3': {
return 'Heading 3';
}
case 'heading-4': {
return 'Heading 4';
}
case 'heading-5': {
return 'Heading 5';
}
case 'heading-6': {
return 'Heading 6';
}
case PLUGINS.toggle: {
return 'Toggle list';
}
case PLUGINS.codeBlock: {
return 'Code';
}
case PLUGINS.codeDrawing: {
return 'Code Drawing';
}
case PLUGINS.blockquote: {
return 'Quote';
}
case ACTION_THREE_COLUMNS: {
return '3 columns';
}
default: {
return 'Text';
}
}
}
function TurnIntoMenuItem({
children,
icon,
value,
}: React.PropsWithChildren<{
icon: React.ReactNode;
value: string;
}>) {
return (
<DropdownMenuRadioItem
className="min-w-[180px] pl-2 *:first:[span]:hidden"
value={value}
>
{icon}
{children}
</DropdownMenuRadioItem>
);
}
export function TurnIntoToolbarButton() {
const editor = useEditor();
const [open, setOpen] = React.useState(false);
const documentValue = useSelectionFragmentProp({
defaultValue: editor.plugin(PLUGINS.paragraph).schema.type,
getProp: (node) =>
ElementApi.isElement(node) ? getBlockType(node) : undefined,
});
const selectedDocumentValue =
typeof documentValue === 'string' ? documentValue : PLUGINS.paragraph;
const selectedValue =
[
PLUGINS.paragraph,
'heading-1',
'heading-2',
'heading-3',
'heading-4',
'heading-5',
'heading-6',
PLUGINS.toggle,
PLUGINS.codeBlock,
PLUGINS.codeDrawing,
PLUGINS.blockquote,
].find((name) => name === selectedDocumentValue) ?? selectedDocumentValue;
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger>
<ToolbarButton
className="min-w-[125px]"
pressed={open}
tooltip="Turn into"
isDropdown
>
{getTurnIntoLabel(selectedValue)}
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="ignore-click-outside/toolbar min-w-0"
onFinalFocus={(event) => {
event.preventDefault();
editor.api.dom.focus();
}}
align="start"
>
<DropdownMenuRadioGroup
value={selectedValue}
onValueChange={(action) => {
applyBlockAction(editor, action);
}}
>
<ToolbarMenuGroup label="Turn into">
<TurnIntoMenuItem icon={<PilcrowIcon />} value={PLUGINS.paragraph}>
Text
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading1Icon />} value="heading-1">
Heading 1
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading2Icon />} value="heading-2">
Heading 2
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading3Icon />} value="heading-3">
Heading 3
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading4Icon />} value="heading-4">
Heading 4
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading5Icon />} value="heading-5">
Heading 5
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading6Icon />} value="heading-6">
Heading 6
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<ListIcon />} value="disc">
Bulleted list
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<ListOrderedIcon />} value="decimal">
Numbered list
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<SquareIcon />} value="todo">
To-do list
</TurnIntoMenuItem>
<TurnIntoMenuItem
icon={<ChevronRightIcon />}
value={PLUGINS.toggle}
>
Toggle list
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<FileCodeIcon />} value={PLUGINS.codeBlock}>
Code
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Code2Icon />} value={PLUGINS.codeDrawing}>
Code Drawing
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<QuoteIcon />} value={PLUGINS.blockquote}>
Quote
</TurnIntoMenuItem>
<TurnIntoMenuItem
icon={<Columns3Icon />}
value={ACTION_THREE_COLUMNS}
>
3 columns
</TurnIntoMenuItem>
</ToolbarMenuGroup>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
'use client';
import { ElementApi } from '@platejs/plite';
import { PLUGINS } from '@platejs/utils';
import {
  ChevronRightIcon,
  Code2Icon,
  Columns3Icon,
  FileCodeIcon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  Heading4Icon,
  Heading5Icon,
  Heading6Icon,
  ListIcon,
  ListOrderedIcon,
  PilcrowIcon,
  QuoteIcon,
  SquareIcon,
} from 'lucide-react';
import { useEditor, useSelectionFragmentProp } from 'platejs/react';
import * as React from 'react';
 
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import {
  ToolbarButton,
  ToolbarMenuGroup,
} from '@/components/editor/toolbar';
import {
  getBlockType,
  applyBlockAction,
} from '@/components/editor/transforms';
 
const ACTION_THREE_COLUMNS = 'action_three_columns';
 
function getTurnIntoLabel(value: string) {
  switch (value) {
    case 'disc': {
      return 'Bulleted list';
    }
    case 'decimal': {
      return 'Numbered list';
    }
    case 'todo': {
      return 'To-do list';
    }
    case 'heading-1': {
      return 'Heading 1';
    }
    case 'heading-2': {
      return 'Heading 2';
    }
    case 'heading-3': {
      return 'Heading 3';
    }
    case 'heading-4': {
      return 'Heading 4';
    }
    case 'heading-5': {
      return 'Heading 5';
    }
    case 'heading-6': {
      return 'Heading 6';
    }
    case PLUGINS.toggle: {
      return 'Toggle list';
    }
    case PLUGINS.codeBlock: {
      return 'Code';
    }
    case PLUGINS.codeDrawing: {
      return 'Code Drawing';
    }
    case PLUGINS.blockquote: {
      return 'Quote';
    }
    case ACTION_THREE_COLUMNS: {
      return '3 columns';
    }
    default: {
      return 'Text';
    }
  }
}
 
function TurnIntoMenuItem({
  children,
  icon,
  value,
}: React.PropsWithChildren<{
  icon: React.ReactNode;
  value: string;
}>) {
  return (
    <DropdownMenuRadioItem
      className="min-w-[180px] pl-2 *:first:[span]:hidden"
      value={value}
    >
      {icon}
      {children}
    </DropdownMenuRadioItem>
  );
}
 
export function TurnIntoToolbarButton() {
  const editor = useEditor();
  const [open, setOpen] = React.useState(false);
  const documentValue = useSelectionFragmentProp({
    defaultValue: editor.plugin(PLUGINS.paragraph).schema.type,
    getProp: (node) =>
      ElementApi.isElement(node) ? getBlockType(node) : undefined,
  });
  const selectedDocumentValue =
    typeof documentValue === 'string' ? documentValue : PLUGINS.paragraph;
  const selectedValue =
    [
      PLUGINS.paragraph,
      'heading-1',
      'heading-2',
      'heading-3',
      'heading-4',
      'heading-5',
      'heading-6',
      PLUGINS.toggle,
      PLUGINS.codeBlock,
      PLUGINS.codeDrawing,
      PLUGINS.blockquote,
    ].find((name) => name === selectedDocumentValue) ?? selectedDocumentValue;
 
  return (
    <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
      <DropdownMenuTrigger>
        <ToolbarButton
          className="min-w-[125px]"
          pressed={open}
          tooltip="Turn into"
          isDropdown
        >
          {getTurnIntoLabel(selectedValue)}
        </ToolbarButton>
      </DropdownMenuTrigger>
 
      <DropdownMenuContent
        className="ignore-click-outside/toolbar min-w-0"
        onFinalFocus={(event) => {
          event.preventDefault();
          editor.api.dom.focus();
        }}
        align="start"
      >
        <DropdownMenuRadioGroup
          value={selectedValue}
          onValueChange={(action) => {
            applyBlockAction(editor, action);
          }}
        >
          <ToolbarMenuGroup label="Turn into">
            <TurnIntoMenuItem icon={<PilcrowIcon />} value={PLUGINS.paragraph}>
              Text
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<Heading1Icon />} value="heading-1">
              Heading 1
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<Heading2Icon />} value="heading-2">
              Heading 2
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<Heading3Icon />} value="heading-3">
              Heading 3
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<Heading4Icon />} value="heading-4">
              Heading 4
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<Heading5Icon />} value="heading-5">
              Heading 5
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<Heading6Icon />} value="heading-6">
              Heading 6
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<ListIcon />} value="disc">
              Bulleted list
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<ListOrderedIcon />} value="decimal">
              Numbered list
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<SquareIcon />} value="todo">
              To-do list
            </TurnIntoMenuItem>
            <TurnIntoMenuItem
              icon={<ChevronRightIcon />}
              value={PLUGINS.toggle}
            >
              Toggle list
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<FileCodeIcon />} value={PLUGINS.codeBlock}>
              Code
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<Code2Icon />} value={PLUGINS.codeDrawing}>
              Code Drawing
            </TurnIntoMenuItem>
            <TurnIntoMenuItem icon={<QuoteIcon />} value={PLUGINS.blockquote}>
              Quote
            </TurnIntoMenuItem>
            <TurnIntoMenuItem
              icon={<Columns3Icon />}
              value={ACTION_THREE_COLUMNS}
            >
              3 columns
            </TurnIntoMenuItem>
          </ToolbarMenuGroup>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
label:
'Custom Block'
,
value: 'custom-block',
},
];
import { type PlateEditor, useEditor } from 'platejs/react';
import * as React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
ToolbarButton,
ToolbarMenuGroup,
} from '@/components/editor/toolbar';
import {
insertBlock,
insertInlineElement,
} from '@/components/editor/transforms';
type Group = {
group: string;
items: Item[];
};
type Item = {
icon: React.ReactNode;
value: string;
onSelect: (editor: PlateEditor, value: string) => void;
focusEditor?: boolean;
label?: string;
};
const groups: Group[] = [
{
group: 'Basic blocks',
items: [
{
icon: <PilcrowIcon />,
label: 'Paragraph',
value: PLUGINS.paragraph,
},
{
icon: <Heading1Icon />,
label: 'Heading 1',
value: 'heading-1',
},
{
icon: <Heading2Icon />,
label: 'Heading 2',
value: 'heading-2',
},
{
icon: <Heading3Icon />,
label: 'Heading 3',
value: 'heading-3',
},
{
icon: <TableIcon />,
label: 'Table',
value: PLUGINS.table,
},
{
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
},
{
icon: <QuoteIcon />,
label: 'Quote',
value: PLUGINS.blockquote,
},
{
icon: <MinusIcon />,
label: 'Divider',
value: PLUGINS.horizontalRule,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Lists',
items: [
{
icon: <ListIcon />,
label: 'Bulleted list',
value: 'disc',
},
{
icon: <ListOrderedIcon />,
label: 'Numbered list',
value: 'decimal',
},
{
icon: <SquareIcon />,
label: 'To-do list',
value: 'todo',
},
{
icon: <ChevronRightIcon />,
label: 'Toggle list',
value: PLUGINS.toggle,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Media',
items: [
{
icon: <ImageIcon />,
label: 'Image',
value: PLUGINS.image,
},
{
icon: <FilmIcon />,
label: 'Embed',
value: PLUGINS.mediaEmbed,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Advanced blocks',
items: [
{
icon: <TableOfContentsIcon />,
label: 'Table of contents',
value: PLUGINS.toc,
},
{
icon: <Columns3Icon />,
label: '3 columns',
value: 'action_three_columns',
},
{
focusEditor: false,
icon: <RadicalIcon />,
label: 'Equation',
value: PLUGINS.equation,
},
{
icon: <PenToolIcon />,
label: 'Excalidraw',
value: PLUGINS.excalidraw,
},
{
icon: <Code2 />,
label: 'Code Drawing',
value: PLUGINS.codeDrawing,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Inline',
items: [
{
icon: <Link2Icon />,
label: 'Link',
value: PLUGINS.link,
},
{
focusEditor: true,
icon: <CalendarIcon />,
label: 'Date',
value: PLUGINS.date,
},
{
focusEditor: true,
icon: <SuperscriptIcon />,
label: 'Footnote',
value: 'action_footnote',
},
{
focusEditor: false,
icon: <RadicalIcon />,
label: 'Inline Equation',
value: PLUGINS.inlineEquation,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertInlineElement(editor, value);
},
})),
},
];
export function InsertToolbarButton() {
const editor = useEditor();
const [open, setOpen] = React.useState(false);
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger asChild>
<ToolbarButton pressed={open} tooltip="Insert" isDropdown>
<PlusIcon />
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="flex max-h-[500px] min-w-0 flex-col overflow-y-auto"
align="start"
>
{groups.map(({ group, items: nestedItems }) => (
<ToolbarMenuGroup key={group} label={group}>
{nestedItems.map(({ icon, label, value, onSelect }) => (
<DropdownMenuItem
key={value}
className="min-w-[180px]"
onSelect={() => {
onSelect(editor, value);
editor.api.dom.focus();
}}
>
{icon}
{label}
</DropdownMenuItem>
))}
</ToolbarMenuGroup>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
'use client';
 
import {
  CalendarIcon,
  ChevronRightIcon,
  Code2,
  Columns3Icon,
  FileCodeIcon,
  FilmIcon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  ImageIcon,
  Link2Icon,
  ListIcon,
  ListOrderedIcon,
  MinusIcon,
  PenToolIcon,
  PilcrowIcon,
  PlusIcon,
  QuoteIcon,
  RadicalIcon,
  SquareIcon,
  SuperscriptIcon,
  TableIcon,
  TableOfContentsIcon,
} from 'lucide-react';
import { PLUGINS } from 'platejs';
import { type PlateEditor, useEditor } from 'platejs/react';
import * as React from 'react';
 
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
  ToolbarButton,
  ToolbarMenuGroup,
} from '@/components/editor/toolbar';
import {
  insertBlock,
  insertInlineElement,
} from '@/components/editor/transforms';
 
type Group = {
  group: string;
  items: Item[];
};
 
type Item = {
  icon: React.ReactNode;
  value: string;
  onSelect: (editor: PlateEditor, value: string) => void;
  focusEditor?: boolean;
  label?: string;
};
 
const groups: Group[] = [
  {
    group: 'Basic blocks',
    items: [
      {
        icon: <PilcrowIcon />,
        label: 'Paragraph',
        value: PLUGINS.paragraph,
      },
      {
        icon: <Heading1Icon />,
        label: 'Heading 1',
        value: 'heading-1',
      },
      {
        icon: <Heading2Icon />,
        label: 'Heading 2',
        value: 'heading-2',
      },
      {
        icon: <Heading3Icon />,
        label: 'Heading 3',
        value: 'heading-3',
      },
      {
        icon: <TableIcon />,
        label: 'Table',
        value: PLUGINS.table,
      },
      {
        icon: <FileCodeIcon />,
        label: 'Code',
        value: PLUGINS.codeBlock,
      },
      {
        icon: <QuoteIcon />,
        label: 'Quote',
        value: PLUGINS.blockquote,
      },
      {
        icon: <MinusIcon />,
        label: 'Divider',
        value: PLUGINS.horizontalRule,
      },
    ].map((item) => ({
      ...item,
      onSelect: (editor, value) => {
        insertBlock(editor, value);
      },
    })),
  },
  {
    group: 'Lists',
    items: [
      {
        icon: <ListIcon />,
        label: 'Bulleted list',
        value: 'disc',
      },
      {
        icon: <ListOrderedIcon />,
        label: 'Numbered list',
        value: 'decimal',
      },
      {
        icon: <SquareIcon />,
        label: 'To-do list',
        value: 'todo',
      },
      {
        icon: <ChevronRightIcon />,
        label: 'Toggle list',
        value: PLUGINS.toggle,
      },
    ].map((item) => ({
      ...item,
      onSelect: (editor, value) => {
        insertBlock(editor, value);
      },
    })),
  },
  {
    group: 'Media',
    items: [
      {
        icon: <ImageIcon />,
        label: 'Image',
        value: PLUGINS.image,
      },
      {
        icon: <FilmIcon />,
        label: 'Embed',
        value: PLUGINS.mediaEmbed,
      },
    ].map((item) => ({
      ...item,
      onSelect: (editor, value) => {
        insertBlock(editor, value);
      },
    })),
  },
  {
    group: 'Advanced blocks',
    items: [
      {
        icon: <TableOfContentsIcon />,
        label: 'Table of contents',
        value: PLUGINS.toc,
      },
      {
        icon: <Columns3Icon />,
        label: '3 columns',
        value: 'action_three_columns',
      },
      {
        focusEditor: false,
        icon: <RadicalIcon />,
        label: 'Equation',
        value: PLUGINS.equation,
      },
      {
        icon: <PenToolIcon />,
        label: 'Excalidraw',
        value: PLUGINS.excalidraw,
      },
      {
        icon: <Code2 />,
        label: 'Code Drawing',
        value: PLUGINS.codeDrawing,
      },
    ].map((item) => ({
      ...item,
      onSelect: (editor, value) => {
        insertBlock(editor, value);
      },
    })),
  },
  {
    group: 'Inline',
    items: [
      {
        icon: <Link2Icon />,
        label: 'Link',
        value: PLUGINS.link,
      },
      {
        focusEditor: true,
        icon: <CalendarIcon />,
        label: 'Date',
        value: PLUGINS.date,
      },
      {
        focusEditor: true,
        icon: <SuperscriptIcon />,
        label: 'Footnote',
        value: 'action_footnote',
      },
      {
        focusEditor: false,
        icon: <RadicalIcon />,
        label: 'Inline Equation',
        value: PLUGINS.inlineEquation,
      },
    ].map((item) => ({
      ...item,
      onSelect: (editor, value) => {
        insertInlineElement(editor, value);
      },
    })),
  },
];
 
export function InsertToolbarButton() {
  const editor = useEditor();
  const [open, setOpen] = React.useState(false);
 
  return (
    <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
      <DropdownMenuTrigger asChild>
        <ToolbarButton pressed={open} tooltip="Insert" isDropdown>
          <PlusIcon />
        </ToolbarButton>
      </DropdownMenuTrigger>
 
      <DropdownMenuContent
        className="flex max-h-[500px] min-w-0 flex-col overflow-y-auto"
        align="start"
      >
        {groups.map(({ group, items: nestedItems }) => (
          <ToolbarMenuGroup key={group} label={group}>
            {nestedItems.map(({ icon, label, value, onSelect }) => (
              <DropdownMenuItem
                key={value}
                className="min-w-[180px]"
                onSelect={() => {
                  onSelect(editor, value);
                  editor.api.dom.focus();
                }}
              >
                {icon}
                {label}
              </DropdownMenuItem>
            ))}
          </ToolbarMenuGroup>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
{
  group: 'Basic blocks',
  items: [
    // ... existing items
    {
      icon: <CustomIcon />,
      label: 'Custom Block',
      value: 'custom-block',
    },
  ].map((item) => ({
    ...item,
    onSelect: (editor, value) => {
      insertBlock(editor, value);
    },
  })),
}