feat: add new autoformat and drag-and-drop functionality to the editor, refactor list handling and update dependencies

This commit is contained in:
Anish Sarkar 2026-02-16 16:05:16 +05:30
parent 1450e22f54
commit 93a0487e56
14 changed files with 972 additions and 321 deletions

View file

@ -6,15 +6,15 @@ import { Plate, usePlateEditor } from 'platejs/react';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math'; import remarkMath from 'remark-math';
import { AutoformatKit } from '@/components/editor/plugins/autoformat-classic-kit'; import { AutoformatKit } from '@/components/editor/plugins/autoformat-kit';
import { BasicNodesKit } from '@/components/editor/plugins/basic-nodes-kit'; import { BasicNodesKit } from '@/components/editor/plugins/basic-nodes-kit';
import { CalloutKit } from '@/components/editor/plugins/callout-kit'; import { CalloutKit } from '@/components/editor/plugins/callout-kit';
import { CodeBlockKit } from '@/components/editor/plugins/code-block-kit'; import { CodeBlockKit } from '@/components/editor/plugins/code-block-kit';
import { DndKit } from '@/components/editor/plugins/dnd-kit';
import { FixedToolbarKit } from '@/components/editor/plugins/fixed-toolbar-kit'; import { FixedToolbarKit } from '@/components/editor/plugins/fixed-toolbar-kit';
import { FloatingToolbarKit } from '@/components/editor/plugins/floating-toolbar-kit'; import { FloatingToolbarKit } from '@/components/editor/plugins/floating-toolbar-kit';
import { IndentKit } from '@/components/editor/plugins/indent-kit';
import { LinkKit } from '@/components/editor/plugins/link-kit'; import { LinkKit } from '@/components/editor/plugins/link-kit';
import { ListKit } from '@/components/editor/plugins/list-classic-kit'; import { ListKit } from '@/components/editor/plugins/list-kit';
import { MathKit } from '@/components/editor/plugins/math-kit'; import { MathKit } from '@/components/editor/plugins/math-kit';
import { SelectionKit } from '@/components/editor/plugins/selection-kit'; import { SelectionKit } from '@/components/editor/plugins/selection-kit';
import { SlashCommandKit } from '@/components/editor/plugins/slash-command-kit'; import { SlashCommandKit } from '@/components/editor/plugins/slash-command-kit';
@ -59,13 +59,13 @@ export function PlateEditor({
...LinkKit, ...LinkKit,
...CalloutKit, ...CalloutKit,
...ToggleKit, ...ToggleKit,
...IndentKit,
...MathKit, ...MathKit,
...SelectionKit, ...SelectionKit,
...SlashCommandKit, ...SlashCommandKit,
...FixedToolbarKit, ...FixedToolbarKit,
...FloatingToolbarKit, ...FloatingToolbarKit,
...AutoformatKit, ...AutoformatKit,
...DndKit,
MarkdownPlugin.configure({ MarkdownPlugin.configure({
options: { options: {
remarkPlugins: [remarkGfm, remarkMath], remarkPlugins: [remarkGfm, remarkMath],

View file

@ -1,7 +1,6 @@
'use client'; 'use client';
import type { AutoformatBlockRule, AutoformatRule } from '@platejs/autoformat'; import type { AutoformatRule } from '@platejs/autoformat';
import type { SlateEditor } from 'platejs';
import { import {
autoformatArrow, autoformatArrow,
@ -13,38 +12,9 @@ import {
autoformatSmartQuotes, autoformatSmartQuotes,
} from '@platejs/autoformat'; } from '@platejs/autoformat';
import { insertEmptyCodeBlock } from '@platejs/code-block'; import { insertEmptyCodeBlock } from '@platejs/code-block';
import { toggleList, toggleTaskList, unwrapList } from '@platejs/list-classic'; import { toggleList } from '@platejs/list';
import { openNextToggles } from '@platejs/toggle/react'; import { openNextToggles } from '@platejs/toggle/react';
import { ElementApi, isType, KEYS } from 'platejs'; import { KEYS } from 'platejs';
const preFormat: AutoformatBlockRule['preFormat'] = (editor) =>
unwrapList(editor);
const format = (editor: SlateEditor, customFormatting: any) => {
if (editor.selection) {
const parentEntry = editor.api.parent(editor.selection);
if (!parentEntry) return;
const [node] = parentEntry;
if (ElementApi.isElement(node) && !isType(editor, node, KEYS.codeBlock)) {
customFormatting();
}
}
};
const formatTaskList = (editor: SlateEditor, defaultChecked = false) => {
format(editor, () => toggleTaskList(editor, defaultChecked));
};
const formatList = (editor: SlateEditor, elementType: string) => {
format(editor, () =>
toggleList(editor, {
type: elementType,
})
);
};
const autoformatMarks: AutoformatRule[] = [ const autoformatMarks: AutoformatRule[] = [
{ {
@ -123,49 +93,41 @@ const autoformatBlocks: AutoformatRule[] = [
{ {
match: '# ', match: '# ',
mode: 'block', mode: 'block',
preFormat,
type: KEYS.h1, type: KEYS.h1,
}, },
{ {
match: '## ', match: '## ',
mode: 'block', mode: 'block',
preFormat,
type: KEYS.h2, type: KEYS.h2,
}, },
{ {
match: '### ', match: '### ',
mode: 'block', mode: 'block',
preFormat,
type: KEYS.h3, type: KEYS.h3,
}, },
{ {
match: '#### ', match: '#### ',
mode: 'block', mode: 'block',
preFormat,
type: KEYS.h4, type: KEYS.h4,
}, },
{ {
match: '##### ', match: '##### ',
mode: 'block', mode: 'block',
preFormat,
type: KEYS.h5, type: KEYS.h5,
}, },
{ {
match: '###### ', match: '###### ',
mode: 'block', mode: 'block',
preFormat,
type: KEYS.h6, type: KEYS.h6,
}, },
{ {
match: '> ', match: '> ',
mode: 'block', mode: 'block',
preFormat,
type: KEYS.blockquote, type: KEYS.blockquote,
}, },
{ {
match: '```', match: '```',
mode: 'block', mode: 'block',
preFormat,
type: KEYS.codeBlock, type: KEYS.codeBlock,
format: (editor) => { format: (editor) => {
insertEmptyCodeBlock(editor, { insertEmptyCodeBlock(editor, {
@ -198,29 +160,52 @@ const autoformatLists: AutoformatRule[] = [
{ {
match: ['* ', '- '], match: ['* ', '- '],
mode: 'block', mode: 'block',
preFormat, type: 'list',
type: KEYS.li, format: (editor) => {
format: (editor) => formatList(editor, KEYS.ulClassic), toggleList(editor, {
listStyleType: KEYS.ul,
});
},
}, },
{ {
match: [String.raw`^\d+\.$ `, String.raw`^\d+\)$ `], match: [String.raw`^\d+\.$ `, String.raw`^\d+\)$ `],
matchByRegex: true, matchByRegex: true,
mode: 'block', mode: 'block',
preFormat, type: 'list',
type: KEYS.li, format: (editor, { matchString }) => {
format: (editor) => formatList(editor, KEYS.olClassic), toggleList(editor, {
listRestartPolite: Number(matchString) || 1,
listStyleType: KEYS.ol,
});
},
}, },
{ {
match: '[] ', match: ['[] '],
mode: 'block', mode: 'block',
type: KEYS.taskList, type: 'list',
format: (editor) => formatTaskList(editor, false), format: (editor) => {
toggleList(editor, {
listStyleType: KEYS.listTodo,
});
editor.tf.setNodes({
checked: false,
listStyleType: KEYS.listTodo,
});
},
}, },
{ {
match: '[x] ', match: ['[x] '],
mode: 'block', mode: 'block',
type: KEYS.taskList, type: 'list',
format: (editor) => formatTaskList(editor, true), format: (editor) => {
toggleList(editor, {
listStyleType: KEYS.listTodo,
});
editor.tf.setNodes({
checked: true,
listStyleType: KEYS.listTodo,
});
},
}, },
]; ];
@ -238,13 +223,16 @@ export const AutoformatKit = [
...autoformatArrow, ...autoformatArrow,
...autoformatMath, ...autoformatMath,
...autoformatLists, ...autoformatLists,
].map((rule) => ({ ].map(
...rule, (rule): AutoformatRule => ({
query: (editor) => ...rule,
!editor.api.some({ query: (editor) =>
match: { type: editor.getType(KEYS.codeBlock) }, !editor.api.some({
}), match: { type: editor.getType(KEYS.codeBlock) },
})), }),
})
),
}, },
}), }),
]; ];

View file

@ -0,0 +1,23 @@
'use client';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndPlugin } from '@platejs/dnd';
import { BlockDraggable } from '@/components/ui/block-draggable';
export const DndKit = [
DndPlugin.configure({
options: {
enableScroller: true,
},
render: {
aboveNodes: BlockDraggable,
aboveSlate: ({ children }) => (
<DndProvider backend={HTML5Backend}>{children}</DndProvider>
),
},
}),
];

View file

@ -1,36 +0,0 @@
'use client';
import {
BulletedListPlugin,
ListItemContentPlugin,
ListItemPlugin,
ListPlugin,
NumberedListPlugin,
TaskListPlugin,
} from '@platejs/list-classic/react';
import {
BulletedListElement,
ListItemElement,
NumberedListElement,
TaskListElement,
} from '@/components/ui/list-classic-node';
export const ListKit = [
ListPlugin,
ListItemPlugin,
ListItemContentPlugin,
BulletedListPlugin.configure({
node: { component: BulletedListElement },
shortcuts: { toggle: { keys: 'mod+alt+5' } },
}),
NumberedListPlugin.configure({
node: { component: NumberedListElement },
shortcuts: { toggle: { keys: 'mod+alt+6' } },
}),
TaskListPlugin.configure({
node: { component: TaskListElement },
shortcuts: { toggle: { keys: 'mod+alt+7' } },
}),
ListItemPlugin.withComponent(ListItemElement),
];

View file

@ -0,0 +1,26 @@
'use client';
import { ListPlugin } from '@platejs/list/react';
import { KEYS } from 'platejs';
import { IndentKit } from '@/components/editor/plugins/indent-kit';
import { BlockList } from '@/components/ui/block-list';
export const ListKit = [
...IndentKit,
ListPlugin.configure({
inject: {
targetPlugins: [
...KEYS.heading,
KEYS.p,
KEYS.blockquote,
KEYS.codeBlock,
KEYS.toggle,
],
},
render: {
belowNodes: BlockList,
},
}),
];

View file

@ -0,0 +1,513 @@
'use client';
import * as React from 'react';
import { DndPlugin, useDraggable, useDropLine } from '@platejs/dnd';
import { expandListItemsWithChildren } from '@platejs/list';
import { BlockSelectionPlugin } from '@platejs/selection/react';
import { GripVertical } from 'lucide-react';
import { type TElement, getPluginByType, isType, KEYS } from 'platejs';
import {
type PlateEditor,
type PlateElementProps,
type RenderNodeWrapper,
MemoizedChildren,
useEditorRef,
useElement,
usePluginOption,
} from 'platejs/react';
import { useSelected } from 'platejs/react';
import { Button } from '@/components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
const UNDRAGGABLE_KEYS = [KEYS.column, KEYS.tr, KEYS.td];
export const BlockDraggable: RenderNodeWrapper = (props) => {
const { editor, element, path } = props;
const enabled = React.useMemo(() => {
if (editor.dom.readOnly) return false;
if (path.length === 1 && !isType(editor, element, UNDRAGGABLE_KEYS)) {
return true;
}
if (path.length === 3 && !isType(editor, element, UNDRAGGABLE_KEYS)) {
const block = editor.api.some({
at: path,
match: {
type: editor.getType(KEYS.column),
},
});
if (block) {
return true;
}
}
if (path.length === 4 && !isType(editor, element, UNDRAGGABLE_KEYS)) {
const block = editor.api.some({
at: path,
match: {
type: editor.getType(KEYS.table),
},
});
if (block) {
return true;
}
}
return false;
}, [editor, element, path]);
if (!enabled) return;
return (props) => <Draggable {...props} />;
};
function Draggable(props: PlateElementProps) {
const { children, editor, element, path } = props;
const blockSelectionApi = editor.getApi(BlockSelectionPlugin).blockSelection;
const { isAboutToDrag, isDragging, nodeRef, previewRef, handleRef } =
useDraggable({
element,
onDropHandler: (_, { dragItem }) => {
const id = (dragItem as { id: string[] | string }).id;
if (blockSelectionApi) {
blockSelectionApi.add(id);
}
resetPreview();
},
});
const isInColumn = path.length === 3;
const isInTable = path.length === 4;
const [previewTop, setPreviewTop] = React.useState(0);
const resetPreview = () => {
if (previewRef.current) {
previewRef.current.replaceChildren();
previewRef.current?.classList.add('hidden');
}
};
// clear up virtual multiple preview when drag end
React.useEffect(() => {
if (!isDragging) {
resetPreview();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDragging]);
React.useEffect(() => {
if (isAboutToDrag) {
previewRef.current?.classList.remove('opacity-0');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAboutToDrag]);
const [dragButtonTop, setDragButtonTop] = React.useState(0);
return (
<div
className={cn(
'relative',
isDragging && 'opacity-50',
getPluginByType(editor, element.type)?.node.isContainer
? 'group/container'
: 'group'
)}
onMouseEnter={() => {
if (isDragging) return;
setDragButtonTop(calcDragButtonTop(editor, element));
}}
>
{!isInTable && (
<Gutter>
<div
className={cn(
'slate-blockToolbarWrapper',
'flex h-[1.5em]',
isInColumn && 'h-4'
)}
>
<div
className={cn(
'slate-blockToolbar relative w-4.5',
'pointer-events-auto mr-1 flex items-center',
isInColumn && 'mr-1.5'
)}
>
<Button
ref={handleRef}
variant="ghost"
className="-left-0 absolute h-6 w-full p-0"
style={{ top: `${dragButtonTop + 3}px` }}
data-plate-prevent-deselect
>
<DragHandle
isDragging={isDragging}
previewRef={previewRef}
resetPreview={resetPreview}
setPreviewTop={setPreviewTop}
/>
</Button>
</div>
</div>
</Gutter>
)}
<div
ref={previewRef}
className={cn('-left-0 absolute hidden w-full')}
style={{ top: `${-previewTop}px` }}
contentEditable={false}
/>
<div
ref={nodeRef}
className="slate-blockWrapper flow-root"
onContextMenu={(event) =>
editor
.getApi(BlockSelectionPlugin)
.blockSelection.addOnContextMenu({ element, event })
}
>
<MemoizedChildren>{children}</MemoizedChildren>
<DropLine />
</div>
</div>
);
}
function Gutter({
children,
className,
...props
}: React.ComponentProps<'div'>) {
const editor = useEditorRef();
const element = useElement();
const isSelectionAreaVisible = usePluginOption(
BlockSelectionPlugin,
'isSelectionAreaVisible'
);
const selected = useSelected();
return (
<div
{...props}
className={cn(
'slate-gutterLeft',
'-translate-x-full absolute top-0 z-50 flex h-full cursor-text hover:opacity-100 sm:opacity-0',
getPluginByType(editor, element.type)?.node.isContainer
? 'group-hover/container:opacity-100'
: 'group-hover:opacity-100',
isSelectionAreaVisible && 'hidden',
!selected && 'opacity-0',
className
)}
contentEditable={false}
>
{children}
</div>
);
}
const DragHandle = React.memo(function DragHandle({
isDragging,
previewRef,
resetPreview,
setPreviewTop,
}: {
isDragging: boolean;
previewRef: React.RefObject<HTMLDivElement | null>;
resetPreview: () => void;
setPreviewTop: (top: number) => void;
}) {
const editor = useEditorRef();
const element = useElement();
return (
<Tooltip>
<TooltipTrigger asChild>
<div
className="flex size-full items-center justify-center"
onClick={(e) => {
e.preventDefault();
editor.getApi(BlockSelectionPlugin).blockSelection.focus();
}}
onMouseDown={(e) => {
resetPreview();
if ((e.button !== 0 && e.button !== 2) || e.shiftKey) return;
const blockSelection = editor
.getApi(BlockSelectionPlugin)
.blockSelection.getNodes({ sort: true });
let selectionNodes =
blockSelection.length > 0
? blockSelection
: editor.api.blocks({ mode: 'highest' });
// If current block is not in selection, use it as the starting point
if (!selectionNodes.some(([node]) => node.id === element.id)) {
selectionNodes = [[element, editor.api.findPath(element)!]];
}
// Process selection nodes to include list children
const blocks = expandListItemsWithChildren(
editor,
selectionNodes
).map(([node]) => node);
if (blockSelection.length === 0) {
editor.tf.blur();
editor.tf.collapse();
}
const elements = createDragPreviewElements(editor, blocks);
previewRef.current?.append(...elements);
previewRef.current?.classList.remove('hidden');
previewRef.current?.classList.add('opacity-0');
editor.setOption(DndPlugin, 'multiplePreviewRef', previewRef);
editor
.getApi(BlockSelectionPlugin)
.blockSelection.set(blocks.map((block) => block.id as string));
}}
onMouseEnter={() => {
if (isDragging) return;
const blockSelection = editor
.getApi(BlockSelectionPlugin)
.blockSelection.getNodes({ sort: true });
let selectedBlocks =
blockSelection.length > 0
? blockSelection
: editor.api.blocks({ mode: 'highest' });
// If current block is not in selection, use it as the starting point
if (!selectedBlocks.some(([node]) => node.id === element.id)) {
selectedBlocks = [[element, editor.api.findPath(element)!]];
}
// Process selection to include list children
const processedBlocks = expandListItemsWithChildren(
editor,
selectedBlocks
);
const ids = processedBlocks.map((block) => block[0].id as string);
if (ids.length > 1 && ids.includes(element.id as string)) {
const previewTop = calculatePreviewTop(editor, {
blocks: processedBlocks.map((block) => block[0]),
element,
});
setPreviewTop(previewTop);
} else {
setPreviewTop(0);
}
}}
onMouseUp={() => {
resetPreview();
}}
data-plate-prevent-deselect
role="button"
>
<GripVertical className="text-muted-foreground" />
</div>
</TooltipTrigger>
</Tooltip>
);
});
const DropLine = React.memo(function DropLine({
className,
...props
}: React.ComponentProps<'div'>) {
const { dropLine } = useDropLine();
if (!dropLine) return null;
return (
<div
{...props}
className={cn(
'slate-dropLine',
'absolute inset-x-0 h-0.5 opacity-100 transition-opacity',
'bg-brand/50',
dropLine === 'top' && '-top-px',
dropLine === 'bottom' && '-bottom-px',
className
)}
/>
);
});
const createDragPreviewElements = (
editor: PlateEditor,
blocks: TElement[]
): HTMLElement[] => {
const elements: HTMLElement[] = [];
const ids: string[] = [];
/**
* Remove data attributes from the element to avoid recognized as slate
* elements incorrectly.
*/
const removeDataAttributes = (element: HTMLElement) => {
Array.from(element.attributes).forEach((attr) => {
if (
attr.name.startsWith('data-slate') ||
attr.name.startsWith('data-block-id')
) {
element.removeAttribute(attr.name);
}
});
Array.from(element.children).forEach((child) => {
removeDataAttributes(child as HTMLElement);
});
};
const resolveElement = (node: TElement, index: number) => {
const domNode = editor.api.toDOMNode(node)!;
const newDomNode = domNode.cloneNode(true) as HTMLElement;
// Apply visual compensation for horizontal scroll
const applyScrollCompensation = (
original: Element,
cloned: HTMLElement
) => {
const scrollLeft = original.scrollLeft;
if (scrollLeft > 0) {
// Create a wrapper to handle the scroll offset
const scrollWrapper = document.createElement('div');
scrollWrapper.style.overflow = 'hidden';
scrollWrapper.style.width = `${original.clientWidth}px`;
// Create inner container with the full content
const innerContainer = document.createElement('div');
innerContainer.style.transform = `translateX(-${scrollLeft}px)`;
innerContainer.style.width = `${original.scrollWidth}px`;
// Move all children to the inner container
while (cloned.firstChild) {
innerContainer.append(cloned.firstChild);
}
// Apply the original element's styles to maintain appearance
const originalStyles = window.getComputedStyle(original);
cloned.style.padding = '0';
innerContainer.style.padding = originalStyles.padding;
scrollWrapper.append(innerContainer);
cloned.append(scrollWrapper);
}
};
applyScrollCompensation(domNode, newDomNode);
ids.push(node.id as string);
const wrapper = document.createElement('div');
wrapper.append(newDomNode);
wrapper.style.display = 'flow-root';
const lastDomNode = blocks[index - 1];
if (lastDomNode) {
const lastDomNodeRect = editor.api
.toDOMNode(lastDomNode)!
.parentElement!.getBoundingClientRect();
const domNodeRect = domNode.parentElement!.getBoundingClientRect();
const distance = domNodeRect.top - lastDomNodeRect.bottom;
// Check if the two elements are adjacent (touching each other)
if (distance > 15) {
wrapper.style.marginTop = `${distance}px`;
}
}
removeDataAttributes(newDomNode);
elements.push(wrapper);
};
blocks.forEach((node, index) => {
resolveElement(node, index);
});
editor.setOption(DndPlugin, 'draggingId', ids);
return elements;
};
const calculatePreviewTop = (
editor: PlateEditor,
{
blocks,
element,
}: {
blocks: TElement[];
element: TElement;
}
): number => {
const child = editor.api.toDOMNode(element)!;
const editable = editor.api.toDOMNode(editor)!;
const firstSelectedChild = blocks[0];
const firstDomNode = editor.api.toDOMNode(firstSelectedChild)!;
// Get editor's top padding
const editorPaddingTop = Number(
window.getComputedStyle(editable).paddingTop.replace('px', '')
);
// Calculate distance from first selected node to editor top
const firstNodeToEditorDistance =
firstDomNode.getBoundingClientRect().top -
editable.getBoundingClientRect().top -
editorPaddingTop;
// Get margin top of first selected node
const firstMarginTopString = window.getComputedStyle(firstDomNode).marginTop;
const marginTop = Number(firstMarginTopString.replace('px', ''));
// Calculate distance from current node to editor top
const currentToEditorDistance =
child.getBoundingClientRect().top -
editable.getBoundingClientRect().top -
editorPaddingTop;
const currentMarginTopString = window.getComputedStyle(child).marginTop;
const currentMarginTop = Number(currentMarginTopString.replace('px', ''));
const previewElementsTopDistance =
currentToEditorDistance -
firstNodeToEditorDistance +
marginTop -
currentMarginTop;
return previewElementsTopDistance;
};
const calcDragButtonTop = (editor: PlateEditor, element: TElement): number => {
const child = editor.api.toDOMNode(element)!;
const currentMarginTopString = window.getComputedStyle(child).marginTop;
const currentMarginTop = Number(currentMarginTopString.replace('px', ''));
return currentMarginTop;
};

View file

@ -0,0 +1,85 @@
import * as React from 'react';
import type { RenderStaticNodeWrapper, TListElement } from 'platejs';
import type { SlateRenderElementProps } from 'platejs/static';
import { isOrderedList } from '@platejs/list';
import { CheckIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
const config: Record<
string,
{
Li: React.FC<SlateRenderElementProps>;
Marker: React.FC<SlateRenderElementProps>;
}
> = {
todo: {
Li: TodoLiStatic,
Marker: TodoMarkerStatic,
},
};
export const BlockListStatic: RenderStaticNodeWrapper = (props) => {
if (!props.element.listStyleType) return;
return (props) => <List {...props} />;
};
function List(props: SlateRenderElementProps) {
const { indent, listStart, listStyleType } = props.element as TListElement & {
indent?: number;
};
const { Li, Marker } = config[listStyleType] ?? {};
const List = isOrderedList(props.element) ? 'ol' : 'ul';
// Apply margin-left for indent (24px per level) for DOCX export compatibility
const marginLeft = indent ? `${indent * 24}px` : undefined;
return (
<List
className="relative m-0 p-0"
style={{ listStyleType, marginLeft }}
start={listStart}
>
{Marker && <Marker {...props} />}
{Li ? <Li {...props} /> : <li>{props.children}</li>}
</List>
);
}
function TodoMarkerStatic(props: SlateRenderElementProps) {
const checked = props.element.checked as boolean;
return (
<div contentEditable={false}>
<button
className={cn(
'peer -left-6 pointer-events-none absolute top-1 size-4 shrink-0 rounded-sm border border-primary bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.className
)}
data-state={checked ? 'checked' : 'unchecked'}
type="button"
>
<div className={cn('flex items-center justify-center text-current')}>
{checked && <CheckIcon className="size-4" />}
</div>
</button>
</div>
);
}
function TodoLiStatic(props: SlateRenderElementProps) {
return (
<li
className={cn(
'list-none',
(props.element.checked as boolean) &&
'text-muted-foreground line-through'
)}
>
{props.children}
</li>
);
}

View file

@ -0,0 +1,87 @@
'use client';
import React from 'react';
import type { TListElement } from 'platejs';
import { isOrderedList } from '@platejs/list';
import {
useTodoListElement,
useTodoListElementState,
} from '@platejs/list/react';
import {
type PlateElementProps,
type RenderNodeWrapper,
useReadOnly,
} from 'platejs/react';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
const config: Record<
string,
{
Li: React.FC<PlateElementProps>;
Marker: React.FC<PlateElementProps>;
}
> = {
todo: {
Li: TodoLi,
Marker: TodoMarker,
},
};
export const BlockList: RenderNodeWrapper = (props) => {
if (!props.element.listStyleType) return;
return (props) => <List {...props} />;
};
function List(props: PlateElementProps) {
const { listStart, listStyleType } = props.element as TListElement;
const { Li, Marker } = config[listStyleType] ?? {};
const List = isOrderedList(props.element) ? 'ol' : 'ul';
return (
<List
className="relative m-0 p-0"
style={{ listStyleType }}
start={listStart}
>
{Marker && <Marker {...props} />}
{Li ? <Li {...props} /> : <li>{props.children}</li>}
</List>
);
}
function TodoMarker(props: PlateElementProps) {
const state = useTodoListElementState({ element: props.element });
const { checkboxProps } = useTodoListElement(state);
const readOnly = useReadOnly();
return (
<div contentEditable={false}>
<Checkbox
className={cn(
'-left-6 absolute top-1',
readOnly && 'pointer-events-none'
)}
{...checkboxProps}
/>
</div>
);
}
function TodoLi(props: PlateElementProps) {
return (
<li
className={cn(
'list-none',
(props.element.checked as boolean) &&
'text-muted-foreground line-through'
)}
>
{props.children}
</li>
);
}

View file

@ -1,30 +1,32 @@
"use client"; "use client"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; import * as React from "react"
import { CheckIcon, MinusIcon } from "lucide-react"; import { CheckIcon } from "lucide-react"
import type * as React from "react"; import { Checkbox as CheckboxPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) { function Checkbox({
return ( className,
<CheckboxPrimitive.Root ...props
data-slot="checkbox" }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
className={cn( return (
"peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary data-[state=indeterminate]:bg-transparent data-[state=indeterminate]:text-foreground data-[state=indeterminate]:border-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", <CheckboxPrimitive.Root
className data-slot="checkbox"
)} className={cn(
{...props} "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
> className
<CheckboxPrimitive.Indicator )}
data-slot="checkbox-indicator" {...props}
className="group flex items-center justify-center text-current transition-none" >
> <CheckboxPrimitive.Indicator
<CheckIcon className="size-3.5 hidden group-data-[state=checked]:block" /> data-slot="checkbox-indicator"
<MinusIcon className="size-3.5 hidden group-data-[state=indeterminate]:block" /> className="grid place-content-center text-current transition-none"
</CheckboxPrimitive.Indicator> >
</CheckboxPrimitive.Root> <CheckIcon className="size-3.5" />
); </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
} }
export { Checkbox }; export { Checkbox }

View file

@ -1,105 +0,0 @@
'use client';
import * as React from 'react';
import type { PlateElementProps } from 'platejs/react';
import {
useTodoListElement,
useTodoListElementState,
} from '@platejs/list-classic/react';
import { type VariantProps, cva } from 'class-variance-authority';
import { PlateElement } from 'platejs/react';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
const listVariants = cva('m-0 py-1 ps-6', {
variants: {
variant: {
ol: 'list-decimal',
ul: 'list-disc [&_ul]:list-[circle] [&_ul_ul]:list-[square]',
},
},
});
export function ListElement({
variant,
...props
}: PlateElementProps & VariantProps<typeof listVariants>) {
return (
<PlateElement
as={variant!}
className={listVariants({ variant })}
{...props}
>
{props.children}
</PlateElement>
);
}
export function BulletedListElement(props: PlateElementProps) {
return <ListElement variant="ul" {...props} />;
}
export function NumberedListElement(props: PlateElementProps) {
return <ListElement variant="ol" {...props} />;
}
export function TaskListElement(props: PlateElementProps) {
return (
<PlateElement as="ul" className="m-0 list-none! py-1 ps-6" {...props}>
{props.children}
</PlateElement>
);
}
export function ListItemElement(props: PlateElementProps) {
const isTaskList = 'checked' in props.element;
if (isTaskList) {
return <TaskListItemElement {...props} />;
}
return <BaseListItemElement {...props} />;
}
export function BaseListItemElement(props: PlateElementProps) {
return (
<PlateElement as="li" {...props}>
{props.children}
</PlateElement>
);
}
export function TaskListItemElement(props: PlateElementProps) {
const { element } = props;
const state = useTodoListElementState({ element });
const { checkboxProps } = useTodoListElement(state);
const [firstChild, ...otherChildren] = React.Children.toArray(props.children);
return (
<BaseListItemElement {...props}>
<div
className={cn(
'flex items-stretch *:nth-[2]:flex-1 *:nth-[2]:focus:outline-none',
{
'*:nth-[2]:text-muted-foreground *:nth-[2]:line-through':
state.checked,
}
)}
>
<div
className="-ms-5 me-1.5 flex w-fit select-none items-start justify-center pt-[0.275em]"
contentEditable={false}
>
<Checkbox {...checkboxProps} />
</div>
{firstChild}
</div>
{otherChildren}
</BaseListItemElement>
);
}

View file

@ -1,6 +1,7 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import { createPortal } from 'react-dom';
import type { PlateElementProps } from 'platejs/react'; import type { PlateElementProps } from 'platejs/react';
@ -8,7 +9,6 @@ import { SlashInputPlugin } from '@platejs/slash-command/react';
import { import {
ChevronRightIcon, ChevronRightIcon,
Code2Icon, Code2Icon,
Columns2Icon,
FileCodeIcon, FileCodeIcon,
Heading1Icon, Heading1Icon,
Heading2Icon, Heading2Icon,
@ -30,7 +30,6 @@ import {
Command, Command,
CommandEmpty, CommandEmpty,
CommandGroup, CommandGroup,
CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
} from '@/components/ui/command'; } from '@/components/ui/command';
@ -161,49 +160,71 @@ export function SlashInputElement({
...props ...props
}: PlateElementProps) { }: PlateElementProps) {
const { editor, setOption } = useEditorPlugin(SlashInputPlugin); const { editor, setOption } = useEditorPlugin(SlashInputPlugin);
const anchorRef = React.useRef<HTMLSpanElement>(null);
const [menuPosition, setMenuPosition] = React.useState<{ top: number; left: number } | null>(null);
React.useEffect(() => {
const updatePosition = () => {
if (anchorRef.current) {
const rect = anchorRef.current.getBoundingClientRect();
setMenuPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
});
}
};
updatePosition();
// Re-position on scroll/resize since the editor may scroll
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
};
}, []);
return ( return (
<PlateElement {...props} as="span"> <PlateElement {...props} as="span">
<Command <span ref={anchorRef} />
className="relative z-50 min-w-[280px] overflow-hidden rounded-lg border bg-popover shadow-md" {menuPosition &&
shouldFilter={true} createPortal(
> <Command
<CommandInput className="fixed z-50 w-[330px] overflow-hidden rounded-lg border bg-popover shadow-lg"
className="hidden" style={{ top: menuPosition.top, left: menuPosition.left }}
value={props.element.value as string} shouldFilter={false}
onValueChange={(value) => { >
// The value is managed by the slash input plugin <CommandList className="max-h-[min(400px,40vh)] overflow-y-auto p-1">
}} <CommandEmpty>No results found.</CommandEmpty>
autoFocus {slashCommandGroups.map(({ heading, items }) => (
/> <CommandGroup key={heading} heading={heading}>
<CommandList className="max-h-[300px] overflow-y-auto p-1"> {items.map(({ icon, keywords, label, onSelect }) => (
<CommandEmpty>No results found.</CommandEmpty> <CommandItem
{slashCommandGroups.map(({ heading, items }) => ( key={label}
<CommandGroup key={heading} heading={heading}> className="flex items-center gap-3 px-2 py-2"
{items.map(({ icon, keywords, label, onSelect }) => ( keywords={keywords}
<CommandItem value={label}
key={label} onSelect={() => {
className="flex items-center gap-2 px-2" editor.tf.removeNodes({
keywords={keywords} match: (n) => (n as any).type === SlashInputPlugin.key,
value={label} });
onSelect={() => { onSelect(editor);
editor.tf.removeNodes({ editor.tf.focus();
match: (n) => (n as any).type === SlashInputPlugin.key, }}
}); >
onSelect(editor); <span className="flex size-5 items-center justify-center text-muted-foreground">
editor.tf.focus(); {icon}
}} </span>
> {label}
<span className="flex size-5 items-center justify-center text-muted-foreground"> </CommandItem>
{icon} ))}
</span> </CommandGroup>
{label}
</CommandItem>
))} ))}
</CommandGroup> </CommandList>
))} </Command>,
</CommandList> document.body
</Command> )}
{children} {children}
</PlateElement> </PlateElement>
); );

View file

@ -1,58 +1,57 @@
"use client"; "use client"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as React from "react"
import type * as React from "react"; import { Tooltip as TooltipPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
function TooltipProvider({ function TooltipProvider({
delayDuration = 0, delayDuration = 0,
disableHoverableContent = true, ...props
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return ( return (
<TooltipPrimitive.Provider <TooltipPrimitive.Provider
data-slot="tooltip-provider" data-slot="tooltip-provider"
delayDuration={delayDuration} delayDuration={delayDuration}
disableHoverableContent={disableHoverableContent} {...props}
{...props} />
/> )
);
} }
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) { function Tooltip({
return ( ...props
<TooltipProvider> }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
<TooltipPrimitive.Root data-slot="tooltip" {...props} /> return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
} }
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { function TooltipTrigger({
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; ...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
} }
function TooltipContent({ function TooltipContent({
className, className,
sideOffset = 4, sideOffset = 0,
children, children,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return ( return (
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipPrimitive.Content <TooltipPrimitive.Content
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-black text-white font-medium shadow-xl px-3 py-1.5 dark:bg-zinc-800 dark:text-zinc-50 border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-balance pointer-events-none", "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className className
)} )}
{...props} {...props}
> >
{children} {children}
</TooltipPrimitive.Content> <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Portal> </TooltipPrimitive.Content>
); </TooltipPrimitive.Portal>
)
} }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View file

@ -22,6 +22,7 @@
}, },
"dependencies": { "dependencies": {
"@ai-sdk/react": "^1.2.12", "@ai-sdk/react": "^1.2.12",
"@ariakit/react": "^0.4.21",
"@assistant-ui/react": "^0.11.53", "@assistant-ui/react": "^0.11.53",
"@assistant-ui/react-ai-sdk": "^1.1.20", "@assistant-ui/react-ai-sdk": "^1.1.20",
"@assistant-ui/react-markdown": "^0.11.9", "@assistant-ui/react-markdown": "^0.11.9",
@ -39,11 +40,12 @@
"@platejs/basic-nodes": "^52.0.11", "@platejs/basic-nodes": "^52.0.11",
"@platejs/callout": "^52.0.11", "@platejs/callout": "^52.0.11",
"@platejs/code-block": "^52.0.11", "@platejs/code-block": "^52.0.11",
"@platejs/combobox": "^52.0.15",
"@platejs/dnd": "^52.0.11", "@platejs/dnd": "^52.0.11",
"@platejs/floating": "^52.0.11", "@platejs/floating": "^52.0.11",
"@platejs/indent": "^52.0.11", "@platejs/indent": "^52.0.11",
"@platejs/link": "^52.0.11", "@platejs/link": "^52.0.11",
"@platejs/list-classic": "^52.0.11", "@platejs/list": "^52.0.11",
"@platejs/markdown": "^52.1.0", "@platejs/markdown": "^52.1.0",
"@platejs/math": "^52.0.11", "@platejs/math": "^52.0.11",
"@platejs/resizable": "^52.0.11", "@platejs/resizable": "^52.0.11",
@ -114,6 +116,8 @@
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.3", "react": "^19.2.3",
"react-day-picker": "^9.13.2", "react-day-picker": "^9.13.2",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hook-form": "^7.61.1", "react-hook-form": "^7.61.1",

View file

@ -11,6 +11,9 @@ importers:
'@ai-sdk/react': '@ai-sdk/react':
specifier: ^1.2.12 specifier: ^1.2.12
version: 1.2.12(react@19.2.3)(zod@4.2.1) version: 1.2.12(react@19.2.3)(zod@4.2.1)
'@ariakit/react':
specifier: ^0.4.21
version: 0.4.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@assistant-ui/react': '@assistant-ui/react':
specifier: ^0.11.53 specifier: ^0.11.53
version: 0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) version: 0.11.53(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
@ -62,6 +65,9 @@ importers:
'@platejs/code-block': '@platejs/code-block':
specifier: ^52.0.11 specifier: ^52.0.11
version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@platejs/combobox':
specifier: ^52.0.15
version: 52.0.15(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@platejs/dnd': '@platejs/dnd':
specifier: ^52.0.11 specifier: ^52.0.11
version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/node@20.19.27)(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dnd-html5-backend@16.0.1)(react-dnd@16.0.1(@types/node@20.19.27)(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -74,7 +80,7 @@ importers:
'@platejs/link': '@platejs/link':
specifier: ^52.0.11 specifier: ^52.0.11
version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@platejs/list-classic': '@platejs/list':
specifier: ^52.0.11 specifier: ^52.0.11
version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@platejs/markdown': '@platejs/markdown':
@ -287,6 +293,12 @@ importers:
react-day-picker: react-day-picker:
specifier: ^9.13.2 specifier: ^9.13.2
version: 9.13.2(react@19.2.3) version: 9.13.2(react@19.2.3)
react-dnd:
specifier: ^16.0.1
version: 16.0.1(@types/node@20.19.27)(@types/react@19.2.7)(react@19.2.3)
react-dnd-html5-backend:
specifier: ^16.0.1
version: 16.0.1
react-dom: react-dom:
specifier: ^19.2.3 specifier: ^19.2.3
version: 19.2.3(react@19.2.3) version: 19.2.3(react@19.2.3)
@ -464,6 +476,21 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@ariakit/core@0.4.18':
resolution: {integrity: sha512-9urEa+GbZTSyredq3B/3thQjTcSZSUC68XctwCkJNH/xNfKN5O+VThiem2rcJxpsGw8sRUQenhagZi0yB4foyg==}
'@ariakit/react-core@0.4.21':
resolution: {integrity: sha512-rUI9uB/gT3mROFja/ka7/JukkdljIZR3eq3BGiQqX4Ni/KBMDvPK8FvVLnC0TGzWcqNY2bbfve8QllvHzuw4fQ==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@ariakit/react@0.4.21':
resolution: {integrity: sha512-UjP99Y7cWxA5seRECEE0RPZFImkLGFIWPflp65t0BVZwlMw4wp9OJZRHMrnkEkKl5KBE2NR/gbbzwHc6VNGzsA==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
'@asamuzakjp/css-color@3.2.0': '@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
@ -2302,8 +2329,8 @@ packages:
react: '>=18.0.0' react: '>=18.0.0'
react-dom: '>=18.0.0' react-dom: '>=18.0.0'
'@platejs/list-classic@52.0.11': '@platejs/list@52.0.11':
resolution: {integrity: sha512-cuR0UJcfT7X45dvptofbozEznx604Y263pjlK7vLFerI2k7cUBuXRKOxtUuwVD4VaWE5aHCr8cO+nb+jhdGHSQ==} resolution: {integrity: sha512-azmJhBRQsjoWLwVWgTfHxegTg02k81sEJinwNv5gDPyiDlK1Pk98Oys/7H21SpwRWh75tPR/9JfV0ZGMqAHEJA==}
peerDependencies: peerDependencies:
platejs: '>=52.0.11' platejs: '>=52.0.11'
react: '>=18.0.0' react: '>=18.0.0'
@ -7815,6 +7842,22 @@ snapshots:
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
'@ariakit/core@0.4.18': {}
'@ariakit/react-core@0.4.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@ariakit/core': 0.4.18
'@floating-ui/dom': 1.7.5
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
use-sync-external-store: 1.6.0(react@19.2.3)
'@ariakit/react@0.4.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@ariakit/react-core': 0.4.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@asamuzakjp/css-color@3.2.0': '@asamuzakjp/css-color@3.2.0':
dependencies: dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
@ -9220,7 +9263,7 @@ snapshots:
postcss-selector-parser: 7.1.1 postcss-selector-parser: 7.1.1
react: 19.2.3 react: 19.2.3
react-dom: 19.2.3(react@19.2.3) react-dom: 19.2.3(react@19.2.3)
tailwind-merge: 3.4.0 tailwind-merge: 3.4.1
optionalDependencies: optionalDependencies:
'@types/react': 19.2.7 '@types/react': 19.2.7
next: 16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next: 16.1.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -9731,9 +9774,10 @@ snapshots:
react-compiler-runtime: 1.0.0(react@19.2.3) react-compiler-runtime: 1.0.0(react@19.2.3)
react-dom: 19.2.3(react@19.2.3) react-dom: 19.2.3(react@19.2.3)
'@platejs/list-classic@52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': '@platejs/list@52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies: dependencies:
lodash: 4.17.23 '@platejs/indent': 52.0.11(platejs@52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
clsx: 2.1.1
platejs: 52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3)) platejs: 52.0.17(@types/react@19.2.7)(immer@10.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(scheduler@0.27.0)(use-sync-external-store@1.6.0(react@19.2.3))
react: 19.2.3 react: 19.2.3
react-compiler-runtime: 1.0.0(react@19.2.3) react-compiler-runtime: 1.0.0(react@19.2.3)