feat: introduce fixed toolbar and insert button for enhanced editor functionality

This commit is contained in:
Anish Sarkar 2026-02-16 15:49:07 +05:30
parent 73147c69a3
commit 1450e22f54
8 changed files with 359 additions and 87 deletions

View file

@ -0,0 +1,105 @@
'use client';
import * as React from 'react';
import {
BoldIcon,
Code2Icon,
HighlighterIcon,
ItalicIcon,
RedoIcon,
StrikethroughIcon,
UnderlineIcon,
UndoIcon,
} from 'lucide-react';
import { KEYS } from 'platejs';
import { useEditorReadOnly, useEditorRef } from 'platejs/react';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { ToolbarButton, ToolbarGroup } from './toolbar';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FixedToolbarButtons() {
const readOnly = useEditorReadOnly();
const editor = useEditorRef();
if (readOnly) return null;
return (
<div className="flex w-full flex-wrap">
<ToolbarGroup>
<ToolbarButton
tooltip="Undo (⌘+Z)"
onClick={() => {
editor.undo();
editor.tf.focus();
}}
>
<UndoIcon />
</ToolbarButton>
<ToolbarButton
tooltip="Redo (⌘+⇧+Z)"
onClick={() => {
editor.redo();
editor.tf.focus();
}}
>
<RedoIcon />
</ToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<InsertToolbarButton />
<TurnIntoToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MarkToolbarButton nodeType={KEYS.bold} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.italic} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
nodeType={KEYS.underline}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
nodeType={KEYS.strikethrough}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.code} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<MarkToolbarButton
nodeType={KEYS.highlight}
tooltip="Highlight (⌘+⇧+H)"
>
<HighlighterIcon />
</MarkToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<LinkToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MoreToolbarButton />
</ToolbarGroup>
</div>
);
}

View file

@ -0,0 +1,26 @@
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Toolbar } from './toolbar';
export function FixedToolbar({
children,
className,
...props
}: React.ComponentProps<typeof Toolbar>) {
return (
<Toolbar
className={cn(
'scrollbar-hide sticky top-0 left-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b bg-background/95 p-1 backdrop-blur supports-backdrop-filter:bg-background/60',
className
)}
{...props}
>
{children}
</Toolbar>
);
}

View file

@ -0,0 +1,205 @@
'use client';
import * as React from 'react';
import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
import {
ChevronRightIcon,
FileCodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
InfoIcon,
ListIcon,
ListOrderedIcon,
MinusIcon,
PilcrowIcon,
PlusIcon,
QuoteIcon,
RadicalIcon,
SquareIcon,
TableIcon,
} from 'lucide-react';
import { KEYS } from 'platejs';
import { type PlateEditor, useEditorRef } from 'platejs/react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
insertBlock,
insertInlineElement,
} from '@/components/editor/transforms';
import { ToolbarButton, ToolbarMenuGroup } from './toolbar';
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: KEYS.p,
},
{
icon: <Heading1Icon />,
label: 'Heading 1',
value: 'h1',
},
{
icon: <Heading2Icon />,
label: 'Heading 2',
value: 'h2',
},
{
icon: <Heading3Icon />,
label: 'Heading 3',
value: 'h3',
},
{
icon: <TableIcon />,
label: 'Table',
value: KEYS.table,
},
{
icon: <FileCodeIcon />,
label: 'Code block',
value: KEYS.codeBlock,
},
{
icon: <QuoteIcon />,
label: 'Quote',
value: KEYS.blockquote,
},
{
icon: <MinusIcon />,
label: 'Divider',
value: KEYS.hr,
},
].map((item) => ({
...item,
onSelect: (editor: PlateEditor, value: string) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Lists',
items: [
{
icon: <ListIcon />,
label: 'Bulleted list',
value: KEYS.ul,
},
{
icon: <ListOrderedIcon />,
label: 'Numbered list',
value: KEYS.ol,
},
{
icon: <SquareIcon />,
label: 'To-do list',
value: KEYS.listTodo,
},
{
icon: <ChevronRightIcon />,
label: 'Toggle list',
value: KEYS.toggle,
},
].map((item) => ({
...item,
onSelect: (editor: PlateEditor, value: string) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Advanced',
items: [
{
icon: <InfoIcon />,
label: 'Callout',
value: KEYS.callout,
},
{
focusEditor: false,
icon: <RadicalIcon />,
label: 'Equation',
value: KEYS.equation,
},
].map((item) => ({
...item,
onSelect: (editor: PlateEditor, value: string) => {
if (item.value === KEYS.equation) {
insertInlineElement(editor, value);
} else {
insertBlock(editor, value);
}
},
})),
},
];
export function InsertToolbarButton(props: DropdownMenuProps) {
const editor = useEditorRef();
const [open, setOpen] = React.useState(false);
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false} {...props}>
<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 }) => (
<React.Fragment key={group}>
<ToolbarMenuGroup>
{items.map(({ icon, label, value, onSelect, focusEditor }) => (
<DropdownMenuItem
key={value}
onSelect={() => {
onSelect(editor, value);
if (focusEditor !== false) {
editor.tf.focus();
}
setOpen(false);
}}
className="group"
>
<div className="flex items-center text-sm text-muted-foreground focus:text-accent-foreground group-aria-selected:text-accent-foreground">
{icon}
<span className="ml-2">{label || value}</span>
</div>
</DropdownMenuItem>
))}
</ToolbarMenuGroup>
</React.Fragment>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

View file

@ -9,7 +9,6 @@ import {
BlockSelectionPlugin,
useBlockSelected,
} from '@platejs/selection/react';
import { setCellBackground } from '@platejs/table';
import {
TablePlugin,
TableProvider,
@ -27,10 +26,8 @@ import {
ArrowRight,
ArrowUp,
CombineIcon,
EraserIcon,
Grid2X2Icon,
GripVertical,
PaintBucketIcon,
SquareSplitHorizontalIcon,
Trash2Icon,
XIcon,
@ -66,7 +63,6 @@ import {
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
@ -74,10 +70,6 @@ import { Popover, PopoverContent } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { blockSelectionVariants } from './block-selection';
import {
ColorDropdownMenuItems,
DEFAULT_COLORS,
} from './font-color-toolbar-button';
import { ResizeHandle } from './resize-handle';
import {
BorderAllIcon,
@ -91,8 +83,8 @@ import {
Toolbar,
ToolbarButton,
ToolbarGroup,
ToolbarMenuGroup,
} from './toolbar';
export const TableElement = withHOC(
TableProvider,
function TableElement({
@ -181,9 +173,6 @@ function TableFloatingToolbar({
contentEditable={false}
>
<ToolbarGroup>
<ColorDropdownMenu tooltip="Background color">
<PaintBucketIcon />
</ColorDropdownMenu>
{canMerge && (
<ToolbarButton
onClick={() => tf.table.merge()}
@ -370,59 +359,6 @@ function TableBordersDropdownMenuContent(
);
}
function ColorDropdownMenu({
children,
tooltip,
}: {
children: React.ReactNode;
tooltip: string;
}) {
const [open, setOpen] = React.useState(false);
const editor = useEditorRef();
const selectedCells = usePluginOption(TablePlugin, 'selectedCells');
const onUpdateColor = React.useCallback(
(color: string) => {
setOpen(false);
setCellBackground(editor, { color, selectedCells: selectedCells ?? [] });
},
[selectedCells, editor]
);
const onClearColor = React.useCallback(() => {
setOpen(false);
setCellBackground(editor, {
color: null,
selectedCells: selectedCells ?? [],
});
}, [selectedCells, editor]);
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger asChild>
<ToolbarButton tooltip={tooltip}>{children}</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<ToolbarMenuGroup label="Colors">
<ColorDropdownMenuItems
className="px-2"
colors={DEFAULT_COLORS}
updateColor={onUpdateColor}
/>
</ToolbarMenuGroup>
<DropdownMenuGroup>
<DropdownMenuItem className="p-2" onClick={onClearColor}>
<EraserIcon />
<span>Clear</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
export function TableRowElement({
children,
...props