chore: ran linting

This commit is contained in:
Anish Sarkar 2026-02-17 12:47:39 +05:30
parent e46b24a2b1
commit a482cc95de
67 changed files with 4971 additions and 5539 deletions

View file

@ -72,7 +72,9 @@ def _populate_source_markdown(conn, batch_size: int = 500) -> None:
print("No documents with blocknote_document need migration")
return
print(f" Migrating {total} documents (with blocknote_document) to source_markdown...")
print(
f" Migrating {total} documents (with blocknote_document) to source_markdown..."
)
migrated = 0
failed = 0

View file

@ -107,8 +107,12 @@ def create_create_notion_page_tool(
}
)
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
decisions_raw = (
approval.get("decisions", []) if isinstance(approval, dict) else []
)
decisions = (
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
)
decisions = [d for d in decisions if isinstance(d, dict)]
if not decisions:
logger.warning("No approval decision received")

View file

@ -119,8 +119,12 @@ def create_delete_notion_page_tool(
}
)
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
decisions_raw = (
approval.get("decisions", []) if isinstance(approval, dict) else []
)
decisions = (
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
)
decisions = [d for d in decisions if isinstance(d, dict)]
if not decisions:
logger.warning("No approval decision received")

View file

@ -128,8 +128,12 @@ def create_update_notion_page_tool(
}
)
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
decisions_raw = (
approval.get("decisions", []) if isinstance(approval, dict) else []
)
decisions = (
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
)
decisions = [d for d in decisions if isinstance(d, dict)]
if not decisions:
logger.warning("No approval decision received")

View file

@ -126,9 +126,7 @@ async def get_editor_content(
"title": document.title,
"document_type": document.document_type.value,
"source_markdown": markdown_content,
"updated_at": document.updated_at.isoformat()
if document.updated_at
else None,
"updated_at": document.updated_at.isoformat() if document.updated_at else None,
}
@ -172,14 +170,10 @@ async def save_document(
source_markdown = data.get("source_markdown")
if source_markdown is None:
raise HTTPException(
status_code=400, detail="source_markdown is required"
)
raise HTTPException(status_code=400, detail="source_markdown is required")
if not isinstance(source_markdown, str):
raise HTTPException(
status_code=400, detail="source_markdown must be a string"
)
raise HTTPException(status_code=400, detail="source_markdown must be a string")
# For NOTE type, extract title from first heading line if present
if document.document_type == DocumentType.NOTE:

View file

@ -22,6 +22,7 @@ logger = logging.getLogger(__name__)
# Inline content → markdown text
# ---------------------------------------------------------------------------
def _render_inline_content(content: list[dict[str, Any]] | None) -> str:
"""Convert BlockNote inline content array to a markdown string."""
if not content:
@ -215,6 +216,7 @@ def _render_block(block: dict[str, Any], indent: int = 0) -> list[str]:
# Public API
# ---------------------------------------------------------------------------
def blocknote_to_markdown(
blocks: list[dict[str, Any]] | dict[str, Any] | None,
) -> str | None:
@ -248,7 +250,9 @@ def blocknote_to_markdown(
blocks = [blocks]
if not isinstance(blocks, list):
logger.warning(f"blocknote_to_markdown received unexpected type: {type(blocks)}")
logger.warning(
f"blocknote_to_markdown received unexpected type: {type(blocks)}"
)
return None
all_lines: list[str] = []
@ -272,10 +276,10 @@ def blocknote_to_markdown(
# Add a blank line between blocks (standard markdown spacing)
# Exception: consecutive list items of the same type don't get extra blank lines
if all_lines and block_lines:
same_list = (
(block_type == prev_type and block_type in (
"bulletListItem", "numberedListItem", "checkListItem"
))
same_list = block_type == prev_type and block_type in (
"bulletListItem",
"numberedListItem",
"checkListItem",
)
if not same_list:
all_lines.append("")
@ -285,4 +289,3 @@ def blocknote_to_markdown(
result = "\n".join(all_lines).strip()
return result if result else None

View file

@ -27,7 +27,14 @@ import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-
// Dynamically import PlateEditor (uses 'use client' internally)
const PlateEditor = dynamic(
() => import("@/components/editor/plate-editor").then((mod) => ({ default: mod.PlateEditor })),
{ ssr: false, loading: () => <div className="flex items-center justify-center py-12"><Spinner size="xl" className="text-primary" /></div> }
{
ssr: false,
loading: () => (
<div className="flex items-center justify-center py-12">
<Spinner size="xl" className="text-primary" />
</div>
),
}
);
interface EditorContent {
@ -182,15 +189,12 @@ export default function EditorPage() {
}, [isNote, document?.title, document?.source_markdown, hasUnsavedChanges]);
// Handle markdown changes from the Plate editor
const handleMarkdownChange = useCallback(
(md: string) => {
markdownRef.current = md;
if (initialLoadDone.current) {
setHasUnsavedChanges(true);
}
},
[]
);
const handleMarkdownChange = useCallback((md: string) => {
markdownRef.current = md;
if (initialLoadDone.current) {
setHasUnsavedChanges(true);
}
}, []);
// Save handler
const handleSave = useCallback(async () => {
@ -438,10 +442,11 @@ export default function EditorPage() {
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleCancelLeave}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleSaveAndLeave}>
Save
</AlertDialogAction>
<AlertDialogAction onClick={handleConfirmLeave} className="border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground">
<AlertDialogAction onClick={handleSaveAndLeave}>Save</AlertDialogAction>
<AlertDialogAction
onClick={handleConfirmLeave}
className="border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
>
Leave without saving
</AlertDialogAction>
</AlertDialogFooter>

View file

@ -1,24 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@plate": "https://platejs.org/r/{name}.json"
}
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@plate": "https://platejs.org/r/{name}.json"
}
}

View file

@ -1,25 +1,24 @@
'use client';
"use client";
import { createContext, useContext } from 'react';
import { createContext, useContext } from "react";
interface EditorSaveContextValue {
/** Callback to save the current editor content */
onSave?: () => void;
/** Whether there are unsaved changes */
hasUnsavedChanges: boolean;
/** Whether a save operation is in progress */
isSaving: boolean;
/** Whether the user can toggle between editing and viewing modes */
canToggleMode: boolean;
/** Callback to save the current editor content */
onSave?: () => void;
/** Whether there are unsaved changes */
hasUnsavedChanges: boolean;
/** Whether a save operation is in progress */
isSaving: boolean;
/** Whether the user can toggle between editing and viewing modes */
canToggleMode: boolean;
}
export const EditorSaveContext = createContext<EditorSaveContextValue>({
hasUnsavedChanges: false,
isSaving: false,
canToggleMode: false,
hasUnsavedChanges: false,
isSaving: false,
canToggleMode: false,
});
export function useEditorSave() {
return useContext(EditorSaveContext);
return useContext(EditorSaveContext);
}

View file

@ -1,152 +1,150 @@
'use client';
"use client";
import { useEffect, useRef } from 'react';
import { MarkdownPlugin, remarkMdx } from '@platejs/markdown';
import { Plate, usePlateEditor } from 'platejs/react';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { useEffect, useRef } from "react";
import { MarkdownPlugin, remarkMdx } from "@platejs/markdown";
import { Plate, usePlateEditor } from "platejs/react";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { AutoformatKit } from '@/components/editor/plugins/autoformat-kit';
import { BasicNodesKit } from '@/components/editor/plugins/basic-nodes-kit';
import { CalloutKit } from '@/components/editor/plugins/callout-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 { FloatingToolbarKit } from '@/components/editor/plugins/floating-toolbar-kit';
import { LinkKit } from '@/components/editor/plugins/link-kit';
import { ListKit } from '@/components/editor/plugins/list-kit';
import { MathKit } from '@/components/editor/plugins/math-kit';
import { SelectionKit } from '@/components/editor/plugins/selection-kit';
import { SlashCommandKit } from '@/components/editor/plugins/slash-command-kit';
import { TableKit } from '@/components/editor/plugins/table-kit';
import { ToggleKit } from '@/components/editor/plugins/toggle-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';
import { escapeMdxExpressions } from '@/components/editor/utils/escape-mdx';
import { EditorSaveContext } from '@/components/editor/editor-save-context';
import { AutoformatKit } from "@/components/editor/plugins/autoformat-kit";
import { BasicNodesKit } from "@/components/editor/plugins/basic-nodes-kit";
import { CalloutKit } from "@/components/editor/plugins/callout-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 { FloatingToolbarKit } from "@/components/editor/plugins/floating-toolbar-kit";
import { LinkKit } from "@/components/editor/plugins/link-kit";
import { ListKit } from "@/components/editor/plugins/list-kit";
import { MathKit } from "@/components/editor/plugins/math-kit";
import { SelectionKit } from "@/components/editor/plugins/selection-kit";
import { SlashCommandKit } from "@/components/editor/plugins/slash-command-kit";
import { TableKit } from "@/components/editor/plugins/table-kit";
import { ToggleKit } from "@/components/editor/plugins/toggle-kit";
import { Editor, EditorContainer } from "@/components/ui/editor";
import { escapeMdxExpressions } from "@/components/editor/utils/escape-mdx";
import { EditorSaveContext } from "@/components/editor/editor-save-context";
interface PlateEditorProps {
/** Markdown string to load as initial content */
markdown?: string;
/** Called when the editor content changes, with serialized markdown */
onMarkdownChange?: (markdown: string) => void;
/**
* Force permanent read-only mode (e.g. public/shared view).
* When true, the editor cannot be toggled to editing mode.
* When false (default), the editor starts in viewing mode but
* the user can switch to editing via the mode toolbar button.
*/
readOnly?: boolean;
/** Placeholder text */
placeholder?: string;
/** Editor container variant */
variant?: 'default' | 'demo' | 'comment' | 'select';
/** Editor text variant */
editorVariant?: 'default' | 'demo' | 'fullWidth' | 'none';
/** Additional className for the container */
className?: string;
/** Save callback. When provided, a save button appears in the toolbar on unsaved changes. */
onSave?: () => void;
/** Whether there are unsaved changes */
hasUnsavedChanges?: boolean;
/** Whether a save is in progress */
isSaving?: boolean;
/** Start the editor in editing mode instead of viewing mode. Ignored when readOnly is true. */
defaultEditing?: boolean;
/** Markdown string to load as initial content */
markdown?: string;
/** Called when the editor content changes, with serialized markdown */
onMarkdownChange?: (markdown: string) => void;
/**
* Force permanent read-only mode (e.g. public/shared view).
* When true, the editor cannot be toggled to editing mode.
* When false (default), the editor starts in viewing mode but
* the user can switch to editing via the mode toolbar button.
*/
readOnly?: boolean;
/** Placeholder text */
placeholder?: string;
/** Editor container variant */
variant?: "default" | "demo" | "comment" | "select";
/** Editor text variant */
editorVariant?: "default" | "demo" | "fullWidth" | "none";
/** Additional className for the container */
className?: string;
/** Save callback. When provided, a save button appears in the toolbar on unsaved changes. */
onSave?: () => void;
/** Whether there are unsaved changes */
hasUnsavedChanges?: boolean;
/** Whether a save is in progress */
isSaving?: boolean;
/** Start the editor in editing mode instead of viewing mode. Ignored when readOnly is true. */
defaultEditing?: boolean;
}
export function PlateEditor({
markdown,
onMarkdownChange,
readOnly = false,
placeholder = 'Type...',
variant = 'default',
editorVariant = 'default',
className,
onSave,
hasUnsavedChanges = false,
isSaving = false,
defaultEditing = false,
markdown,
onMarkdownChange,
readOnly = false,
placeholder = "Type...",
variant = "default",
editorVariant = "default",
className,
onSave,
hasUnsavedChanges = false,
isSaving = false,
defaultEditing = false,
}: PlateEditorProps) {
const lastMarkdownRef = useRef(markdown);
const lastMarkdownRef = useRef(markdown);
// When readOnly is forced, always start in readOnly.
// Otherwise, respect defaultEditing to decide initial mode.
// The user can still toggle between editing/viewing via ModeToolbarButton.
const editor = usePlateEditor({
readOnly: readOnly || !defaultEditing,
plugins: [
...BasicNodesKit,
...TableKit,
...ListKit,
...CodeBlockKit,
...LinkKit,
...CalloutKit,
...ToggleKit,
...MathKit,
...SelectionKit,
...SlashCommandKit,
...FixedToolbarKit,
...FloatingToolbarKit,
...AutoformatKit,
...DndKit,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkGfm, remarkMath, remarkMdx],
},
}),
],
// Use markdown deserialization for initial value if provided
value: markdown
? (editor) =>
editor
.getApi(MarkdownPlugin)
.markdown.deserialize(escapeMdxExpressions(markdown))
: undefined,
});
// When readOnly is forced, always start in readOnly.
// Otherwise, respect defaultEditing to decide initial mode.
// The user can still toggle between editing/viewing via ModeToolbarButton.
const editor = usePlateEditor({
readOnly: readOnly || !defaultEditing,
plugins: [
...BasicNodesKit,
...TableKit,
...ListKit,
...CodeBlockKit,
...LinkKit,
...CalloutKit,
...ToggleKit,
...MathKit,
...SelectionKit,
...SlashCommandKit,
...FixedToolbarKit,
...FloatingToolbarKit,
...AutoformatKit,
...DndKit,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkGfm, remarkMath, remarkMdx],
},
}),
],
// Use markdown deserialization for initial value if provided
value: markdown
? (editor) =>
editor.getApi(MarkdownPlugin).markdown.deserialize(escapeMdxExpressions(markdown))
: undefined,
});
// Update editor content when markdown prop changes externally
// (e.g., version switching in report panel)
useEffect(() => {
if (markdown !== undefined && markdown !== lastMarkdownRef.current) {
lastMarkdownRef.current = markdown;
const newValue = editor
.getApi(MarkdownPlugin)
.markdown.deserialize(escapeMdxExpressions(markdown));
editor.tf.reset();
editor.tf.setValue(newValue);
}
}, [markdown, editor]);
// Update editor content when markdown prop changes externally
// (e.g., version switching in report panel)
useEffect(() => {
if (markdown !== undefined && markdown !== lastMarkdownRef.current) {
lastMarkdownRef.current = markdown;
const newValue = editor
.getApi(MarkdownPlugin)
.markdown.deserialize(escapeMdxExpressions(markdown));
editor.tf.reset();
editor.tf.setValue(newValue);
}
}, [markdown, editor]);
// When not forced read-only, the user can toggle between editing/viewing.
const canToggleMode = !readOnly;
// When not forced read-only, the user can toggle between editing/viewing.
const canToggleMode = !readOnly;
return (
<EditorSaveContext.Provider
value={{
onSave,
hasUnsavedChanges,
isSaving,
canToggleMode,
}}
>
<Plate
editor={editor}
// Only pass readOnly as a controlled prop when forced (permanently read-only).
// For non-forced mode, the Plate store manages readOnly internally
// (initialized to true via usePlateEditor, toggled via ModeToolbarButton).
{...(readOnly ? { readOnly: true } : {})}
onChange={({ value }) => {
if (onMarkdownChange) {
const md = editor.getApi(MarkdownPlugin).markdown.serialize({ value });
lastMarkdownRef.current = md;
onMarkdownChange(md);
}
}}
>
<EditorContainer variant={variant} className={className}>
<Editor variant={editorVariant} placeholder={placeholder} />
</EditorContainer>
</Plate>
</EditorSaveContext.Provider>
);
return (
<EditorSaveContext.Provider
value={{
onSave,
hasUnsavedChanges,
isSaving,
canToggleMode,
}}
>
<Plate
editor={editor}
// Only pass readOnly as a controlled prop when forced (permanently read-only).
// For non-forced mode, the Plate store manages readOnly internally
// (initialized to true via usePlateEditor, toggled via ModeToolbarButton).
{...(readOnly ? { readOnly: true } : {})}
onChange={({ value }) => {
if (onMarkdownChange) {
const md = editor.getApi(MarkdownPlugin).markdown.serialize({ value });
lastMarkdownRef.current = md;
onMarkdownChange(md);
}
}}
>
<EditorContainer variant={variant} className={className}>
<Editor variant={editorVariant} placeholder={placeholder} />
</EditorContainer>
</Plate>
</EditorSaveContext.Provider>
);
}

View file

@ -1,238 +1,237 @@
'use client';
"use client";
import type { AutoformatRule } from '@platejs/autoformat';
import type { AutoformatRule } from "@platejs/autoformat";
import {
autoformatArrow,
autoformatLegal,
autoformatLegalHtml,
autoformatMath,
AutoformatPlugin,
autoformatPunctuation,
autoformatSmartQuotes,
} from '@platejs/autoformat';
import { insertEmptyCodeBlock } from '@platejs/code-block';
import { toggleList } from '@platejs/list';
import { openNextToggles } from '@platejs/toggle/react';
import { KEYS } from 'platejs';
autoformatArrow,
autoformatLegal,
autoformatLegalHtml,
autoformatMath,
AutoformatPlugin,
autoformatPunctuation,
autoformatSmartQuotes,
} from "@platejs/autoformat";
import { insertEmptyCodeBlock } from "@platejs/code-block";
import { toggleList } from "@platejs/list";
import { openNextToggles } from "@platejs/toggle/react";
import { KEYS } from "platejs";
const autoformatMarks: AutoformatRule[] = [
{
match: '***',
mode: 'mark',
type: [KEYS.bold, KEYS.italic],
},
{
match: '__*',
mode: 'mark',
type: [KEYS.underline, KEYS.italic],
},
{
match: '__**',
mode: 'mark',
type: [KEYS.underline, KEYS.bold],
},
{
match: '___***',
mode: 'mark',
type: [KEYS.underline, KEYS.bold, KEYS.italic],
},
{
match: '**',
mode: 'mark',
type: KEYS.bold,
},
{
match: '__',
mode: 'mark',
type: KEYS.underline,
},
{
match: '*',
mode: 'mark',
type: KEYS.italic,
},
{
match: '_',
mode: 'mark',
type: KEYS.italic,
},
{
match: '~~',
mode: 'mark',
type: KEYS.strikethrough,
},
{
match: '^',
mode: 'mark',
type: KEYS.sup,
},
{
match: '~',
mode: 'mark',
type: KEYS.sub,
},
{
match: '==',
mode: 'mark',
type: KEYS.highlight,
},
{
match: '≡',
mode: 'mark',
type: KEYS.highlight,
},
{
match: '`',
mode: 'mark',
type: KEYS.code,
},
{
match: "***",
mode: "mark",
type: [KEYS.bold, KEYS.italic],
},
{
match: "__*",
mode: "mark",
type: [KEYS.underline, KEYS.italic],
},
{
match: "__**",
mode: "mark",
type: [KEYS.underline, KEYS.bold],
},
{
match: "___***",
mode: "mark",
type: [KEYS.underline, KEYS.bold, KEYS.italic],
},
{
match: "**",
mode: "mark",
type: KEYS.bold,
},
{
match: "__",
mode: "mark",
type: KEYS.underline,
},
{
match: "*",
mode: "mark",
type: KEYS.italic,
},
{
match: "_",
mode: "mark",
type: KEYS.italic,
},
{
match: "~~",
mode: "mark",
type: KEYS.strikethrough,
},
{
match: "^",
mode: "mark",
type: KEYS.sup,
},
{
match: "~",
mode: "mark",
type: KEYS.sub,
},
{
match: "==",
mode: "mark",
type: KEYS.highlight,
},
{
match: "≡",
mode: "mark",
type: KEYS.highlight,
},
{
match: "`",
mode: "mark",
type: KEYS.code,
},
];
const autoformatBlocks: AutoformatRule[] = [
{
match: '# ',
mode: 'block',
type: KEYS.h1,
},
{
match: '## ',
mode: 'block',
type: KEYS.h2,
},
{
match: '### ',
mode: 'block',
type: KEYS.h3,
},
{
match: '#### ',
mode: 'block',
type: KEYS.h4,
},
{
match: '##### ',
mode: 'block',
type: KEYS.h5,
},
{
match: '###### ',
mode: 'block',
type: KEYS.h6,
},
{
match: '> ',
mode: 'block',
type: KEYS.blockquote,
},
{
match: '```',
mode: 'block',
type: KEYS.codeBlock,
format: (editor) => {
insertEmptyCodeBlock(editor, {
defaultType: KEYS.p,
insertNodesOptions: { select: true },
});
},
},
{
match: '+ ',
mode: 'block',
preFormat: openNextToggles,
type: KEYS.toggle,
},
{
match: ['---', '—-', '___ '],
mode: 'block',
type: KEYS.hr,
format: (editor) => {
editor.tf.setNodes({ type: KEYS.hr });
editor.tf.insertNodes({
children: [{ text: '' }],
type: KEYS.p,
});
},
},
{
match: "# ",
mode: "block",
type: KEYS.h1,
},
{
match: "## ",
mode: "block",
type: KEYS.h2,
},
{
match: "### ",
mode: "block",
type: KEYS.h3,
},
{
match: "#### ",
mode: "block",
type: KEYS.h4,
},
{
match: "##### ",
mode: "block",
type: KEYS.h5,
},
{
match: "###### ",
mode: "block",
type: KEYS.h6,
},
{
match: "> ",
mode: "block",
type: KEYS.blockquote,
},
{
match: "```",
mode: "block",
type: KEYS.codeBlock,
format: (editor) => {
insertEmptyCodeBlock(editor, {
defaultType: KEYS.p,
insertNodesOptions: { select: true },
});
},
},
{
match: "+ ",
mode: "block",
preFormat: openNextToggles,
type: KEYS.toggle,
},
{
match: ["---", "—-", "___ "],
mode: "block",
type: KEYS.hr,
format: (editor) => {
editor.tf.setNodes({ type: KEYS.hr });
editor.tf.insertNodes({
children: [{ text: "" }],
type: KEYS.p,
});
},
},
];
const autoformatLists: AutoformatRule[] = [
{
match: ['* ', '- '],
mode: 'block',
type: 'list',
format: (editor) => {
toggleList(editor, {
listStyleType: KEYS.ul,
});
},
},
{
match: [String.raw`^\d+\.$ `, String.raw`^\d+\)$ `],
matchByRegex: true,
mode: 'block',
type: 'list',
format: (editor, { matchString }) => {
toggleList(editor, {
listRestartPolite: Number(matchString) || 1,
listStyleType: KEYS.ol,
});
},
},
{
match: ['[] '],
mode: 'block',
type: 'list',
format: (editor) => {
toggleList(editor, {
listStyleType: KEYS.listTodo,
});
editor.tf.setNodes({
checked: false,
listStyleType: KEYS.listTodo,
});
},
},
{
match: ['[x] '],
mode: 'block',
type: 'list',
format: (editor) => {
toggleList(editor, {
listStyleType: KEYS.listTodo,
});
editor.tf.setNodes({
checked: true,
listStyleType: KEYS.listTodo,
});
},
},
{
match: ["* ", "- "],
mode: "block",
type: "list",
format: (editor) => {
toggleList(editor, {
listStyleType: KEYS.ul,
});
},
},
{
match: [String.raw`^\d+\.$ `, String.raw`^\d+\)$ `],
matchByRegex: true,
mode: "block",
type: "list",
format: (editor, { matchString }) => {
toggleList(editor, {
listRestartPolite: Number(matchString) || 1,
listStyleType: KEYS.ol,
});
},
},
{
match: ["[] "],
mode: "block",
type: "list",
format: (editor) => {
toggleList(editor, {
listStyleType: KEYS.listTodo,
});
editor.tf.setNodes({
checked: false,
listStyleType: KEYS.listTodo,
});
},
},
{
match: ["[x] "],
mode: "block",
type: "list",
format: (editor) => {
toggleList(editor, {
listStyleType: KEYS.listTodo,
});
editor.tf.setNodes({
checked: true,
listStyleType: KEYS.listTodo,
});
},
},
];
export const AutoformatKit = [
AutoformatPlugin.configure({
options: {
enableUndoOnDelete: true,
rules: [
...autoformatBlocks,
...autoformatMarks,
...autoformatSmartQuotes,
...autoformatPunctuation,
...autoformatLegal,
...autoformatLegalHtml,
...autoformatArrow,
...autoformatMath,
...autoformatLists,
].map(
(rule): AutoformatRule => ({
...rule,
query: (editor) =>
!editor.api.some({
match: { type: editor.getType(KEYS.codeBlock) },
}),
})
),
},
}),
AutoformatPlugin.configure({
options: {
enableUndoOnDelete: true,
rules: [
...autoformatBlocks,
...autoformatMarks,
...autoformatSmartQuotes,
...autoformatPunctuation,
...autoformatLegal,
...autoformatLegalHtml,
...autoformatArrow,
...autoformatMath,
...autoformatLists,
].map(
(rule): AutoformatRule => ({
...rule,
query: (editor) =>
!editor.api.some({
match: { type: editor.getType(KEYS.codeBlock) },
}),
})
),
},
}),
];

View file

@ -1,86 +1,86 @@
'use client';
"use client";
import {
BlockquotePlugin,
H1Plugin,
H2Plugin,
H3Plugin,
H4Plugin,
H5Plugin,
H6Plugin,
HorizontalRulePlugin,
} from '@platejs/basic-nodes/react';
import { ParagraphPlugin } from 'platejs/react';
BlockquotePlugin,
H1Plugin,
H2Plugin,
H3Plugin,
H4Plugin,
H5Plugin,
H6Plugin,
HorizontalRulePlugin,
} from "@platejs/basic-nodes/react";
import { ParagraphPlugin } from "platejs/react";
import { BlockquoteElement } from '@/components/ui/blockquote-node';
import { BlockquoteElement } from "@/components/ui/blockquote-node";
import {
H1Element,
H2Element,
H3Element,
H4Element,
H5Element,
H6Element,
} from '@/components/ui/heading-node';
import { HrElement } from '@/components/ui/hr-node';
import { ParagraphElement } from '@/components/ui/paragraph-node';
H1Element,
H2Element,
H3Element,
H4Element,
H5Element,
H6Element,
} from "@/components/ui/heading-node";
import { HrElement } from "@/components/ui/hr-node";
import { ParagraphElement } from "@/components/ui/paragraph-node";
export const BasicBlocksKit = [
ParagraphPlugin.withComponent(ParagraphElement),
H1Plugin.configure({
node: {
component: H1Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+1' } },
}),
H2Plugin.configure({
node: {
component: H2Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+2' } },
}),
H3Plugin.configure({
node: {
component: H3Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+3' } },
}),
H4Plugin.configure({
node: {
component: H4Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+4' } },
}),
H5Plugin.configure({
node: {
component: H5Element,
},
rules: {
break: { empty: 'reset' },
},
}),
H6Plugin.configure({
node: {
component: H6Element,
},
rules: {
break: { empty: 'reset' },
},
}),
BlockquotePlugin.configure({
node: { component: BlockquoteElement },
shortcuts: { toggle: { keys: 'mod+shift+period' } },
}),
HorizontalRulePlugin.withComponent(HrElement),
ParagraphPlugin.withComponent(ParagraphElement),
H1Plugin.configure({
node: {
component: H1Element,
},
rules: {
break: { empty: "reset" },
},
shortcuts: { toggle: { keys: "mod+alt+1" } },
}),
H2Plugin.configure({
node: {
component: H2Element,
},
rules: {
break: { empty: "reset" },
},
shortcuts: { toggle: { keys: "mod+alt+2" } },
}),
H3Plugin.configure({
node: {
component: H3Element,
},
rules: {
break: { empty: "reset" },
},
shortcuts: { toggle: { keys: "mod+alt+3" } },
}),
H4Plugin.configure({
node: {
component: H4Element,
},
rules: {
break: { empty: "reset" },
},
shortcuts: { toggle: { keys: "mod+alt+4" } },
}),
H5Plugin.configure({
node: {
component: H5Element,
},
rules: {
break: { empty: "reset" },
},
}),
H6Plugin.configure({
node: {
component: H6Element,
},
rules: {
break: { empty: "reset" },
},
}),
BlockquotePlugin.configure({
node: { component: BlockquoteElement },
shortcuts: { toggle: { keys: "mod+shift+period" } },
}),
HorizontalRulePlugin.withComponent(HrElement),
];

View file

@ -1,38 +1,38 @@
'use client';
"use client";
import {
BoldPlugin,
CodePlugin,
HighlightPlugin,
ItalicPlugin,
StrikethroughPlugin,
SubscriptPlugin,
SuperscriptPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
BoldPlugin,
CodePlugin,
HighlightPlugin,
ItalicPlugin,
StrikethroughPlugin,
SubscriptPlugin,
SuperscriptPlugin,
UnderlinePlugin,
} from "@platejs/basic-nodes/react";
import { CodeLeaf } from '@/components/ui/code-node';
import { HighlightLeaf } from '@/components/ui/highlight-node';
import { CodeLeaf } from "@/components/ui/code-node";
import { HighlightLeaf } from "@/components/ui/highlight-node";
export const BasicMarksKit = [
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
CodePlugin.configure({
node: { component: CodeLeaf },
shortcuts: { toggle: { keys: 'mod+e' } },
}),
StrikethroughPlugin.configure({
shortcuts: { toggle: { keys: 'mod+shift+x' } },
}),
SubscriptPlugin.configure({
shortcuts: { toggle: { keys: 'mod+comma' } },
}),
SuperscriptPlugin.configure({
shortcuts: { toggle: { keys: 'mod+period' } },
}),
HighlightPlugin.configure({
node: { component: HighlightLeaf },
shortcuts: { toggle: { keys: 'mod+shift+h' } },
}),
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
CodePlugin.configure({
node: { component: CodeLeaf },
shortcuts: { toggle: { keys: "mod+e" } },
}),
StrikethroughPlugin.configure({
shortcuts: { toggle: { keys: "mod+shift+x" } },
}),
SubscriptPlugin.configure({
shortcuts: { toggle: { keys: "mod+comma" } },
}),
SuperscriptPlugin.configure({
shortcuts: { toggle: { keys: "mod+period" } },
}),
HighlightPlugin.configure({
node: { component: HighlightLeaf },
shortcuts: { toggle: { keys: "mod+shift+h" } },
}),
];

View file

@ -1,6 +1,6 @@
'use client';
"use client";
import { BasicBlocksKit } from './basic-blocks-kit';
import { BasicMarksKit } from './basic-marks-kit';
import { BasicBlocksKit } from "./basic-blocks-kit";
import { BasicMarksKit } from "./basic-marks-kit";
export const BasicNodesKit = [...BasicBlocksKit, ...BasicMarksKit];

View file

@ -1,8 +1,7 @@
'use client';
"use client";
import { CalloutPlugin } from '@platejs/callout/react';
import { CalloutPlugin } from "@platejs/callout/react";
import { CalloutElement } from '@/components/ui/callout-node';
import { CalloutElement } from "@/components/ui/callout-node";
export const CalloutKit = [CalloutPlugin.withComponent(CalloutElement)];

View file

@ -1,26 +1,18 @@
'use client';
"use client";
import {
CodeBlockPlugin,
CodeLinePlugin,
CodeSyntaxPlugin,
} from '@platejs/code-block/react';
import { all, createLowlight } from 'lowlight';
import { CodeBlockPlugin, CodeLinePlugin, CodeSyntaxPlugin } from "@platejs/code-block/react";
import { all, createLowlight } from "lowlight";
import {
CodeBlockElement,
CodeLineElement,
CodeSyntaxLeaf,
} from '@/components/ui/code-block-node';
import { CodeBlockElement, CodeLineElement, CodeSyntaxLeaf } from "@/components/ui/code-block-node";
const lowlight = createLowlight(all);
export const CodeBlockKit = [
CodeBlockPlugin.configure({
node: { component: CodeBlockElement },
options: { lowlight },
shortcuts: { toggle: { keys: 'mod+alt+8' } },
}),
CodeLinePlugin.withComponent(CodeLineElement),
CodeSyntaxPlugin.withComponent(CodeSyntaxLeaf),
CodeBlockPlugin.configure({
node: { component: CodeBlockElement },
options: { lowlight },
shortcuts: { toggle: { keys: "mod+alt+8" } },
}),
CodeLinePlugin.withComponent(CodeLineElement),
CodeSyntaxPlugin.withComponent(CodeSyntaxLeaf),
];

View file

@ -1,23 +1,20 @@
'use client';
"use client";
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { DndPlugin } from '@platejs/dnd';
import { DndPlugin } from "@platejs/dnd";
import { BlockDraggable } from '@/components/ui/block-draggable';
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>
),
},
}),
DndPlugin.configure({
options: {
enableScroller: true,
},
render: {
aboveNodes: BlockDraggable,
aboveSlate: ({ children }) => <DndProvider backend={HTML5Backend}>{children}</DndProvider>,
},
}),
];

View file

@ -1,20 +1,19 @@
'use client';
"use client";
import { createPlatePlugin } from 'platejs/react';
import { createPlatePlugin } from "platejs/react";
import { FixedToolbar } from '@/components/ui/fixed-toolbar';
import { FixedToolbarButtons } from '@/components/ui/fixed-toolbar-buttons';
import { FixedToolbar } from "@/components/ui/fixed-toolbar";
import { FixedToolbarButtons } from "@/components/ui/fixed-toolbar-buttons";
export const FixedToolbarKit = [
createPlatePlugin({
key: 'fixed-toolbar',
render: {
beforeEditable: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
}),
createPlatePlugin({
key: "fixed-toolbar",
render: {
beforeEditable: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
}),
];

View file

@ -1,19 +1,19 @@
'use client';
"use client";
import { createPlatePlugin } from 'platejs/react';
import { createPlatePlugin } from "platejs/react";
import { FloatingToolbar } from '@/components/ui/floating-toolbar';
import { FloatingToolbarButtons } from '@/components/ui/floating-toolbar-buttons';
import { FloatingToolbar } from "@/components/ui/floating-toolbar";
import { FloatingToolbarButtons } from "@/components/ui/floating-toolbar-buttons";
export const FloatingToolbarKit = [
createPlatePlugin({
key: 'floating-toolbar',
render: {
afterEditable: () => (
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
),
},
}),
createPlatePlugin({
key: "floating-toolbar",
render: {
afterEditable: () => (
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
),
},
}),
];

View file

@ -1,19 +1,12 @@
'use client';
"use client";
import { IndentPlugin } from '@platejs/indent/react';
import { KEYS } from 'platejs';
import { IndentPlugin } from "@platejs/indent/react";
import { KEYS } from "platejs";
export const IndentKit = [
IndentPlugin.configure({
inject: {
targetPlugins: [
...KEYS.heading,
KEYS.p,
KEYS.blockquote,
KEYS.codeBlock,
KEYS.toggle,
],
},
}),
IndentPlugin.configure({
inject: {
targetPlugins: [...KEYS.heading, KEYS.p, KEYS.blockquote, KEYS.codeBlock, KEYS.toggle],
},
}),
];

View file

@ -1,15 +1,15 @@
'use client';
"use client";
import { LinkPlugin } from '@platejs/link/react';
import { LinkPlugin } from "@platejs/link/react";
import { LinkElement } from '@/components/ui/link-node';
import { LinkFloatingToolbar } from '@/components/ui/link-toolbar';
import { LinkElement } from "@/components/ui/link-node";
import { LinkFloatingToolbar } from "@/components/ui/link-toolbar";
export const LinkKit = [
LinkPlugin.configure({
render: {
node: LinkElement,
afterEditable: () => <LinkFloatingToolbar />,
},
}),
LinkPlugin.configure({
render: {
node: LinkElement,
afterEditable: () => <LinkFloatingToolbar />,
},
}),
];

View file

@ -1,26 +1,19 @@
'use client';
"use client";
import { ListPlugin } from '@platejs/list/react';
import { KEYS } from 'platejs';
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';
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,
},
}),
...IndentKit,
ListPlugin.configure({
inject: {
targetPlugins: [...KEYS.heading, KEYS.p, KEYS.blockquote, KEYS.codeBlock, KEYS.toggle],
},
render: {
belowNodes: BlockList,
},
}),
];

View file

@ -1,11 +1,10 @@
'use client';
"use client";
import { EquationPlugin, InlineEquationPlugin } from '@platejs/math/react';
import { EquationPlugin, InlineEquationPlugin } from "@platejs/math/react";
import { EquationElement, InlineEquationElement } from '@/components/ui/equation-node';
import { EquationElement, InlineEquationElement } from "@/components/ui/equation-node";
export const MathKit = [
EquationPlugin.withComponent(EquationElement),
InlineEquationPlugin.withComponent(InlineEquationElement),
EquationPlugin.withComponent(EquationElement),
InlineEquationPlugin.withComponent(InlineEquationElement),
];

View file

@ -1,23 +1,23 @@
'use client';
"use client";
import { BlockSelectionPlugin } from '@platejs/selection/react';
import { BlockSelectionPlugin } from "@platejs/selection/react";
import { BlockSelection } from '@/components/ui/block-selection';
import { BlockSelection } from "@/components/ui/block-selection";
export const SelectionKit = [
BlockSelectionPlugin.configure({
render: {
belowRootNodes: BlockSelection as any,
},
options: {
isSelectable: (element) => {
// Exclude specific block types from selection
if (['code_line', 'td', 'th'].includes(element.type as string)) {
return false;
}
BlockSelectionPlugin.configure({
render: {
belowRootNodes: BlockSelection as any,
},
options: {
isSelectable: (element) => {
// Exclude specific block types from selection
if (["code_line", "td", "th"].includes(element.type as string)) {
return false;
}
return true;
},
},
}),
return true;
},
},
}),
];

View file

@ -1,21 +1,20 @@
'use client';
"use client";
import { SlashInputPlugin, SlashPlugin } from '@platejs/slash-command/react';
import { KEYS } from 'platejs';
import { SlashInputPlugin, SlashPlugin } from "@platejs/slash-command/react";
import { KEYS } from "platejs";
import { SlashInputElement } from '@/components/ui/slash-node';
import { SlashInputElement } from "@/components/ui/slash-node";
export const SlashCommandKit = [
SlashPlugin.configure({
options: {
trigger: '/',
triggerPreviousCharPattern: /^\s?$/,
triggerQuery: (editor) =>
!editor.api.some({
match: { type: editor.getType(KEYS.codeBlock) },
}),
},
}),
SlashInputPlugin.withComponent(SlashInputElement),
SlashPlugin.configure({
options: {
trigger: "/",
triggerPreviousCharPattern: /^\s?$/,
triggerQuery: (editor) =>
!editor.api.some({
match: { type: editor.getType(KEYS.codeBlock) },
}),
},
}),
SlashInputPlugin.withComponent(SlashInputElement),
];

View file

@ -1,22 +1,22 @@
'use client';
"use client";
import {
TableCellHeaderPlugin,
TableCellPlugin,
TablePlugin,
TableRowPlugin,
} from '@platejs/table/react';
TableCellHeaderPlugin,
TableCellPlugin,
TablePlugin,
TableRowPlugin,
} from "@platejs/table/react";
import {
TableCellElement,
TableCellHeaderElement,
TableElement,
TableRowElement,
} from '@/components/ui/table-node';
TableCellElement,
TableCellHeaderElement,
TableElement,
TableRowElement,
} from "@/components/ui/table-node";
export const TableKit = [
TablePlugin.withComponent(TableElement),
TableRowPlugin.withComponent(TableRowElement),
TableCellPlugin.withComponent(TableCellElement),
TableCellHeaderPlugin.withComponent(TableCellHeaderElement),
TablePlugin.withComponent(TableElement),
TableRowPlugin.withComponent(TableRowElement),
TableCellPlugin.withComponent(TableCellElement),
TableCellHeaderPlugin.withComponent(TableCellHeaderElement),
];

View file

@ -1,13 +1,12 @@
'use client';
"use client";
import { TogglePlugin } from '@platejs/toggle/react';
import { TogglePlugin } from "@platejs/toggle/react";
import { ToggleElement } from '@/components/ui/toggle-node';
import { ToggleElement } from "@/components/ui/toggle-node";
export const ToggleKit = [
TogglePlugin.configure({
node: { component: ToggleElement },
shortcuts: { toggle: { keys: 'mod+alt+9' } },
}),
TogglePlugin.configure({
node: { component: ToggleElement },
shortcuts: { toggle: { keys: "mod+alt+9" } },
}),
];

View file

@ -1,184 +1,160 @@
'use client';
"use client";
import type { PlateEditor } from 'platejs/react';
import type { PlateEditor } from "platejs/react";
import { insertCallout } from '@platejs/callout';
import { insertCodeBlock, toggleCodeBlock } from '@platejs/code-block';
import { triggerFloatingLink } from '@platejs/link/react';
import { insertInlineEquation } from '@platejs/math';
import { TablePlugin } from '@platejs/table/react';
import {
type NodeEntry,
type Path,
type TElement,
KEYS,
PathApi,
} from 'platejs';
import { insertCallout } from "@platejs/callout";
import { insertCodeBlock, toggleCodeBlock } from "@platejs/code-block";
import { triggerFloatingLink } from "@platejs/link/react";
import { insertInlineEquation } from "@platejs/math";
import { TablePlugin } from "@platejs/table/react";
import { type NodeEntry, type Path, type TElement, KEYS, PathApi } from "platejs";
const insertList = (editor: PlateEditor, type: string) => {
editor.tf.insertNodes(
editor.api.create.block({
indent: 1,
listStyleType: type,
}),
{ select: true }
);
editor.tf.insertNodes(
editor.api.create.block({
indent: 1,
listStyleType: type,
}),
{ select: true }
);
};
const insertBlockMap: Record<
string,
(editor: PlateEditor, type: string) => void
> = {
[KEYS.listTodo]: insertList,
[KEYS.ol]: insertList,
[KEYS.ul]: insertList,
[KEYS.codeBlock]: (editor) => insertCodeBlock(editor, { select: true }),
[KEYS.table]: (editor) =>
editor.getTransforms(TablePlugin).insert.table({}, { select: true }),
[KEYS.callout]: (editor) => insertCallout(editor, { select: true }),
[KEYS.toggle]: (editor) => {
editor.tf.insertNodes(
editor.api.create.block({ type: KEYS.toggle }),
{ select: true }
);
},
const insertBlockMap: Record<string, (editor: PlateEditor, type: string) => void> = {
[KEYS.listTodo]: insertList,
[KEYS.ol]: insertList,
[KEYS.ul]: insertList,
[KEYS.codeBlock]: (editor) => insertCodeBlock(editor, { select: true }),
[KEYS.table]: (editor) => editor.getTransforms(TablePlugin).insert.table({}, { select: true }),
[KEYS.callout]: (editor) => insertCallout(editor, { select: true }),
[KEYS.toggle]: (editor) => {
editor.tf.insertNodes(editor.api.create.block({ type: KEYS.toggle }), { select: true });
},
};
const insertInlineMap: Record<
string,
(editor: PlateEditor, type: string) => void
> = {
[KEYS.link]: (editor) => triggerFloatingLink(editor, { focused: true }),
[KEYS.equation]: (editor) => insertInlineEquation(editor),
const insertInlineMap: Record<string, (editor: PlateEditor, type: string) => void> = {
[KEYS.link]: (editor) => triggerFloatingLink(editor, { focused: true }),
[KEYS.equation]: (editor) => insertInlineEquation(editor),
};
type InsertBlockOptions = {
upsert?: boolean;
upsert?: boolean;
};
export const insertBlock = (
editor: PlateEditor,
type: string,
options: InsertBlockOptions = {}
editor: PlateEditor,
type: string,
options: InsertBlockOptions = {}
) => {
const { upsert = false } = options;
const { upsert = false } = options;
editor.tf.withoutNormalizing(() => {
const block = editor.api.block();
editor.tf.withoutNormalizing(() => {
const block = editor.api.block();
if (!block) return;
if (!block) return;
const [currentNode, path] = block;
const isCurrentBlockEmpty = editor.api.isEmpty(currentNode);
const currentBlockType = getBlockType(currentNode);
const [currentNode, path] = block;
const isCurrentBlockEmpty = editor.api.isEmpty(currentNode);
const currentBlockType = getBlockType(currentNode);
const isSameBlockType = type === currentBlockType;
const isSameBlockType = type === currentBlockType;
if (upsert && isCurrentBlockEmpty && isSameBlockType) {
return;
}
if (upsert && isCurrentBlockEmpty && isSameBlockType) {
return;
}
if (type in insertBlockMap) {
insertBlockMap[type](editor, type);
} else {
editor.tf.insertNodes(editor.api.create.block({ type }), {
at: PathApi.next(path),
select: true,
});
}
if (type in insertBlockMap) {
insertBlockMap[type](editor, type);
} else {
editor.tf.insertNodes(editor.api.create.block({ type }), {
at: PathApi.next(path),
select: true,
});
}
if (!isSameBlockType) {
editor.tf.removeNodes({ previousEmptyBlock: true });
}
});
if (!isSameBlockType) {
editor.tf.removeNodes({ previousEmptyBlock: true });
}
});
};
export const insertInlineElement = (editor: PlateEditor, type: string) => {
if (insertInlineMap[type]) {
insertInlineMap[type](editor, type);
}
if (insertInlineMap[type]) {
insertInlineMap[type](editor, type);
}
};
const setList = (
editor: PlateEditor,
type: string,
entry: NodeEntry<TElement>
) => {
editor.tf.setNodes(
editor.api.create.block({
indent: 1,
listStyleType: type,
}),
{
at: entry[1],
}
);
const setList = (editor: PlateEditor, type: string, entry: NodeEntry<TElement>) => {
editor.tf.setNodes(
editor.api.create.block({
indent: 1,
listStyleType: type,
}),
{
at: entry[1],
}
);
};
const setBlockMap: Record<
string,
(editor: PlateEditor, type: string, entry: NodeEntry<TElement>) => void
string,
(editor: PlateEditor, type: string, entry: NodeEntry<TElement>) => void
> = {
[KEYS.listTodo]: setList,
[KEYS.ol]: setList,
[KEYS.ul]: setList,
[KEYS.codeBlock]: (editor) => toggleCodeBlock(editor),
[KEYS.callout]: (editor, _type, entry) => {
editor.tf.setNodes({ type: KEYS.callout }, { at: entry[1] });
},
[KEYS.toggle]: (editor, _type, entry) => {
editor.tf.setNodes({ type: KEYS.toggle }, { at: entry[1] });
},
[KEYS.listTodo]: setList,
[KEYS.ol]: setList,
[KEYS.ul]: setList,
[KEYS.codeBlock]: (editor) => toggleCodeBlock(editor),
[KEYS.callout]: (editor, _type, entry) => {
editor.tf.setNodes({ type: KEYS.callout }, { at: entry[1] });
},
[KEYS.toggle]: (editor, _type, entry) => {
editor.tf.setNodes({ type: KEYS.toggle }, { at: entry[1] });
},
};
export const setBlockType = (
editor: PlateEditor,
type: string,
{ at }: { at?: Path } = {}
) => {
editor.tf.withoutNormalizing(() => {
const setEntry = (entry: NodeEntry<TElement>) => {
const [node, path] = entry;
export const setBlockType = (editor: PlateEditor, type: string, { at }: { at?: Path } = {}) => {
editor.tf.withoutNormalizing(() => {
const setEntry = (entry: NodeEntry<TElement>) => {
const [node, path] = entry;
if (node[KEYS.listType]) {
editor.tf.unsetNodes([KEYS.listType, 'indent'], { at: path });
}
if (type in setBlockMap) {
return setBlockMap[type](editor, type, entry);
}
if (node.type !== type) {
editor.tf.setNodes({ type }, { at: path });
}
};
if (node[KEYS.listType]) {
editor.tf.unsetNodes([KEYS.listType, "indent"], { at: path });
}
if (type in setBlockMap) {
return setBlockMap[type](editor, type, entry);
}
if (node.type !== type) {
editor.tf.setNodes({ type }, { at: path });
}
};
if (at) {
const entry = editor.api.node<TElement>(at);
if (at) {
const entry = editor.api.node<TElement>(at);
if (entry) {
setEntry(entry);
if (entry) {
setEntry(entry);
return;
}
}
return;
}
}
const entries = editor.api.blocks({ mode: 'lowest' });
const entries = editor.api.blocks({ mode: "lowest" });
entries.forEach((entry) => {
setEntry(entry);
});
});
entries.forEach((entry) => {
setEntry(entry);
});
});
};
export const getBlockType = (block: TElement) => {
if (block[KEYS.listType]) {
if (block[KEYS.listType] === KEYS.ol) {
return KEYS.ol;
}
if (block[KEYS.listType] === KEYS.listTodo) {
return KEYS.listTodo;
}
return KEYS.ul;
}
if (block[KEYS.listType]) {
if (block[KEYS.listType] === KEYS.ol) {
return KEYS.ol;
}
if (block[KEYS.listType] === KEYS.listTodo) {
return KEYS.listTodo;
}
return KEYS.ul;
}
return block.type;
return block.type;
};

View file

@ -12,15 +12,14 @@
const FENCED_OR_INLINE_CODE = /(```[\s\S]*?```|`[^`\n]+`)/g;
export function escapeMdxExpressions(md: string): string {
const parts = md.split(FENCED_OR_INLINE_CODE);
const parts = md.split(FENCED_OR_INLINE_CODE);
return parts
.map((part, i) => {
// Odd indices are code blocks / inline code leave untouched
if (i % 2 === 1) return part;
// Escape { and } that are NOT already escaped (no preceding \)
return part.replace(/(?<!\\)\{/g, '\\{').replace(/(?<!\\)\}/g, '\\}');
})
.join('');
return parts
.map((part, i) => {
// Odd indices are code blocks / inline code leave untouched
if (i % 2 === 1) return part;
// Escape { and } that are NOT already escaped (no preceding \)
return part.replace(/(?<!\\)\{/g, "\\{").replace(/(?<!\\)\}/g, "\\}");
})
.join("");
}

View file

@ -302,7 +302,7 @@ function ReportPanelContent({
{/* Action bar — always visible after initial load */}
<div className="flex items-center justify-between px-4 py-2 shrink-0">
<div className="flex items-center gap-2">
{/* Copy button */}
{/* Copy button */}
<Button
variant="outline"
size="sm"
@ -426,16 +426,16 @@ function ReportPanelContent({
</div>
</div>
) : reportContent.content ? (
<PlateEditor
markdown={reportContent.content}
onMarkdownChange={shareToken ? undefined : setEditedMarkdown}
readOnly={!!shareToken}
placeholder="Report content..."
editorVariant="default"
onSave={shareToken ? undefined : handleSave}
hasUnsavedChanges={!shareToken && editedMarkdown !== null}
isSaving={saving}
/>
<PlateEditor
markdown={reportContent.content}
onMarkdownChange={shareToken ? undefined : setEditedMarkdown}
readOnly={!!shareToken}
placeholder="Report content..."
editorVariant="default"
onSave={shareToken ? undefined : handleSave}
hasUnsavedChanges={!shareToken && editedMarkdown !== null}
isSaving={saving}
/>
) : (
<div className="px-5 py-5">
<p className="text-muted-foreground italic">No content available.</p>
@ -499,7 +499,10 @@ function MobileReportDrawer() {
}}
shouldScaleBackground={false}
>
<DrawerContent className="h-[90vh] max-h-[90vh] z-80 !rounded-none border-none" overlayClassName="z-80">
<DrawerContent
className="h-[90vh] max-h-[90vh] z-80 !rounded-none border-none"
overlayClassName="z-80"
>
<DrawerHandle />
<DrawerTitle className="sr-only">{panelState.title || "Report"}</DrawerTitle>
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">

View file

@ -1,513 +1,467 @@
'use client';
"use client";
import * as React from 'react';
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 { 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';
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';
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 { editor, element, path } = props;
const enabled = React.useMemo(() => {
if (editor.dom.readOnly) return false;
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 (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;
}
}
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;
}
}
if (block) {
return true;
}
}
return false;
}, [editor, element, path]);
return false;
}, [editor, element, path]);
if (!enabled) return;
if (!enabled) return;
return (props) => <Draggable {...props} />;
return (props) => <Draggable {...props} />;
};
function Draggable(props: PlateElementProps) {
const { children, editor, element, path } = props;
const blockSelectionApi = editor.getApi(BlockSelectionPlugin).blockSelection;
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;
const { isAboutToDrag, isDragging, nodeRef, previewRef, handleRef } = useDraggable({
element,
onDropHandler: (_, { dragItem }) => {
const id = (dragItem as { id: string[] | string }).id;
if (blockSelectionApi) {
blockSelectionApi.add(id);
}
resetPreview();
},
});
if (blockSelectionApi) {
blockSelectionApi.add(id);
}
resetPreview();
},
});
const isInColumn = path.length === 3;
const isInTable = path.length === 4;
const isInColumn = path.length === 3;
const isInTable = path.length === 4;
const [previewTop, setPreviewTop] = React.useState(0);
const [previewTop, setPreviewTop] = React.useState(0);
const resetPreview = () => {
if (previewRef.current) {
previewRef.current.replaceChildren();
previewRef.current?.classList.add('hidden');
}
};
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]);
// 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]);
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);
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>
)}
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={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>
);
<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();
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>
);
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,
previewRef,
resetPreview,
setPreviewTop,
}: {
isDragging: boolean;
previewRef: React.RefObject<HTMLDivElement | null>;
resetPreview: () => void;
setPreviewTop: (top: number) => void;
isDragging: boolean;
previewRef: React.RefObject<HTMLDivElement | null>;
resetPreview: () => void;
setPreviewTop: (top: number) => void;
}) {
const editor = useEditorRef();
const element = useElement();
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();
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;
if ((e.button !== 0 && e.button !== 2) || e.shiftKey) return;
const blockSelection = editor
.getApi(BlockSelectionPlugin)
.blockSelection.getNodes({ sort: true });
const blockSelection = editor
.getApi(BlockSelectionPlugin)
.blockSelection.getNodes({ sort: true });
let selectionNodes =
blockSelection.length > 0
? blockSelection
: editor.api.blocks({ mode: 'highest' });
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)!]];
}
// 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);
// 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();
}
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);
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;
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 });
const blockSelection = editor
.getApi(BlockSelectionPlugin)
.blockSelection.getNodes({ sort: true });
let selectedBlocks =
blockSelection.length > 0
? blockSelection
: editor.api.blocks({ mode: 'highest' });
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)!]];
}
// 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
);
// Process selection to include list children
const processedBlocks = expandListItemsWithChildren(editor, selectedBlocks);
const ids = processedBlocks.map((block) => block[0].id as string);
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>
);
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();
className,
...props
}: React.ComponentProps<"div">) {
const { dropLine } = useDropLine();
if (!dropLine) return null;
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
)}
/>
);
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[] = [];
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);
}
});
/**
* 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);
});
};
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;
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;
// 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`;
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`;
// 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);
}
// 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;
// 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);
}
};
scrollWrapper.append(innerContainer);
cloned.append(scrollWrapper);
}
};
applyScrollCompensation(domNode, newDomNode);
applyScrollCompensation(domNode, newDomNode);
ids.push(node.id as string);
const wrapper = document.createElement('div');
wrapper.append(newDomNode);
wrapper.style.display = 'flow-root';
ids.push(node.id as string);
const wrapper = document.createElement("div");
wrapper.append(newDomNode);
wrapper.style.display = "flow-root";
const lastDomNode = blocks[index - 1];
const lastDomNode = blocks[index - 1];
if (lastDomNode) {
const lastDomNodeRect = editor.api
.toDOMNode(lastDomNode)!
.parentElement!.getBoundingClientRect();
if (lastDomNode) {
const lastDomNodeRect = editor.api
.toDOMNode(lastDomNode)!
.parentElement!.getBoundingClientRect();
const domNodeRect = domNode.parentElement!.getBoundingClientRect();
const domNodeRect = domNode.parentElement!.getBoundingClientRect();
const distance = domNodeRect.top - lastDomNodeRect.bottom;
const distance = domNodeRect.top - lastDomNodeRect.bottom;
// Check if the two elements are adjacent (touching each other)
if (distance > 15) {
wrapper.style.marginTop = `${distance}px`;
}
}
// Check if the two elements are adjacent (touching each other)
if (distance > 15) {
wrapper.style.marginTop = `${distance}px`;
}
}
removeDataAttributes(newDomNode);
elements.push(wrapper);
};
removeDataAttributes(newDomNode);
elements.push(wrapper);
};
blocks.forEach((node, index) => {
resolveElement(node, index);
});
blocks.forEach((node, index) => {
resolveElement(node, index);
});
editor.setOption(DndPlugin, 'draggingId', ids);
editor.setOption(DndPlugin, "draggingId", ids);
return elements;
return elements;
};
const calculatePreviewTop = (
editor: PlateEditor,
{
blocks,
element,
}: {
blocks: TElement[];
element: TElement;
}
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 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', '')
);
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;
// 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', ''));
// 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;
// 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 currentMarginTopString = window.getComputedStyle(child).marginTop;
const currentMarginTop = Number(currentMarginTopString.replace("px", ""));
const previewElementsTopDistance =
currentToEditorDistance -
firstNodeToEditorDistance +
marginTop -
currentMarginTop;
const previewElementsTopDistance =
currentToEditorDistance - firstNodeToEditorDistance + marginTop - currentMarginTop;
return previewElementsTopDistance;
return previewElementsTopDistance;
};
const calcDragButtonTop = (editor: PlateEditor, element: TElement): number => {
const child = editor.api.toDOMNode(element)!;
const child = editor.api.toDOMNode(element)!;
const currentMarginTopString = window.getComputedStyle(child).marginTop;
const currentMarginTop = Number(currentMarginTopString.replace('px', ''));
const currentMarginTopString = window.getComputedStyle(child).marginTop;
const currentMarginTop = Number(currentMarginTopString.replace("px", ""));
return currentMarginTop;
return currentMarginTop;
};

View file

@ -1,87 +1,72 @@
'use client';
"use client";
import React from 'react';
import React from "react";
import type { TListElement } from 'platejs';
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 { 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';
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
const config: Record<
string,
{
Li: React.FC<PlateElementProps>;
Marker: React.FC<PlateElementProps>;
}
string,
{
Li: React.FC<PlateElementProps>;
Marker: React.FC<PlateElementProps>;
}
> = {
todo: {
Li: TodoLi,
Marker: TodoMarker,
},
todo: {
Li: TodoLi,
Marker: TodoMarker,
},
};
export const BlockList: RenderNodeWrapper = (props) => {
if (!props.element.listStyleType) return;
if (!props.element.listStyleType) return;
return (props) => <List {...props} />;
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';
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>
);
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();
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>
);
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>
);
return (
<li
className={cn(
"list-none",
(props.element.checked as boolean) && "text-muted-foreground line-through"
)}
>
{props.children}
</li>
);
}

View file

@ -1,44 +1,39 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import { DndPlugin } from '@platejs/dnd';
import { useBlockSelected } from '@platejs/selection/react';
import { cva } from 'class-variance-authority';
import { type PlateElementProps, usePluginOption } from 'platejs/react';
import { DndPlugin } from "@platejs/dnd";
import { useBlockSelected } from "@platejs/selection/react";
import { cva } from "class-variance-authority";
import { type PlateElementProps, usePluginOption } from "platejs/react";
export const blockSelectionVariants = cva(
'pointer-events-none absolute inset-0 z-1 bg-brand/[.13] transition-opacity',
{
defaultVariants: {
active: true,
},
variants: {
active: {
false: 'opacity-0',
true: 'opacity-100',
},
},
}
"pointer-events-none absolute inset-0 z-1 bg-brand/[.13] transition-opacity",
{
defaultVariants: {
active: true,
},
variants: {
active: {
false: "opacity-0",
true: "opacity-100",
},
},
}
);
export function BlockSelection(props: PlateElementProps) {
const isBlockSelected = useBlockSelected();
const isDragging = usePluginOption(DndPlugin, 'isDragging');
const isBlockSelected = useBlockSelected();
const isDragging = usePluginOption(DndPlugin, "isDragging");
if (
!isBlockSelected ||
props.plugin.key === 'tr' ||
props.plugin.key === 'table'
)
return null;
if (!isBlockSelected || props.plugin.key === "tr" || props.plugin.key === "table") return null;
return (
<div
className={blockSelectionVariants({
active: isBlockSelected && !isDragging,
})}
data-slot="block-selection"
/>
);
return (
<div
className={blockSelectionVariants({
active: isBlockSelected && !isDragging,
})}
data-slot="block-selection"
/>
);
}

View file

@ -1,13 +1,7 @@
'use client';
"use client";
import { type PlateElementProps, PlateElement } from 'platejs/react';
import { type PlateElementProps, PlateElement } from "platejs/react";
export function BlockquoteElement(props: PlateElementProps) {
return (
<PlateElement
as="blockquote"
className="my-1 border-l-2 pl-6 italic"
{...props}
/>
);
return <PlateElement as="blockquote" className="my-1 border-l-2 pl-6 italic" {...props} />;
}

View file

@ -1,83 +1,77 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { TCalloutElement } from 'platejs';
import type { TCalloutElement } from "platejs";
import { CalloutPlugin } from '@platejs/callout/react';
import { cva } from 'class-variance-authority';
import { type PlateElementProps, PlateElement, useEditorPlugin } from 'platejs/react';
import { CalloutPlugin } from "@platejs/callout/react";
import { cva } from "class-variance-authority";
import { type PlateElementProps, PlateElement, useEditorPlugin } from "platejs/react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
const calloutVariants = cva(
'my-1 flex w-full items-start gap-2 rounded-lg border p-4',
{
defaultVariants: {
variant: 'info',
},
variants: {
variant: {
info: 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/50',
warning: 'border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950/50',
error: 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/50',
success: 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/50',
note: 'border-muted bg-muted/50',
tip: 'border-purple-200 bg-purple-50 dark:border-purple-800 dark:bg-purple-950/50',
},
},
}
);
const calloutVariants = cva("my-1 flex w-full items-start gap-2 rounded-lg border p-4", {
defaultVariants: {
variant: "info",
},
variants: {
variant: {
info: "border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/50",
warning: "border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950/50",
error: "border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/50",
success: "border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/50",
note: "border-muted bg-muted/50",
tip: "border-purple-200 bg-purple-50 dark:border-purple-800 dark:bg-purple-950/50",
},
},
});
const calloutIcons: Record<string, string> = {
info: '💡',
warning: '⚠️',
error: '🚨',
success: '✅',
note: '📝',
tip: '💜',
info: "💡",
warning: "⚠️",
error: "🚨",
success: "✅",
note: "📝",
tip: "💜",
};
const variantCycle = ['info', 'warning', 'error', 'success', 'note', 'tip'] as const;
const variantCycle = ["info", "warning", "error", "success", "note", "tip"] as const;
export function CalloutElement({
children,
...props
}: PlateElementProps<TCalloutElement>) {
const { editor } = useEditorPlugin(CalloutPlugin);
const element = props.element;
const variant = element.variant || 'info';
const icon = element.icon || calloutIcons[variant] || '💡';
export function CalloutElement({ children, ...props }: PlateElementProps<TCalloutElement>) {
const { editor } = useEditorPlugin(CalloutPlugin);
const element = props.element;
const variant = element.variant || "info";
const icon = element.icon || calloutIcons[variant] || "💡";
const cycleVariant = React.useCallback(() => {
const currentIndex = variantCycle.indexOf(variant as (typeof variantCycle)[number]);
const nextIndex = (currentIndex + 1) % variantCycle.length;
const nextVariant = variantCycle[nextIndex];
const cycleVariant = React.useCallback(() => {
const currentIndex = variantCycle.indexOf(variant as (typeof variantCycle)[number]);
const nextIndex = (currentIndex + 1) % variantCycle.length;
const nextVariant = variantCycle[nextIndex];
editor.tf.setNodes(
{
variant: nextVariant,
icon: calloutIcons[nextVariant],
},
{ at: props.path }
);
}, [editor, variant, props.path]);
editor.tf.setNodes(
{
variant: nextVariant,
icon: calloutIcons[nextVariant],
},
{ at: props.path }
);
}, [editor, variant, props.path]);
return (
<PlateElement
{...props}
className={cn(calloutVariants({ variant: variant as any }), props.className)}
>
<button
className="mt-0.5 shrink-0 cursor-pointer select-none text-lg leading-none"
contentEditable={false}
onClick={cycleVariant}
type="button"
aria-label="Change callout type"
>
{icon}
</button>
<div className="min-w-0 flex-1">{children}</div>
</PlateElement>
);
return (
<PlateElement
{...props}
className={cn(calloutVariants({ variant: variant as any }), props.className)}
>
<button
className="mt-0.5 shrink-0 cursor-pointer select-none text-lg leading-none"
contentEditable={false}
onClick={cycleVariant}
type="button"
aria-label="Change callout type"
>
{icon}
</button>
<div className="min-w-0 flex-1">{children}</div>
</PlateElement>
);
}

View file

@ -1,32 +1,29 @@
"use client"
"use client";
import * as React from "react"
import { CheckIcon } from "lucide-react"
import { Checkbox as CheckboxPrimitive } from "radix-ui"
import * as React from "react";
import { CheckIcon } from "lucide-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>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"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
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"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
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox }
export { Checkbox };

View file

@ -1,289 +1,264 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import { formatCodeBlock, isLangSupported } from '@platejs/code-block';
import { BracesIcon, Check, CheckIcon, CopyIcon } from 'lucide-react';
import { type TCodeBlockElement, type TCodeSyntaxLeaf, NodeApi } from 'platejs';
import { formatCodeBlock, isLangSupported } from "@platejs/code-block";
import { BracesIcon, Check, CheckIcon, CopyIcon } from "lucide-react";
import { type TCodeBlockElement, type TCodeSyntaxLeaf, NodeApi } from "platejs";
import {
type PlateElementProps,
type PlateLeafProps,
PlateElement,
PlateLeaf,
} from 'platejs/react';
import { useEditorRef, useElement, useReadOnly } from 'platejs/react';
type PlateElementProps,
type PlateLeafProps,
PlateElement,
PlateLeaf,
} from "platejs/react";
import { useEditorRef, useElement, useReadOnly } from "platejs/react";
import { Button } from '@/components/ui/button';
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
const { editor, element } = props;
const { editor, element } = props;
return (
<PlateElement
className="py-1 **:[.hljs-addition]:bg-[#f0fff4] **:[.hljs-addition]:text-[#22863a] dark:**:[.hljs-addition]:bg-[#3c5743] dark:**:[.hljs-addition]:text-[#ceead5] **:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#005cc5] dark:**:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#6596cf] **:[.hljs-built\\\\_in,.hljs-symbol]:text-[#e36209] dark:**:[.hljs-built\\\\_in,.hljs-symbol]:text-[#c3854e] **:[.hljs-bullet]:text-[#735c0f] **:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] dark:**:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] **:[.hljs-deletion]:bg-[#ffeef0] **:[.hljs-deletion]:text-[#b31d28] dark:**:[.hljs-deletion]:bg-[#473235] dark:**:[.hljs-deletion]:text-[#e7c7cb] **:[.hljs-emphasis]:italic **:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#d73a49] dark:**:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#ee6960] **:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#22863a] dark:**:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#36a84f] **:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#032f62] dark:**:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#3593ff] **:[.hljs-section]:font-bold **:[.hljs-section]:text-[#005cc5] dark:**:[.hljs-section]:text-[#61a5f2] **:[.hljs-strong]:font-bold **:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#6f42c1] dark:**:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#a77bfa]"
{...props}
>
<div className="relative rounded-md bg-muted/50">
<pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
<code>{props.children}</code>
</pre>
return (
<PlateElement
className="py-1 **:[.hljs-addition]:bg-[#f0fff4] **:[.hljs-addition]:text-[#22863a] dark:**:[.hljs-addition]:bg-[#3c5743] dark:**:[.hljs-addition]:text-[#ceead5] **:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#005cc5] dark:**:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#6596cf] **:[.hljs-built\\\\_in,.hljs-symbol]:text-[#e36209] dark:**:[.hljs-built\\\\_in,.hljs-symbol]:text-[#c3854e] **:[.hljs-bullet]:text-[#735c0f] **:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] dark:**:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] **:[.hljs-deletion]:bg-[#ffeef0] **:[.hljs-deletion]:text-[#b31d28] dark:**:[.hljs-deletion]:bg-[#473235] dark:**:[.hljs-deletion]:text-[#e7c7cb] **:[.hljs-emphasis]:italic **:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#d73a49] dark:**:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#ee6960] **:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#22863a] dark:**:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#36a84f] **:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#032f62] dark:**:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#3593ff] **:[.hljs-section]:font-bold **:[.hljs-section]:text-[#005cc5] dark:**:[.hljs-section]:text-[#61a5f2] **:[.hljs-strong]:font-bold **:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#6f42c1] dark:**:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#a77bfa]"
{...props}
>
<div className="relative rounded-md bg-muted/50">
<pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
<code>{props.children}</code>
</pre>
<div
className="absolute top-1 right-1 z-10 flex select-none gap-0.5"
contentEditable={false}
>
{isLangSupported(element.lang) && (
<Button
size="icon"
variant="ghost"
className="size-6 text-xs"
onClick={() => formatCodeBlock(editor, { element })}
title="Format code"
>
<BracesIcon className="!size-3.5 text-muted-foreground" />
</Button>
)}
<div
className="absolute top-1 right-1 z-10 flex select-none gap-0.5"
contentEditable={false}
>
{isLangSupported(element.lang) && (
<Button
size="icon"
variant="ghost"
className="size-6 text-xs"
onClick={() => formatCodeBlock(editor, { element })}
title="Format code"
>
<BracesIcon className="!size-3.5 text-muted-foreground" />
</Button>
)}
<CodeBlockCombobox />
<CodeBlockCombobox />
<CopyButton
size="icon"
variant="ghost"
className="size-6 gap-1 text-muted-foreground text-xs"
value={() => NodeApi.string(element)}
/>
</div>
</div>
</PlateElement>
);
<CopyButton
size="icon"
variant="ghost"
className="size-6 gap-1 text-muted-foreground text-xs"
value={() => NodeApi.string(element)}
/>
</div>
</div>
</PlateElement>
);
}
function CodeBlockCombobox() {
const [open, setOpen] = React.useState(false);
const readOnly = useReadOnly();
const editor = useEditorRef();
const element = useElement<TCodeBlockElement>();
const value = element.lang || 'plaintext';
const [searchValue, setSearchValue] = React.useState('');
const [open, setOpen] = React.useState(false);
const readOnly = useReadOnly();
const editor = useEditorRef();
const element = useElement<TCodeBlockElement>();
const value = element.lang || "plaintext";
const [searchValue, setSearchValue] = React.useState("");
const items = React.useMemo(
() =>
languages.filter(
(language) =>
!searchValue ||
language.label.toLowerCase().includes(searchValue.toLowerCase())
),
[searchValue]
);
const items = React.useMemo(
() =>
languages.filter(
(language) =>
!searchValue || language.label.toLowerCase().includes(searchValue.toLowerCase())
),
[searchValue]
);
if (readOnly) return null;
if (readOnly) return null;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
size="sm"
variant="ghost"
className="h-6 select-none justify-between gap-1 px-2 text-muted-foreground text-xs"
aria-expanded={open}
role="combobox"
>
{languages.find((language) => language.value === value)?.label ??
'Plain Text'}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[200px] p-0"
onCloseAutoFocus={() => setSearchValue('')}
>
<Command shouldFilter={false}>
<CommandInput
className="h-9"
value={searchValue}
onValueChange={(value) => setSearchValue(value)}
placeholder="Search language..."
/>
<CommandEmpty>No language found.</CommandEmpty>
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
size="sm"
variant="ghost"
className="h-6 select-none justify-between gap-1 px-2 text-muted-foreground text-xs"
aria-expanded={open}
role="combobox"
>
{languages.find((language) => language.value === value)?.label ?? "Plain Text"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" onCloseAutoFocus={() => setSearchValue("")}>
<Command shouldFilter={false}>
<CommandInput
className="h-9"
value={searchValue}
onValueChange={(value) => setSearchValue(value)}
placeholder="Search language..."
/>
<CommandEmpty>No language found.</CommandEmpty>
<CommandList className="h-[344px] overflow-y-auto">
<CommandGroup>
{items.map((language) => (
<CommandItem
key={language.label}
className="cursor-pointer"
value={language.value}
onSelect={(value) => {
editor.tf.setNodes<TCodeBlockElement>(
{ lang: value },
{ at: element }
);
setSearchValue(value);
setOpen(false);
}}
>
<Check
className={cn(
value === language.value ? 'opacity-100' : 'opacity-0'
)}
/>
{language.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
<CommandList className="h-[344px] overflow-y-auto">
<CommandGroup>
{items.map((language) => (
<CommandItem
key={language.label}
className="cursor-pointer"
value={language.value}
onSelect={(value) => {
editor.tf.setNodes<TCodeBlockElement>({ lang: value }, { at: element });
setSearchValue(value);
setOpen(false);
}}
>
<Check className={cn(value === language.value ? "opacity-100" : "opacity-0")} />
{language.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
function CopyButton({
value,
...props
}: { value: (() => string) | string } & Omit<
React.ComponentProps<typeof Button>,
'value'
>) {
const [hasCopied, setHasCopied] = React.useState(false);
value,
...props
}: { value: (() => string) | string } & Omit<React.ComponentProps<typeof Button>, "value">) {
const [hasCopied, setHasCopied] = React.useState(false);
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}, [hasCopied]);
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}, [hasCopied]);
return (
<Button
onClick={() => {
void navigator.clipboard.writeText(
typeof value === 'function' ? value() : value
);
setHasCopied(true);
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? (
<CheckIcon className="!size-3" />
) : (
<CopyIcon className="!size-3" />
)}
</Button>
);
return (
<Button
onClick={() => {
void navigator.clipboard.writeText(typeof value === "function" ? value() : value);
setHasCopied(true);
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon className="!size-3" /> : <CopyIcon className="!size-3" />}
</Button>
);
}
export function CodeLineElement(props: PlateElementProps) {
return <PlateElement {...props} />;
return <PlateElement {...props} />;
}
export function CodeSyntaxLeaf(props: PlateLeafProps<TCodeSyntaxLeaf>) {
const tokenClassName = props.leaf.className as string;
const tokenClassName = props.leaf.className as string;
return <PlateLeaf className={tokenClassName} {...props} />;
return <PlateLeaf className={tokenClassName} {...props} />;
}
const languages: { label: string; value: string }[] = [
{ label: 'Auto', value: 'auto' },
{ label: 'Plain Text', value: 'plaintext' },
{ label: 'ABAP', value: 'abap' },
{ label: 'Agda', value: 'agda' },
{ label: 'Arduino', value: 'arduino' },
{ label: 'ASCII Art', value: 'ascii' },
{ label: 'Assembly', value: 'x86asm' },
{ label: 'Bash', value: 'bash' },
{ label: 'BASIC', value: 'basic' },
{ label: 'BNF', value: 'bnf' },
{ label: 'C', value: 'c' },
{ label: 'C#', value: 'csharp' },
{ label: 'C++', value: 'cpp' },
{ label: 'Clojure', value: 'clojure' },
{ label: 'CoffeeScript', value: 'coffeescript' },
{ label: 'Coq', value: 'coq' },
{ label: 'CSS', value: 'css' },
{ label: 'Dart', value: 'dart' },
{ label: 'Dhall', value: 'dhall' },
{ label: 'Diff', value: 'diff' },
{ label: 'Docker', value: 'dockerfile' },
{ label: 'EBNF', value: 'ebnf' },
{ label: 'Elixir', value: 'elixir' },
{ label: 'Elm', value: 'elm' },
{ label: 'Erlang', value: 'erlang' },
{ label: 'F#', value: 'fsharp' },
{ label: 'Flow', value: 'flow' },
{ label: 'Fortran', value: 'fortran' },
{ label: 'Gherkin', value: 'gherkin' },
{ label: 'GLSL', value: 'glsl' },
{ label: 'Go', value: 'go' },
{ label: 'GraphQL', value: 'graphql' },
{ label: 'Groovy', value: 'groovy' },
{ label: 'Haskell', value: 'haskell' },
{ label: 'HCL', value: 'hcl' },
{ label: 'HTML', value: 'html' },
{ label: 'Idris', value: 'idris' },
{ label: 'Java', value: 'java' },
{ label: 'JavaScript', value: 'javascript' },
{ label: 'JSON', value: 'json' },
{ label: 'Julia', value: 'julia' },
{ label: 'Kotlin', value: 'kotlin' },
{ label: 'LaTeX', value: 'latex' },
{ label: 'Less', value: 'less' },
{ label: 'Lisp', value: 'lisp' },
{ label: 'LiveScript', value: 'livescript' },
{ label: 'LLVM IR', value: 'llvm' },
{ label: 'Lua', value: 'lua' },
{ label: 'Makefile', value: 'makefile' },
{ label: 'Markdown', value: 'markdown' },
{ label: 'Markup', value: 'markup' },
{ label: 'MATLAB', value: 'matlab' },
{ label: 'Mathematica', value: 'mathematica' },
{ label: 'Mermaid', value: 'mermaid' },
{ label: 'Nix', value: 'nix' },
{ label: 'Notion Formula', value: 'notion' },
{ label: 'Objective-C', value: 'objectivec' },
{ label: 'OCaml', value: 'ocaml' },
{ label: 'Pascal', value: 'pascal' },
{ label: 'Perl', value: 'perl' },
{ label: 'PHP', value: 'php' },
{ label: 'PowerShell', value: 'powershell' },
{ label: 'Prolog', value: 'prolog' },
{ label: 'Protocol Buffers', value: 'protobuf' },
{ label: 'PureScript', value: 'purescript' },
{ label: 'Python', value: 'python' },
{ label: 'R', value: 'r' },
{ label: 'Racket', value: 'racket' },
{ label: 'Reason', value: 'reasonml' },
{ label: 'Ruby', value: 'ruby' },
{ label: 'Rust', value: 'rust' },
{ label: 'Sass', value: 'scss' },
{ label: 'Scala', value: 'scala' },
{ label: 'Scheme', value: 'scheme' },
{ label: 'SCSS', value: 'scss' },
{ label: 'Shell', value: 'shell' },
{ label: 'Smalltalk', value: 'smalltalk' },
{ label: 'Solidity', value: 'solidity' },
{ label: 'SQL', value: 'sql' },
{ label: 'Swift', value: 'swift' },
{ label: 'TOML', value: 'toml' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'VB.Net', value: 'vbnet' },
{ label: 'Verilog', value: 'verilog' },
{ label: 'VHDL', value: 'vhdl' },
{ label: 'Visual Basic', value: 'vbnet' },
{ label: 'WebAssembly', value: 'wasm' },
{ label: 'XML', value: 'xml' },
{ label: 'YAML', value: 'yaml' },
{ label: "Auto", value: "auto" },
{ label: "Plain Text", value: "plaintext" },
{ label: "ABAP", value: "abap" },
{ label: "Agda", value: "agda" },
{ label: "Arduino", value: "arduino" },
{ label: "ASCII Art", value: "ascii" },
{ label: "Assembly", value: "x86asm" },
{ label: "Bash", value: "bash" },
{ label: "BASIC", value: "basic" },
{ label: "BNF", value: "bnf" },
{ label: "C", value: "c" },
{ label: "C#", value: "csharp" },
{ label: "C++", value: "cpp" },
{ label: "Clojure", value: "clojure" },
{ label: "CoffeeScript", value: "coffeescript" },
{ label: "Coq", value: "coq" },
{ label: "CSS", value: "css" },
{ label: "Dart", value: "dart" },
{ label: "Dhall", value: "dhall" },
{ label: "Diff", value: "diff" },
{ label: "Docker", value: "dockerfile" },
{ label: "EBNF", value: "ebnf" },
{ label: "Elixir", value: "elixir" },
{ label: "Elm", value: "elm" },
{ label: "Erlang", value: "erlang" },
{ label: "F#", value: "fsharp" },
{ label: "Flow", value: "flow" },
{ label: "Fortran", value: "fortran" },
{ label: "Gherkin", value: "gherkin" },
{ label: "GLSL", value: "glsl" },
{ label: "Go", value: "go" },
{ label: "GraphQL", value: "graphql" },
{ label: "Groovy", value: "groovy" },
{ label: "Haskell", value: "haskell" },
{ label: "HCL", value: "hcl" },
{ label: "HTML", value: "html" },
{ label: "Idris", value: "idris" },
{ label: "Java", value: "java" },
{ label: "JavaScript", value: "javascript" },
{ label: "JSON", value: "json" },
{ label: "Julia", value: "julia" },
{ label: "Kotlin", value: "kotlin" },
{ label: "LaTeX", value: "latex" },
{ label: "Less", value: "less" },
{ label: "Lisp", value: "lisp" },
{ label: "LiveScript", value: "livescript" },
{ label: "LLVM IR", value: "llvm" },
{ label: "Lua", value: "lua" },
{ label: "Makefile", value: "makefile" },
{ label: "Markdown", value: "markdown" },
{ label: "Markup", value: "markup" },
{ label: "MATLAB", value: "matlab" },
{ label: "Mathematica", value: "mathematica" },
{ label: "Mermaid", value: "mermaid" },
{ label: "Nix", value: "nix" },
{ label: "Notion Formula", value: "notion" },
{ label: "Objective-C", value: "objectivec" },
{ label: "OCaml", value: "ocaml" },
{ label: "Pascal", value: "pascal" },
{ label: "Perl", value: "perl" },
{ label: "PHP", value: "php" },
{ label: "PowerShell", value: "powershell" },
{ label: "Prolog", value: "prolog" },
{ label: "Protocol Buffers", value: "protobuf" },
{ label: "PureScript", value: "purescript" },
{ label: "Python", value: "python" },
{ label: "R", value: "r" },
{ label: "Racket", value: "racket" },
{ label: "Reason", value: "reasonml" },
{ label: "Ruby", value: "ruby" },
{ label: "Rust", value: "rust" },
{ label: "Sass", value: "scss" },
{ label: "Scala", value: "scala" },
{ label: "Scheme", value: "scheme" },
{ label: "SCSS", value: "scss" },
{ label: "Shell", value: "shell" },
{ label: "Smalltalk", value: "smalltalk" },
{ label: "Solidity", value: "solidity" },
{ label: "SQL", value: "sql" },
{ label: "Swift", value: "swift" },
{ label: "TOML", value: "toml" },
{ label: "TypeScript", value: "typescript" },
{ label: "VB.Net", value: "vbnet" },
{ label: "Verilog", value: "verilog" },
{ label: "VHDL", value: "vhdl" },
{ label: "Visual Basic", value: "vbnet" },
{ label: "WebAssembly", value: "wasm" },
{ label: "XML", value: "xml" },
{ label: "YAML", value: "yaml" },
];

View file

@ -1,19 +1,19 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { PlateLeafProps } from 'platejs/react';
import type { PlateLeafProps } from "platejs/react";
import { PlateLeaf } from 'platejs/react';
import { PlateLeaf } from "platejs/react";
export function CodeLeaf(props: PlateLeafProps) {
return (
<PlateLeaf
{...props}
as="code"
className="whitespace-pre-wrap rounded-md bg-muted px-[0.3em] py-[0.2em] font-mono text-sm"
>
{props.children}
</PlateLeaf>
);
return (
<PlateLeaf
{...props}
as="code"
className="whitespace-pre-wrap rounded-md bg-muted px-[0.3em] py-[0.2em] font-mono text-sm"
>
{props.children}
</PlateLeaf>
);
}

View file

@ -1,257 +1,228 @@
"use client"
"use client";
import * as React from "react"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
import * as React from "react";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
}
function DropdownMenuTrigger({
...props
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground dark:focus:bg-neutral-700 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground dark:focus:bg-neutral-700 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground dark:focus:bg-neutral-700 relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground dark:focus:bg-neutral-700 relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
}
function DropdownMenuRadioItem({
className,
children,
...props
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground dark:focus:bg-neutral-700 relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground dark:focus:bg-neutral-700 relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground dark:focus:bg-neutral-700 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground dark:data-[state=open]:bg-neutral-700 [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground dark:focus:bg-neutral-700 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground dark:data-[state=open]:bg-neutral-700 [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View file

@ -1,132 +1,124 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { VariantProps } from 'class-variance-authority';
import type { PlateContentProps, PlateViewProps } from 'platejs/react';
import type { VariantProps } from "class-variance-authority";
import type { PlateContentProps, PlateViewProps } from "platejs/react";
import { cva } from 'class-variance-authority';
import { PlateContainer, PlateContent, PlateView } from 'platejs/react';
import { cva } from "class-variance-authority";
import { PlateContainer, PlateContent, PlateView } from "platejs/react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
const editorContainerVariants = cva(
'relative w-full cursor-text select-text overflow-y-auto caret-primary selection:bg-brand/25 focus-visible:outline-none [&_.slate-selection-area]:z-50 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',
{
defaultVariants: {
variant: 'default',
},
variants: {
variant: {
comment: cn(
'flex flex-wrap justify-between gap-1 px-1 py-0.5 text-sm',
'rounded-md border-[1.5px] border-transparent bg-transparent',
'has-[[data-slate-editor]:focus]:border-brand/50 has-[[data-slate-editor]:focus]:ring-2 has-[[data-slate-editor]:focus]:ring-brand/30',
'has-aria-disabled:border-input has-aria-disabled:bg-muted'
),
default: 'h-full',
demo: 'h-[650px]',
select: cn(
'group rounded-md border border-input ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
'has-data-readonly:w-fit has-data-readonly:cursor-default has-data-readonly:border-transparent has-data-readonly:focus-within:[box-shadow:none]'
),
},
},
}
"relative w-full cursor-text select-text overflow-y-auto caret-primary selection:bg-brand/25 focus-visible:outline-none [&_.slate-selection-area]:z-50 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15",
{
defaultVariants: {
variant: "default",
},
variants: {
variant: {
comment: cn(
"flex flex-wrap justify-between gap-1 px-1 py-0.5 text-sm",
"rounded-md border-[1.5px] border-transparent bg-transparent",
"has-[[data-slate-editor]:focus]:border-brand/50 has-[[data-slate-editor]:focus]:ring-2 has-[[data-slate-editor]:focus]:ring-brand/30",
"has-aria-disabled:border-input has-aria-disabled:bg-muted"
),
default: "h-full",
demo: "h-[650px]",
select: cn(
"group rounded-md border border-input ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
"has-data-readonly:w-fit has-data-readonly:cursor-default has-data-readonly:border-transparent has-data-readonly:focus-within:[box-shadow:none]"
),
},
},
}
);
export function EditorContainer({
className,
variant,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof editorContainerVariants>) {
return (
<PlateContainer
className={cn(
'ignore-click-outside/toolbar',
editorContainerVariants({ variant }),
className
)}
{...props}
/>
);
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof editorContainerVariants>) {
return (
<PlateContainer
className={cn(
"ignore-click-outside/toolbar",
editorContainerVariants({ variant }),
className
)}
{...props}
/>
);
}
const editorVariants = cva(
cn(
'group/editor',
'relative w-full cursor-text select-text overflow-x-hidden whitespace-pre-wrap break-words',
'rounded-md ring-offset-background focus-visible:outline-none',
'**:data-slate-placeholder:!top-1/2 **:data-slate-placeholder:-translate-y-1/2 placeholder:text-muted-foreground/80 **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!',
'[&_strong]:font-bold'
),
{
defaultVariants: {
variant: 'default',
},
variants: {
disabled: {
true: 'cursor-not-allowed opacity-50',
},
focused: {
true: 'ring-2 ring-ring ring-offset-2',
},
variant: {
ai: 'w-full px-0 text-base md:text-sm',
aiChat:
'max-h-[min(70vh,320px)] w-full overflow-y-auto px-3 py-2 text-base md:text-sm',
comment: cn('rounded-none border-none bg-transparent text-sm'),
default:
'size-full px-6 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
demo: 'size-full px-6 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
fullWidth: 'size-full px-6 pt-4 pb-72 text-base sm:px-24',
none: '',
select: 'px-3 py-2 text-base data-readonly:w-fit',
},
},
}
cn(
"group/editor",
"relative w-full cursor-text select-text overflow-x-hidden whitespace-pre-wrap break-words",
"rounded-md ring-offset-background focus-visible:outline-none",
"**:data-slate-placeholder:!top-1/2 **:data-slate-placeholder:-translate-y-1/2 placeholder:text-muted-foreground/80 **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!",
"[&_strong]:font-bold"
),
{
defaultVariants: {
variant: "default",
},
variants: {
disabled: {
true: "cursor-not-allowed opacity-50",
},
focused: {
true: "ring-2 ring-ring ring-offset-2",
},
variant: {
ai: "w-full px-0 text-base md:text-sm",
aiChat: "max-h-[min(70vh,320px)] w-full overflow-y-auto px-3 py-2 text-base md:text-sm",
comment: cn("rounded-none border-none bg-transparent text-sm"),
default: "size-full px-6 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]",
demo: "size-full px-6 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]",
fullWidth: "size-full px-6 pt-4 pb-72 text-base sm:px-24",
none: "",
select: "px-3 py-2 text-base data-readonly:w-fit",
},
},
}
);
export type EditorProps = PlateContentProps &
VariantProps<typeof editorVariants>;
export type EditorProps = PlateContentProps & VariantProps<typeof editorVariants>;
export const Editor = ({
className,
disabled,
focused,
variant,
ref,
...props
className,
disabled,
focused,
variant,
ref,
...props
}: EditorProps & { ref?: React.RefObject<HTMLDivElement | null> }) => (
<PlateContent
ref={ref}
className={cn(
editorVariants({
disabled,
focused,
variant,
}),
className
)}
disabled={disabled}
disableDefaultStyles
{...props}
/>
<PlateContent
ref={ref}
className={cn(
editorVariants({
disabled,
focused,
variant,
}),
className
)}
disabled={disabled}
disableDefaultStyles
{...props}
/>
);
Editor.displayName = 'Editor';
Editor.displayName = "Editor";
export function EditorView({
className,
variant,
...props
className,
variant,
...props
}: PlateViewProps & VariantProps<typeof editorVariants>) {
return (
<PlateView
{...props}
className={cn(editorVariants({ variant }), className)}
/>
);
return <PlateView {...props} className={cn(editorVariants({ variant }), className)} />;
}
EditorView.displayName = 'EditorView';
EditorView.displayName = "EditorView";

View file

@ -1,183 +1,176 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { TEquationElement } from 'platejs';
import type { TEquationElement } from "platejs";
import {
useEquationElement,
useEquationInput,
} from '@platejs/math/react';
import { RadicalIcon } from 'lucide-react';
import { type PlateElementProps, PlateElement, useSelected } from 'platejs/react';
import { useEquationElement, useEquationInput } from "@platejs/math/react";
import { RadicalIcon } from "lucide-react";
import { type PlateElementProps, PlateElement, useSelected } from "platejs/react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
export function EquationElement({
children,
...props
}: PlateElementProps<TEquationElement>) {
const element = props.element;
const selected = useSelected();
const katexRef = React.useRef<HTMLDivElement | null>(null);
const [isEditing, setIsEditing] = React.useState(false);
export function EquationElement({ children, ...props }: PlateElementProps<TEquationElement>) {
const element = props.element;
const selected = useSelected();
const katexRef = React.useRef<HTMLDivElement | null>(null);
const [isEditing, setIsEditing] = React.useState(false);
useEquationElement({
element,
katexRef,
options: {
displayMode: true,
throwOnError: false,
},
});
useEquationElement({
element,
katexRef,
options: {
displayMode: true,
throwOnError: false,
},
});
const { props: inputProps, ref: inputRef, onDismiss, onSubmit } = useEquationInput({
isInline: false,
open: isEditing,
onClose: () => setIsEditing(false),
});
const {
props: inputProps,
ref: inputRef,
onDismiss,
onSubmit,
} = useEquationInput({
isInline: false,
open: isEditing,
onClose: () => setIsEditing(false),
});
return (
<PlateElement
{...props}
className={cn(
'my-3 rounded-md py-2',
selected && 'ring-2 ring-ring ring-offset-2',
props.className
)}
>
<div
className="flex cursor-pointer items-center justify-center"
contentEditable={false}
onDoubleClick={() => setIsEditing(true)}
>
{element.texExpression ? (
<div ref={katexRef} className="text-center" />
) : (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<RadicalIcon className="size-4" />
<span>Add an equation</span>
</div>
)}
</div>
return (
<PlateElement
{...props}
className={cn(
"my-3 rounded-md py-2",
selected && "ring-2 ring-ring ring-offset-2",
props.className
)}
>
<div
className="flex cursor-pointer items-center justify-center"
contentEditable={false}
onDoubleClick={() => setIsEditing(true)}
>
{element.texExpression ? (
<div ref={katexRef} className="text-center" />
) : (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<RadicalIcon className="size-4" />
<span>Add an equation</span>
</div>
)}
</div>
{isEditing && (
<div
className="mt-2 rounded-md border bg-muted/50 p-2"
contentEditable={false}
>
<textarea
ref={inputRef}
className="w-full resize-none rounded border-none bg-transparent p-2 font-mono text-sm outline-none"
placeholder="E = mc^2"
rows={3}
{...inputProps}
/>
<div className="mt-1 flex justify-end gap-1">
<button
className="rounded px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
onClick={onDismiss}
type="button"
>
Cancel
</button>
<button
className="rounded bg-primary px-2 py-1 text-xs text-primary-foreground hover:bg-primary/90"
onClick={onSubmit}
type="button"
>
Done
</button>
</div>
</div>
)}
{isEditing && (
<div className="mt-2 rounded-md border bg-muted/50 p-2" contentEditable={false}>
<textarea
ref={inputRef}
className="w-full resize-none rounded border-none bg-transparent p-2 font-mono text-sm outline-none"
placeholder="E = mc^2"
rows={3}
{...inputProps}
/>
<div className="mt-1 flex justify-end gap-1">
<button
className="rounded px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
onClick={onDismiss}
type="button"
>
Cancel
</button>
<button
className="rounded bg-primary px-2 py-1 text-xs text-primary-foreground hover:bg-primary/90"
onClick={onSubmit}
type="button"
>
Done
</button>
</div>
</div>
)}
{children}
</PlateElement>
);
{children}
</PlateElement>
);
}
export function InlineEquationElement({
children,
...props
}: PlateElementProps<TEquationElement>) {
const element = props.element;
const selected = useSelected();
const katexRef = React.useRef<HTMLDivElement | null>(null);
const [isEditing, setIsEditing] = React.useState(false);
export function InlineEquationElement({ children, ...props }: PlateElementProps<TEquationElement>) {
const element = props.element;
const selected = useSelected();
const katexRef = React.useRef<HTMLDivElement | null>(null);
const [isEditing, setIsEditing] = React.useState(false);
useEquationElement({
element,
katexRef,
options: {
displayMode: false,
throwOnError: false,
},
});
useEquationElement({
element,
katexRef,
options: {
displayMode: false,
throwOnError: false,
},
});
const { props: inputProps, ref: inputRef, onDismiss, onSubmit } = useEquationInput({
isInline: true,
open: isEditing,
onClose: () => setIsEditing(false),
});
const {
props: inputProps,
ref: inputRef,
onDismiss,
onSubmit,
} = useEquationInput({
isInline: true,
open: isEditing,
onClose: () => setIsEditing(false),
});
return (
<PlateElement
{...props}
as="span"
className={cn(
'inline rounded-sm px-0.5',
selected && 'bg-brand/15',
props.className
)}
>
<span
className="cursor-pointer"
contentEditable={false}
onDoubleClick={() => setIsEditing(true)}
>
{element.texExpression ? (
<span ref={katexRef} />
) : (
<span className="text-sm text-muted-foreground">
<RadicalIcon className="inline size-3.5" />
</span>
)}
</span>
return (
<PlateElement
{...props}
as="span"
className={cn("inline rounded-sm px-0.5", selected && "bg-brand/15", props.className)}
>
<span
className="cursor-pointer"
contentEditable={false}
onDoubleClick={() => setIsEditing(true)}
>
{element.texExpression ? (
<span ref={katexRef} />
) : (
<span className="text-sm text-muted-foreground">
<RadicalIcon className="inline size-3.5" />
</span>
)}
</span>
{isEditing && (
<span
className="absolute z-50 mt-1 rounded-md border bg-popover p-2 shadow-md"
contentEditable={false}
>
<textarea
ref={inputRef}
className="w-48 resize-none rounded border-none bg-transparent p-1 font-mono text-sm outline-none"
placeholder="x^2"
rows={1}
{...inputProps}
/>
<span className="mt-1 flex justify-end gap-1">
<button
className="rounded px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent"
onClick={onDismiss}
type="button"
>
Cancel
</button>
<button
className="rounded bg-primary px-2 py-0.5 text-xs text-primary-foreground hover:bg-primary/90"
onClick={onSubmit}
type="button"
>
Done
</button>
</span>
</span>
)}
{isEditing && (
<span
className="absolute z-50 mt-1 rounded-md border bg-popover p-2 shadow-md"
contentEditable={false}
>
<textarea
ref={inputRef}
className="w-48 resize-none rounded border-none bg-transparent p-1 font-mono text-sm outline-none"
placeholder="x^2"
rows={1}
{...inputProps}
/>
<span className="mt-1 flex justify-end gap-1">
<button
className="rounded px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent"
onClick={onDismiss}
type="button"
>
Cancel
</button>
<button
className="rounded bg-primary px-2 py-0.5 text-xs text-primary-foreground hover:bg-primary/90"
onClick={onSubmit}
type="button"
>
Done
</button>
</span>
</span>
)}
{children}
</PlateElement>
);
{children}
</PlateElement>
);
}

View file

@ -1,138 +1,125 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import {
BoldIcon,
Code2Icon,
HighlighterIcon,
ItalicIcon,
RedoIcon,
SaveIcon,
StrikethroughIcon,
UnderlineIcon,
UndoIcon,
} from 'lucide-react';
import { KEYS } from 'platejs';
import { useEditorReadOnly, useEditorRef } from 'platejs/react';
BoldIcon,
Code2Icon,
HighlighterIcon,
ItalicIcon,
RedoIcon,
SaveIcon,
StrikethroughIcon,
UnderlineIcon,
UndoIcon,
} from "lucide-react";
import { KEYS } from "platejs";
import { useEditorReadOnly, useEditorRef } from "platejs/react";
import { useEditorSave } from '@/components/editor/editor-save-context';
import { Spinner } from '@/components/ui/spinner';
import { useEditorSave } from "@/components/editor/editor-save-context";
import { Spinner } from "@/components/ui/spinner";
import { InsertToolbarButton } from './insert-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { ToolbarButton, ToolbarGroup } from './toolbar';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
import { InsertToolbarButton } from "./insert-toolbar-button";
import { LinkToolbarButton } from "./link-toolbar-button";
import { MarkToolbarButton } from "./mark-toolbar-button";
import { ModeToolbarButton } from "./mode-toolbar-button";
import { ToolbarButton, ToolbarGroup } from "./toolbar";
import { TurnIntoToolbarButton } from "./turn-into-toolbar-button";
export function FixedToolbarButtons() {
const readOnly = useEditorReadOnly();
const editor = useEditorRef();
const { onSave, hasUnsavedChanges, isSaving, canToggleMode } = useEditorSave();
const readOnly = useEditorReadOnly();
const editor = useEditorRef();
const { onSave, hasUnsavedChanges, isSaving, canToggleMode } = useEditorSave();
return (
<div className="flex w-full items-center">
{/* Scrollable editing buttons */}
<div className="flex flex-1 min-w-0 overflow-x-auto scrollbar-hide">
{!readOnly && (
<>
<ToolbarGroup>
<ToolbarButton
tooltip="Undo (⌘+Z)"
onClick={() => {
editor.undo();
editor.tf.focus();
}}
>
<UndoIcon />
</ToolbarButton>
return (
<div className="flex w-full items-center">
{/* Scrollable editing buttons */}
<div className="flex flex-1 min-w-0 overflow-x-auto scrollbar-hide">
{!readOnly && (
<>
<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>
<ToolbarButton
tooltip="Redo (⌘+⇧+Z)"
onClick={() => {
editor.redo();
editor.tf.focus();
}}
>
<RedoIcon />
</ToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<InsertToolbarButton />
<TurnIntoToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<InsertToolbarButton />
<TurnIntoToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MarkToolbarButton nodeType={KEYS.bold} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<ToolbarGroup>
<MarkToolbarButton nodeType={KEYS.bold} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.italic} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.italic} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
nodeType={KEYS.underline}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.underline} tooltip="Underline (⌘+U)">
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
nodeType={KEYS.strikethrough}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.strikethrough} tooltip="Strikethrough (⌘+⇧+M)">
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.code} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.code} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<MarkToolbarButton
nodeType={KEYS.highlight}
tooltip="Highlight (⌘+⇧+H)"
>
<HighlighterIcon />
</MarkToolbarButton>
</ToolbarGroup>
<MarkToolbarButton nodeType={KEYS.highlight} tooltip="Highlight (⌘+⇧+H)">
<HighlighterIcon />
</MarkToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<LinkToolbarButton />
</ToolbarGroup>
</>
)}
</div>
<ToolbarGroup>
<LinkToolbarButton />
</ToolbarGroup>
</>
)}
</div>
{/* Fixed right-side buttons (Save + Mode) */}
<div className="flex shrink-0 items-center">
{/* Save button — only in edit mode with unsaved changes */}
{!readOnly && onSave && hasUnsavedChanges && (
<ToolbarGroup>
<ToolbarButton
tooltip={isSaving ? 'Saving...' : 'Save (⌘+S)'}
onClick={onSave}
disabled={isSaving}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{isSaving ? (
<Spinner size="xs" />
) : (
<SaveIcon />
)}
</ToolbarButton>
</ToolbarGroup>
)}
{/* Fixed right-side buttons (Save + Mode) */}
<div className="flex shrink-0 items-center">
{/* Save button — only in edit mode with unsaved changes */}
{!readOnly && onSave && hasUnsavedChanges && (
<ToolbarGroup>
<ToolbarButton
tooltip={isSaving ? "Saving..." : "Save (⌘+S)"}
onClick={onSave}
disabled={isSaving}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{isSaving ? <Spinner size="xs" /> : <SaveIcon />}
</ToolbarButton>
</ToolbarGroup>
)}
{/* Mode toggle */}
{canToggleMode && (
<ToolbarGroup>
<ModeToolbarButton />
</ToolbarGroup>
)}
</div>
</div>
);
{/* Mode toggle */}
{canToggleMode && (
<ToolbarGroup>
<ModeToolbarButton />
</ToolbarGroup>
)}
</div>
</div>
);
}

View file

@ -1,26 +1,25 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
import { Toolbar } from './toolbar';
import { Toolbar } from "./toolbar";
export function FixedToolbar({
children,
className,
...props
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>
);
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

@ -1,55 +1,48 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import {
BoldIcon,
Code2Icon,
ItalicIcon,
StrikethroughIcon,
UnderlineIcon,
} from 'lucide-react';
import { KEYS } from 'platejs';
import { useEditorReadOnly } from 'platejs/react';
import { BoldIcon, Code2Icon, ItalicIcon, StrikethroughIcon, UnderlineIcon } from "lucide-react";
import { KEYS } from "platejs";
import { useEditorReadOnly } from "platejs/react";
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { ToolbarGroup } from './toolbar';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
import { LinkToolbarButton } from "./link-toolbar-button";
import { MarkToolbarButton } from "./mark-toolbar-button";
import { ToolbarGroup } from "./toolbar";
import { TurnIntoToolbarButton } from "./turn-into-toolbar-button";
export function FloatingToolbarButtons() {
const readOnly = useEditorReadOnly();
const readOnly = useEditorReadOnly();
if (readOnly) return null;
if (readOnly) return null;
return (
<>
<ToolbarGroup>
<TurnIntoToolbarButton tooltip={false} />
return (
<>
<ToolbarGroup>
<TurnIntoToolbarButton tooltip={false} />
<MarkToolbarButton nodeType={KEYS.bold}>
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.bold}>
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.italic}>
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.italic}>
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.underline}>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.underline}>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.strikethrough}>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.strikethrough}>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.code}>
<Code2Icon />
</MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.code}>
<Code2Icon />
</MarkToolbarButton>
<LinkToolbarButton tooltip={false} />
</ToolbarGroup>
</>
);
<LinkToolbarButton tooltip={false} />
</ToolbarGroup>
</>
);
}

View file

@ -1,88 +1,79 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import {
type FloatingToolbarState,
flip,
offset,
useFloatingToolbar,
useFloatingToolbarState,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { KEYS } from 'platejs';
import {
useEditorId,
useEventEditorValue,
usePluginOption,
} from 'platejs/react';
type FloatingToolbarState,
flip,
offset,
useFloatingToolbar,
useFloatingToolbarState,
} from "@platejs/floating";
import { useComposedRef } from "@udecode/cn";
import { KEYS } from "platejs";
import { useEditorId, useEventEditorValue, usePluginOption } from "platejs/react";
import { cn } from '@/lib/utils';
import { useIsMobile } from '@/hooks/use-mobile';
import { cn } from "@/lib/utils";
import { useIsMobile } from "@/hooks/use-mobile";
import { Toolbar } from './toolbar';
import { Toolbar } from "./toolbar";
export function FloatingToolbar({
children,
className,
state,
...props
children,
className,
state,
...props
}: React.ComponentProps<typeof Toolbar> & {
state?: FloatingToolbarState;
state?: FloatingToolbarState;
}) {
const editorId = useEditorId();
const focusedEditorId = useEventEditorValue('focus');
const isFloatingLinkOpen = !!usePluginOption({ key: KEYS.link }, 'mode');
const isMobile = useIsMobile();
const editorId = useEditorId();
const focusedEditorId = useEventEditorValue("focus");
const isFloatingLinkOpen = !!usePluginOption({ key: KEYS.link }, "mode");
const isMobile = useIsMobile();
const floatingToolbarState = useFloatingToolbarState({
editorId,
focusedEditorId,
hideToolbar: isFloatingLinkOpen,
...state,
floatingOptions: {
middleware: [
offset(12),
flip({
fallbackPlacements: [
'top-start',
'top-end',
'bottom-start',
'bottom-end',
],
padding: 12,
}),
],
placement: 'top',
...state?.floatingOptions,
},
});
const floatingToolbarState = useFloatingToolbarState({
editorId,
focusedEditorId,
hideToolbar: isFloatingLinkOpen,
...state,
floatingOptions: {
middleware: [
offset(12),
flip({
fallbackPlacements: ["top-start", "top-end", "bottom-start", "bottom-end"],
padding: 12,
}),
],
placement: "top",
...state?.floatingOptions,
},
});
const {
clickOutsideRef,
hidden,
props: rootProps,
ref: floatingRef,
} = useFloatingToolbar(floatingToolbarState);
const {
clickOutsideRef,
hidden,
props: rootProps,
ref: floatingRef,
} = useFloatingToolbar(floatingToolbarState);
const ref = useComposedRef<HTMLDivElement>(props.ref, floatingRef);
const ref = useComposedRef<HTMLDivElement>(props.ref, floatingRef);
if (hidden || isMobile) return null;
if (hidden || isMobile) return null;
return (
<div ref={clickOutsideRef}>
<Toolbar
{...props}
{...rootProps}
ref={ref}
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 dark:bg-neutral-800 dark:border-neutral-700',
'max-w-[80vw]',
className
)}
>
{children}
</Toolbar>
</div>
);
return (
<div ref={clickOutsideRef}>
<Toolbar
{...props}
{...rootProps}
ref={ref}
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 dark:bg-neutral-800 dark:border-neutral-700",
"max-w-[80vw]",
className
)}
>
{children}
</Toolbar>
</div>
);
}

View file

@ -1,60 +1,56 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { PlateElementProps } from 'platejs/react';
import type { PlateElementProps } from "platejs/react";
import { type VariantProps, cva } from 'class-variance-authority';
import { PlateElement } from 'platejs/react';
import { type VariantProps, cva } from "class-variance-authority";
import { PlateElement } from "platejs/react";
const headingVariants = cva('relative mb-1', {
variants: {
variant: {
h1: 'mt-[1.6em] pb-1 font-bold font-heading text-4xl',
h2: 'mt-[1.4em] pb-px font-heading font-semibold text-2xl tracking-tight',
h3: 'mt-[1em] pb-px font-heading font-semibold text-xl tracking-tight',
h4: 'mt-[0.75em] font-heading font-semibold text-lg tracking-tight',
h5: 'mt-[0.75em] font-semibold text-lg tracking-tight',
h6: 'mt-[0.75em] font-semibold text-base tracking-tight',
},
},
const headingVariants = cva("relative mb-1", {
variants: {
variant: {
h1: "mt-[1.6em] pb-1 font-bold font-heading text-4xl",
h2: "mt-[1.4em] pb-px font-heading font-semibold text-2xl tracking-tight",
h3: "mt-[1em] pb-px font-heading font-semibold text-xl tracking-tight",
h4: "mt-[0.75em] font-heading font-semibold text-lg tracking-tight",
h5: "mt-[0.75em] font-semibold text-lg tracking-tight",
h6: "mt-[0.75em] font-semibold text-base tracking-tight",
},
},
});
export function HeadingElement({
variant = 'h1',
...props
variant = "h1",
...props
}: PlateElementProps & VariantProps<typeof headingVariants>) {
return (
<PlateElement
as={variant!}
className={headingVariants({ variant })}
{...props}
>
{props.children}
</PlateElement>
);
return (
<PlateElement as={variant!} className={headingVariants({ variant })} {...props}>
{props.children}
</PlateElement>
);
}
export function H1Element(props: PlateElementProps) {
return <HeadingElement variant="h1" {...props} />;
return <HeadingElement variant="h1" {...props} />;
}
export function H2Element(props: PlateElementProps) {
return <HeadingElement variant="h2" {...props} />;
return <HeadingElement variant="h2" {...props} />;
}
export function H3Element(props: PlateElementProps) {
return <HeadingElement variant="h3" {...props} />;
return <HeadingElement variant="h3" {...props} />;
}
export function H4Element(props: PlateElementProps) {
return <HeadingElement variant="h4" {...props} />;
return <HeadingElement variant="h4" {...props} />;
}
export function H5Element(props: PlateElementProps) {
return <HeadingElement variant="h5" {...props} />;
return <HeadingElement variant="h5" {...props} />;
}
export function H6Element(props: PlateElementProps) {
return <HeadingElement variant="h6" {...props} />;
return <HeadingElement variant="h6" {...props} />;
}

View file

@ -1,15 +1,15 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { PlateLeafProps } from 'platejs/react';
import type { PlateLeafProps } from "platejs/react";
import { PlateLeaf } from 'platejs/react';
import { PlateLeaf } from "platejs/react";
export function HighlightLeaf(props: PlateLeafProps) {
return (
<PlateLeaf {...props} as="mark" className="bg-yellow-200 dark:bg-yellow-800 text-inherit">
{props.children}
</PlateLeaf>
);
return (
<PlateLeaf {...props} as="mark" className="bg-yellow-200 dark:bg-yellow-800 text-inherit">
{props.children}
</PlateLeaf>
);
}

View file

@ -1,35 +1,30 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { PlateElementProps } from 'platejs/react';
import type { PlateElementProps } from "platejs/react";
import {
PlateElement,
useFocused,
useReadOnly,
useSelected,
} from 'platejs/react';
import { PlateElement, useFocused, useReadOnly, useSelected } from "platejs/react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
export function HrElement(props: PlateElementProps) {
const readOnly = useReadOnly();
const selected = useSelected();
const focused = useFocused();
const readOnly = useReadOnly();
const selected = useSelected();
const focused = useFocused();
return (
<PlateElement {...props}>
<div className="py-6" contentEditable={false}>
<hr
className={cn(
'h-0.5 rounded-sm border-none bg-muted bg-clip-content',
selected && focused && 'ring-2 ring-ring ring-offset-2',
!readOnly && 'cursor-pointer'
)}
/>
</div>
{props.children}
</PlateElement>
);
return (
<PlateElement {...props}>
<div className="py-6" contentEditable={false}>
<hr
className={cn(
"h-0.5 rounded-sm border-none bg-muted bg-clip-content",
selected && focused && "ring-2 ring-ring ring-offset-2",
!readOnly && "cursor-pointer"
)}
/>
</div>
{props.children}
</PlateElement>
);
}

View file

@ -1,450 +1,406 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { Point, TElement } from 'platejs';
import type { Point, TElement } from "platejs";
import {
type ComboboxItemProps,
Combobox,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxItem,
ComboboxPopover,
ComboboxProvider,
ComboboxRow,
Portal,
useComboboxContext,
useComboboxStore,
} from '@ariakit/react';
import { filterWords } from '@platejs/combobox';
type ComboboxItemProps,
Combobox,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxItem,
ComboboxPopover,
ComboboxProvider,
ComboboxRow,
Portal,
useComboboxContext,
useComboboxStore,
} from "@ariakit/react";
import { filterWords } from "@platejs/combobox";
import {
type UseComboboxInputResult,
useComboboxInput,
useHTMLInputCursorState,
} from '@platejs/combobox/react';
import { cva } from 'class-variance-authority';
import { useComposedRef, useEditorRef } from 'platejs/react';
type UseComboboxInputResult,
useComboboxInput,
useHTMLInputCursorState,
} from "@platejs/combobox/react";
import { cva } from "class-variance-authority";
import { useComposedRef, useEditorRef } from "platejs/react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
function useRequiredComboboxContext() {
const context = useComboboxContext();
const context = useComboboxContext();
if (!context) {
throw new Error(
'InlineCombobox compound components must be rendered within InlineCombobox'
);
}
if (!context) {
throw new Error("InlineCombobox compound components must be rendered within InlineCombobox");
}
return context;
return context;
}
type FilterFn = (
item: { value: string; group?: string; keywords?: string[]; label?: string },
search: string
item: { value: string; group?: string; keywords?: string[]; label?: string },
search: string
) => boolean;
type InlineComboboxContextValue = {
filter: FilterFn | false;
inputProps: UseComboboxInputResult['props'];
inputRef: React.RefObject<HTMLInputElement | null>;
removeInput: UseComboboxInputResult['removeInput'];
showTrigger: boolean;
trigger: string;
setHasEmpty: (hasEmpty: boolean) => void;
filter: FilterFn | false;
inputProps: UseComboboxInputResult["props"];
inputRef: React.RefObject<HTMLInputElement | null>;
removeInput: UseComboboxInputResult["removeInput"];
showTrigger: boolean;
trigger: string;
setHasEmpty: (hasEmpty: boolean) => void;
};
const InlineComboboxContext = React.createContext<InlineComboboxContextValue>(
null as unknown as InlineComboboxContextValue
null as unknown as InlineComboboxContextValue
);
const defaultFilter: FilterFn = (
{ group, keywords = [], label, value },
search
) => {
const uniqueTerms = new Set(
[value, ...keywords, group, label].filter(Boolean)
);
const defaultFilter: FilterFn = ({ group, keywords = [], label, value }, search) => {
const uniqueTerms = new Set([value, ...keywords, group, label].filter(Boolean));
return Array.from(uniqueTerms).some((keyword) =>
filterWords(keyword as string, search)
);
return Array.from(uniqueTerms).some((keyword) => filterWords(keyword as string, search));
};
type InlineComboboxProps = {
children: React.ReactNode;
element: TElement;
trigger: string;
filter?: FilterFn | false;
hideWhenNoValue?: boolean;
showTrigger?: boolean;
value?: string;
setValue?: (value: string) => void;
children: React.ReactNode;
element: TElement;
trigger: string;
filter?: FilterFn | false;
hideWhenNoValue?: boolean;
showTrigger?: boolean;
value?: string;
setValue?: (value: string) => void;
};
const InlineCombobox = ({
children,
element,
filter = defaultFilter,
hideWhenNoValue = false,
setValue: setValueProp,
showTrigger = true,
trigger,
value: valueProp,
children,
element,
filter = defaultFilter,
hideWhenNoValue = false,
setValue: setValueProp,
showTrigger = true,
trigger,
value: valueProp,
}: InlineComboboxProps) => {
const editor = useEditorRef();
const inputRef = React.useRef<HTMLInputElement>(null);
const cursorState = useHTMLInputCursorState(inputRef);
const editor = useEditorRef();
const inputRef = React.useRef<HTMLInputElement>(null);
const cursorState = useHTMLInputCursorState(inputRef);
const [valueState, setValueState] = React.useState('');
const hasValueProp = valueProp !== undefined;
const value = hasValueProp ? valueProp : valueState;
const [valueState, setValueState] = React.useState("");
const hasValueProp = valueProp !== undefined;
const value = hasValueProp ? valueProp : valueState;
// Check if current user is the creator of this element (for Yjs collaboration)
const isCreator = React.useMemo(() => {
const elementUserId = (element as Record<string, unknown>).userId;
const currentUserId = editor.meta.userId;
// Check if current user is the creator of this element (for Yjs collaboration)
const isCreator = React.useMemo(() => {
const elementUserId = (element as Record<string, unknown>).userId;
const currentUserId = editor.meta.userId;
// If no userId (backwards compatibility or non-Yjs), allow
if (!elementUserId) return true;
// If no userId (backwards compatibility or non-Yjs), allow
if (!elementUserId) return true;
return elementUserId === currentUserId;
}, [editor.meta.userId, element]);
return elementUserId === currentUserId;
}, [editor.meta.userId, element]);
const setValue = React.useCallback(
(newValue: string) => {
setValueProp?.(newValue);
const setValue = React.useCallback(
(newValue: string) => {
setValueProp?.(newValue);
if (!hasValueProp) {
setValueState(newValue);
}
},
[setValueProp, hasValueProp]
);
if (!hasValueProp) {
setValueState(newValue);
}
},
[setValueProp, hasValueProp]
);
/**
* Track the point just before the input element so we know where to
* insertText if the combobox closes due to a selection change.
*/
const insertPoint = React.useRef<Point | null>(null);
/**
* Track the point just before the input element so we know where to
* insertText if the combobox closes due to a selection change.
*/
const insertPoint = React.useRef<Point | null>(null);
React.useEffect(() => {
const path = editor.api.findPath(element);
React.useEffect(() => {
const path = editor.api.findPath(element);
if (!path) return;
if (!path) return;
const point = editor.api.before(path);
const point = editor.api.before(path);
if (!point) return;
if (!point) return;
const pointRef = editor.api.pointRef(point);
insertPoint.current = pointRef.current;
const pointRef = editor.api.pointRef(point);
insertPoint.current = pointRef.current;
return () => {
pointRef.unref();
};
}, [editor, element]);
return () => {
pointRef.unref();
};
}, [editor, element]);
const { props: inputProps, removeInput } = useComboboxInput({
cancelInputOnBlur: true,
cursorState,
autoFocus: isCreator,
ref: inputRef,
onCancelInput: (cause) => {
if (cause !== 'backspace') {
editor.tf.insertText(trigger + value, {
at: insertPoint?.current ?? undefined,
});
}
if (cause === 'arrowLeft' || cause === 'arrowRight') {
editor.tf.move({
distance: 1,
reverse: cause === 'arrowLeft',
});
}
},
});
const { props: inputProps, removeInput } = useComboboxInput({
cancelInputOnBlur: true,
cursorState,
autoFocus: isCreator,
ref: inputRef,
onCancelInput: (cause) => {
if (cause !== "backspace") {
editor.tf.insertText(trigger + value, {
at: insertPoint?.current ?? undefined,
});
}
if (cause === "arrowLeft" || cause === "arrowRight") {
editor.tf.move({
distance: 1,
reverse: cause === "arrowLeft",
});
}
},
});
const [hasEmpty, setHasEmpty] = React.useState(false);
const [hasEmpty, setHasEmpty] = React.useState(false);
const contextValue: InlineComboboxContextValue = React.useMemo(
() => ({
filter,
inputProps,
inputRef,
removeInput,
setHasEmpty,
showTrigger,
trigger,
}),
[
trigger,
showTrigger,
filter,
inputProps,
removeInput,
]
);
const contextValue: InlineComboboxContextValue = React.useMemo(
() => ({
filter,
inputProps,
inputRef,
removeInput,
setHasEmpty,
showTrigger,
trigger,
}),
[trigger, showTrigger, filter, inputProps, removeInput]
);
const store = useComboboxStore({
// open: ,
setValue: (newValue) => React.startTransition(() => setValue(newValue)),
});
const store = useComboboxStore({
// open: ,
setValue: (newValue) => React.startTransition(() => setValue(newValue)),
});
const items = store.useState('items');
const items = store.useState("items");
/**
* If there is no active ID and the list of items changes, select the first
* item.
*/
React.useEffect(() => {
if (items.length === 0) return;
/**
* If there is no active ID and the list of items changes, select the first
* item.
*/
React.useEffect(() => {
if (items.length === 0) return;
if (!store.getState().activeId) {
store.setActiveId(store.first());
}
}, [items, store]);
if (!store.getState().activeId) {
store.setActiveId(store.first());
}
}, [items, store]);
return (
<span contentEditable={false}>
<ComboboxProvider
open={
(items.length > 0 || hasEmpty) &&
(!hideWhenNoValue || value.length > 0)
}
store={store}
>
<InlineComboboxContext.Provider value={contextValue}>
{children}
</InlineComboboxContext.Provider>
</ComboboxProvider>
</span>
);
return (
<span contentEditable={false}>
<ComboboxProvider
open={(items.length > 0 || hasEmpty) && (!hideWhenNoValue || value.length > 0)}
store={store}
>
<InlineComboboxContext.Provider value={contextValue}>
{children}
</InlineComboboxContext.Provider>
</ComboboxProvider>
</span>
);
};
const InlineComboboxInput = ({
className,
ref: propRef,
...props
className,
ref: propRef,
...props
}: React.HTMLAttributes<HTMLInputElement> & {
ref?: React.RefObject<HTMLInputElement | null>;
ref?: React.RefObject<HTMLInputElement | null>;
}) => {
const {
inputProps,
inputRef: contextRef,
showTrigger,
trigger,
} = React.useContext(InlineComboboxContext);
const {
inputProps,
inputRef: contextRef,
showTrigger,
trigger,
} = React.useContext(InlineComboboxContext);
const store = useRequiredComboboxContext();
const value = store.useState('value');
const store = useRequiredComboboxContext();
const value = store.useState("value");
const ref = useComposedRef(propRef, contextRef);
const ref = useComposedRef(propRef, contextRef);
/**
* To create an auto-resizing input, we render a visually hidden span
* containing the input value and position the input element on top of it.
* This works well for all cases except when input exceeds the width of the
* container.
*/
/**
* To create an auto-resizing input, we render a visually hidden span
* containing the input value and position the input element on top of it.
* This works well for all cases except when input exceeds the width of the
* container.
*/
return (
<>
{showTrigger && trigger}
return (
<>
{showTrigger && trigger}
<span className="relative min-h-[1lh]">
<span
className="invisible overflow-hidden text-nowrap"
aria-hidden="true"
>
{value || '\u200B'}
</span>
<span className="relative min-h-[1lh]">
<span className="invisible overflow-hidden text-nowrap" aria-hidden="true">
{value || "\u200B"}
</span>
<Combobox
ref={ref}
className={cn(
'absolute top-0 left-0 size-full bg-transparent outline-none',
className
)}
value={value}
autoSelect
{...inputProps}
{...props}
/>
</span>
</>
);
<Combobox
ref={ref}
className={cn("absolute top-0 left-0 size-full bg-transparent outline-none", className)}
value={value}
autoSelect
{...inputProps}
{...props}
/>
</span>
</>
);
};
InlineComboboxInput.displayName = 'InlineComboboxInput';
InlineComboboxInput.displayName = "InlineComboboxInput";
const InlineComboboxContent: typeof ComboboxPopover = ({
className,
...props
}) => {
// Portal prevents CSS from leaking into popover
const store = useComboboxContext();
const InlineComboboxContent: typeof ComboboxPopover = ({ className, ...props }) => {
// Portal prevents CSS from leaking into popover
const store = useComboboxContext();
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (!store) return;
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (!store) return;
const state = store.getState();
const { items, activeId } = state;
const state = store.getState();
const { items, activeId } = state;
if (!items.length) return;
if (!items.length) return;
const currentIndex = items.findIndex((item) => item.id === activeId);
const currentIndex = items.findIndex((item) => item.id === activeId);
if (event.key === 'ArrowUp' && currentIndex <= 0) {
event.preventDefault();
store.setActiveId(store.last());
} else if (event.key === 'ArrowDown' && currentIndex >= items.length - 1) {
event.preventDefault();
store.setActiveId(store.first());
}
}
if (event.key === "ArrowUp" && currentIndex <= 0) {
event.preventDefault();
store.setActiveId(store.last());
} else if (event.key === "ArrowDown" && currentIndex >= items.length - 1) {
event.preventDefault();
store.setActiveId(store.first());
}
}
return (
<Portal>
<ComboboxPopover
className={cn(
'z-500 max-h-[288px] w-[300px] overflow-y-auto rounded-md bg-popover shadow-md',
className
)}
onKeyDownCapture={handleKeyDown}
{...props}
/>
</Portal>
);
return (
<Portal>
<ComboboxPopover
className={cn(
"z-500 max-h-[288px] w-[300px] overflow-y-auto rounded-md bg-popover shadow-md",
className
)}
onKeyDownCapture={handleKeyDown}
{...props}
/>
</Portal>
);
};
const comboboxItemVariants = cva(
'relative mx-1 flex h-[28px] select-none items-center rounded-sm px-2 text-foreground text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
defaultVariants: {
interactive: true,
},
variants: {
interactive: {
false: '',
true: 'cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground dark:hover:bg-neutral-700 dark:data-[active-item=true]:bg-neutral-700',
},
},
}
"relative mx-1 flex h-[28px] select-none items-center rounded-sm px-2 text-foreground text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
defaultVariants: {
interactive: true,
},
variants: {
interactive: {
false: "",
true: "cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground dark:hover:bg-neutral-700 dark:data-[active-item=true]:bg-neutral-700",
},
},
}
);
const InlineComboboxItem = ({
className,
focusEditor = true,
group,
keywords,
label,
onClick,
...props
className,
focusEditor = true,
group,
keywords,
label,
onClick,
...props
}: {
focusEditor?: boolean;
group?: string;
keywords?: string[];
label?: string;
focusEditor?: boolean;
group?: string;
keywords?: string[];
label?: string;
} & ComboboxItemProps &
Required<Pick<ComboboxItemProps, 'value'>>) => {
const { value } = props;
Required<Pick<ComboboxItemProps, "value">>) => {
const { value } = props;
const { filter, removeInput } = React.useContext(InlineComboboxContext);
const { filter, removeInput } = React.useContext(InlineComboboxContext);
const store = useRequiredComboboxContext();
const store = useRequiredComboboxContext();
// Always call hook unconditionally; only use value if filter is active
const storeValue = store.useState('value');
const search = filter ? storeValue : '';
// Always call hook unconditionally; only use value if filter is active
const storeValue = store.useState("value");
const search = filter ? storeValue : "";
const visible = React.useMemo(
() =>
!filter || filter({ group, keywords, label, value }, search),
[filter, group, keywords, label, value, search]
);
const visible = React.useMemo(
() => !filter || filter({ group, keywords, label, value }, search),
[filter, group, keywords, label, value, search]
);
if (!visible) return null;
if (!visible) return null;
return (
<ComboboxItem
className={cn(comboboxItemVariants(), className)}
onClick={(event) => {
removeInput(focusEditor);
onClick?.(event);
}}
{...props}
/>
);
return (
<ComboboxItem
className={cn(comboboxItemVariants(), className)}
onClick={(event) => {
removeInput(focusEditor);
onClick?.(event);
}}
{...props}
/>
);
};
const InlineComboboxEmpty = ({
children,
className,
}: React.HTMLAttributes<HTMLDivElement>) => {
const { setHasEmpty } = React.useContext(InlineComboboxContext);
const store = useRequiredComboboxContext();
const items = store.useState('items');
const InlineComboboxEmpty = ({ children, className }: React.HTMLAttributes<HTMLDivElement>) => {
const { setHasEmpty } = React.useContext(InlineComboboxContext);
const store = useRequiredComboboxContext();
const items = store.useState("items");
React.useEffect(() => {
setHasEmpty(true);
React.useEffect(() => {
setHasEmpty(true);
return () => {
setHasEmpty(false);
};
}, [setHasEmpty]);
return () => {
setHasEmpty(false);
};
}, [setHasEmpty]);
if (items.length > 0) return null;
if (items.length > 0) return null;
return (
<div
className={cn(comboboxItemVariants({ interactive: false }), className)}
>
{children}
</div>
);
return (
<div className={cn(comboboxItemVariants({ interactive: false }), className)}>{children}</div>
);
};
const InlineComboboxRow = ComboboxRow;
function InlineComboboxGroup({
className,
...props
}: React.ComponentProps<typeof ComboboxGroup>) {
return (
<ComboboxGroup
{...props}
className={cn(
'hidden not-last:border-b py-1.5 [&:has([role=option])]:block',
className
)}
/>
);
function InlineComboboxGroup({ className, ...props }: React.ComponentProps<typeof ComboboxGroup>) {
return (
<ComboboxGroup
{...props}
className={cn("hidden not-last:border-b py-1.5 [&:has([role=option])]:block", className)}
/>
);
}
function InlineComboboxGroupLabel({
className,
...props
className,
...props
}: React.ComponentProps<typeof ComboboxGroupLabel>) {
return (
<ComboboxGroupLabel
{...props}
className={cn(
'mt-1.5 mb-2 px-3 font-medium text-muted-foreground text-xs',
className
)}
/>
);
return (
<ComboboxGroupLabel
{...props}
className={cn("mt-1.5 mb-2 px-3 font-medium text-muted-foreground text-xs", className)}
/>
);
}
export {
InlineCombobox,
InlineComboboxContent,
InlineComboboxEmpty,
InlineComboboxGroup,
InlineComboboxGroupLabel,
InlineComboboxInput,
InlineComboboxItem,
InlineComboboxRow,
InlineCombobox,
InlineComboboxContent,
InlineComboboxEmpty,
InlineComboboxGroup,
InlineComboboxGroupLabel,
InlineComboboxInput,
InlineComboboxItem,
InlineComboboxRow,
};

View file

@ -1,229 +1,225 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
import type { DropdownMenuProps } from "@radix-ui/react-dropdown-menu";
import {
ChevronRightIcon,
FileCodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
InfoIcon,
ListIcon,
ListOrderedIcon,
MinusIcon,
PilcrowIcon,
PlusIcon,
QuoteIcon,
RadicalIcon,
SquareIcon,
SubscriptIcon,
SuperscriptIcon,
TableIcon,
} from 'lucide-react';
import { KEYS } from 'platejs';
import { type PlateEditor, useEditorRef } from 'platejs/react';
ChevronRightIcon,
FileCodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
InfoIcon,
ListIcon,
ListOrderedIcon,
MinusIcon,
PilcrowIcon,
PlusIcon,
QuoteIcon,
RadicalIcon,
SquareIcon,
SubscriptIcon,
SuperscriptIcon,
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';
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { insertBlock, insertInlineElement } from "@/components/editor/transforms";
import { ToolbarButton, ToolbarMenuGroup } from './toolbar';
import { ToolbarButton, ToolbarMenuGroup } from "./toolbar";
type Group = {
group: string;
items: Item[];
group: string;
items: Item[];
};
type Item = {
icon: React.ReactNode;
value: string;
onSelect: (editor: PlateEditor, value: string) => void;
focusEditor?: boolean;
label?: string;
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);
}
},
})),
},
{
group: 'Marks',
items: [
{
icon: <SuperscriptIcon />,
label: 'Superscript',
value: KEYS.sup,
},
{
icon: <SubscriptIcon />,
label: 'Subscript',
value: KEYS.sub,
},
].map((item) => ({
...item,
onSelect: (editor: PlateEditor, value: string) => {
editor.tf.toggleMark(value, {
remove: value === KEYS.sup ? KEYS.sub : KEYS.sup,
});
},
})),
},
{
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);
}
},
})),
},
{
group: "Marks",
items: [
{
icon: <SuperscriptIcon />,
label: "Superscript",
value: KEYS.sup,
},
{
icon: <SubscriptIcon />,
label: "Subscript",
value: KEYS.sub,
},
].map((item) => ({
...item,
onSelect: (editor: PlateEditor, value: string) => {
editor.tf.toggleMark(value, {
remove: value === KEYS.sup ? KEYS.sub : KEYS.sup,
});
},
})),
},
];
export function InsertToolbarButton(props: DropdownMenuProps) {
const editor = useEditorRef();
const [open, setOpen] = React.useState(false);
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>
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false} {...props}>
<DropdownMenuTrigger asChild>
<ToolbarButton pressed={open} tooltip="Insert" isDropdown>
<PlusIcon />
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="z-[100] flex max-h-[60vh] min-w-0 flex-col overflow-y-auto dark:bg-neutral-800 dark:border dark:border-neutral-700"
align="start"
>
{groups.map(({ group, items }) => (
<React.Fragment key={group}>
<ToolbarMenuGroup label={group}>
{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 dark:text-white 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>
);
<DropdownMenuContent
className="z-[100] flex max-h-[60vh] min-w-0 flex-col overflow-y-auto dark:bg-neutral-800 dark:border dark:border-neutral-700"
align="start"
>
{groups.map(({ group, items }) => (
<React.Fragment key={group}>
<ToolbarMenuGroup label={group}>
{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 dark:text-white 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

@ -1,32 +1,32 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { TLinkElement } from 'platejs';
import type { PlateElementProps } from 'platejs/react';
import type { TLinkElement } from "platejs";
import type { PlateElementProps } from "platejs/react";
import { getLinkAttributes } from '@platejs/link';
import { PlateElement } from 'platejs/react';
import { getLinkAttributes } from "@platejs/link";
import { PlateElement } from "platejs/react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
export function LinkElement(props: PlateElementProps<TLinkElement>) {
return (
<PlateElement
{...props}
as="a"
className={cn(
'font-medium text-blue-600 underline decoration-blue-600 underline-offset-4 hover:text-blue-800 dark:text-blue-400 dark:decoration-blue-400 dark:hover:text-blue-300'
)}
attributes={{
...props.attributes,
...getLinkAttributes(props.editor, props.element),
onMouseOver: (e) => {
e.stopPropagation();
},
}}
>
{props.children}
</PlateElement>
);
return (
<PlateElement
{...props}
as="a"
className={cn(
"font-medium text-blue-600 underline decoration-blue-600 underline-offset-4 hover:text-blue-800 dark:text-blue-400 dark:decoration-blue-400 dark:hover:text-blue-300"
)}
attributes={{
...props.attributes,
...getLinkAttributes(props.editor, props.element),
onMouseOver: (e) => {
e.stopPropagation();
},
}}
>
{props.children}
</PlateElement>
);
}

View file

@ -1,24 +1,19 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import {
useLinkToolbarButton,
useLinkToolbarButtonState,
} from '@platejs/link/react';
import { Link } from 'lucide-react';
import { useLinkToolbarButton, useLinkToolbarButtonState } from "@platejs/link/react";
import { Link } from "lucide-react";
import { ToolbarButton } from './toolbar';
import { ToolbarButton } from "./toolbar";
export function LinkToolbarButton(
props: React.ComponentProps<typeof ToolbarButton>
) {
const state = useLinkToolbarButtonState();
const { props: buttonProps } = useLinkToolbarButton(state);
export function LinkToolbarButton(props: React.ComponentProps<typeof ToolbarButton>) {
const state = useLinkToolbarButtonState();
const { props: buttonProps } = useLinkToolbarButton(state);
return (
<ToolbarButton tooltip="Link" {...props} {...buttonProps} data-plate-focus>
<Link />
</ToolbarButton>
);
return (
<ToolbarButton tooltip="Link" {...props} {...buttonProps} data-plate-focus>
<Link />
</ToolbarButton>
);
}

View file

@ -1,208 +1,196 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { TLinkElement } from 'platejs';
import type { TLinkElement } from "platejs";
import { type UseVirtualFloatingOptions, flip, offset } from "@platejs/floating";
import { getLinkAttributes } from "@platejs/link";
import {
type UseVirtualFloatingOptions,
flip,
offset,
} from '@platejs/floating';
import { getLinkAttributes } from '@platejs/link';
type LinkFloatingToolbarState,
FloatingLinkUrlInput,
useFloatingLinkEdit,
useFloatingLinkEditState,
useFloatingLinkInsert,
useFloatingLinkInsertState,
} from "@platejs/link/react";
import { cva } from "class-variance-authority";
import { ExternalLink, Link, Text, Unlink } from "lucide-react";
import { KEYS } from "platejs";
import {
type LinkFloatingToolbarState,
FloatingLinkUrlInput,
useFloatingLinkEdit,
useFloatingLinkEditState,
useFloatingLinkInsert,
useFloatingLinkInsertState,
} from '@platejs/link/react';
import { cva } from 'class-variance-authority';
import { ExternalLink, Link, Text, Unlink } from 'lucide-react';
import { KEYS } from 'platejs';
import {
useEditorRef,
useEditorSelection,
useFormInputProps,
usePluginOption,
} from 'platejs/react';
useEditorRef,
useEditorSelection,
useFormInputProps,
usePluginOption,
} from "platejs/react";
import { buttonVariants } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { buttonVariants } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
const popoverVariants = cva(
'z-50 w-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden'
"z-50 w-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden"
);
const inputVariants = cva(
'flex h-[28px] w-full rounded-md border-none bg-transparent px-1.5 py-1 text-base placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-transparent md:text-sm'
"flex h-[28px] w-full rounded-md border-none bg-transparent px-1.5 py-1 text-base placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-transparent md:text-sm"
);
export function LinkFloatingToolbar({
state,
}: {
state?: LinkFloatingToolbarState;
}) {
const activeCommentId = usePluginOption({ key: KEYS.comment }, 'activeId');
const activeSuggestionId = usePluginOption(
{ key: KEYS.suggestion },
'activeId'
);
export function LinkFloatingToolbar({ state }: { state?: LinkFloatingToolbarState }) {
const activeCommentId = usePluginOption({ key: KEYS.comment }, "activeId");
const activeSuggestionId = usePluginOption({ key: KEYS.suggestion }, "activeId");
const floatingOptions: UseVirtualFloatingOptions = React.useMemo(
() => ({
middleware: [
offset(8),
flip({
fallbackPlacements: ['bottom-end', 'top-start', 'top-end'],
padding: 12,
}),
],
placement:
activeSuggestionId || activeCommentId ? 'top-start' : 'bottom-start',
}),
[activeCommentId, activeSuggestionId]
);
const floatingOptions: UseVirtualFloatingOptions = React.useMemo(
() => ({
middleware: [
offset(8),
flip({
fallbackPlacements: ["bottom-end", "top-start", "top-end"],
padding: 12,
}),
],
placement: activeSuggestionId || activeCommentId ? "top-start" : "bottom-start",
}),
[activeCommentId, activeSuggestionId]
);
const insertState = useFloatingLinkInsertState({
...state,
floatingOptions: {
...floatingOptions,
...state?.floatingOptions,
},
});
const {
hidden,
props: insertProps,
ref: insertRef,
textInputProps,
} = useFloatingLinkInsert(insertState);
const insertState = useFloatingLinkInsertState({
...state,
floatingOptions: {
...floatingOptions,
...state?.floatingOptions,
},
});
const {
hidden,
props: insertProps,
ref: insertRef,
textInputProps,
} = useFloatingLinkInsert(insertState);
const editState = useFloatingLinkEditState({
...state,
floatingOptions: {
...floatingOptions,
...state?.floatingOptions,
},
});
const {
editButtonProps,
props: editProps,
ref: editRef,
unlinkButtonProps,
} = useFloatingLinkEdit(editState);
const inputProps = useFormInputProps({
preventDefaultOnEnterKeydown: true,
});
const editState = useFloatingLinkEditState({
...state,
floatingOptions: {
...floatingOptions,
...state?.floatingOptions,
},
});
const {
editButtonProps,
props: editProps,
ref: editRef,
unlinkButtonProps,
} = useFloatingLinkEdit(editState);
const inputProps = useFormInputProps({
preventDefaultOnEnterKeydown: true,
});
if (hidden) return null;
if (hidden) return null;
const input = (
<div className="flex w-[330px] flex-col" {...inputProps}>
<div className="flex items-center">
<div className="flex items-center pr-1 pl-2 text-muted-foreground">
<Link className="size-4" />
</div>
const input = (
<div className="flex w-[330px] flex-col" {...inputProps}>
<div className="flex items-center">
<div className="flex items-center pr-1 pl-2 text-muted-foreground">
<Link className="size-4" />
</div>
<FloatingLinkUrlInput
className={inputVariants()}
placeholder="Paste link"
data-plate-focus
/>
</div>
<Separator className="my-1" />
<div className="flex items-center">
<div className="flex items-center pr-1 pl-2 text-muted-foreground">
<Text className="size-4" />
</div>
<input
className={inputVariants()}
placeholder="Text to display"
data-plate-focus
{...textInputProps}
/>
</div>
</div>
);
<FloatingLinkUrlInput
className={inputVariants()}
placeholder="Paste link"
data-plate-focus
/>
</div>
<Separator className="my-1" />
<div className="flex items-center">
<div className="flex items-center pr-1 pl-2 text-muted-foreground">
<Text className="size-4" />
</div>
<input
className={inputVariants()}
placeholder="Text to display"
data-plate-focus
{...textInputProps}
/>
</div>
</div>
);
const editContent = editState.isEditing ? (
input
) : (
<div className="box-content flex items-center">
<button
className={buttonVariants({ size: 'sm', variant: 'ghost' })}
type="button"
{...editButtonProps}
>
Edit link
</button>
const editContent = editState.isEditing ? (
input
) : (
<div className="box-content flex items-center">
<button
className={buttonVariants({ size: "sm", variant: "ghost" })}
type="button"
{...editButtonProps}
>
Edit link
</button>
<Separator orientation="vertical" />
<Separator orientation="vertical" />
<LinkOpenButton />
<LinkOpenButton />
<Separator orientation="vertical" />
<Separator orientation="vertical" />
<button
className={buttonVariants({
size: 'sm',
variant: 'ghost',
})}
type="button"
{...unlinkButtonProps}
>
<Unlink width={18} />
</button>
</div>
);
<button
className={buttonVariants({
size: "sm",
variant: "ghost",
})}
type="button"
{...unlinkButtonProps}
>
<Unlink width={18} />
</button>
</div>
);
return (
<>
<div ref={insertRef} className={popoverVariants()} {...insertProps}>
{input}
</div>
return (
<>
<div ref={insertRef} className={popoverVariants()} {...insertProps}>
{input}
</div>
<div ref={editRef} className={popoverVariants()} {...editProps}>
{editContent}
</div>
</>
);
<div ref={editRef} className={popoverVariants()} {...editProps}>
{editContent}
</div>
</>
);
}
function LinkOpenButton() {
const editor = useEditorRef();
const selection = useEditorSelection();
const editor = useEditorRef();
const selection = useEditorSelection();
const attributes = React.useMemo(
() => {
const entry = editor.api.node<TLinkElement>({
match: { type: editor.getType(KEYS.link) },
});
if (!entry) {
return {};
}
const [element] = entry;
return getLinkAttributes(editor, element);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[editor, selection]
);
const attributes = React.useMemo(
() => {
const entry = editor.api.node<TLinkElement>({
match: { type: editor.getType(KEYS.link) },
});
if (!entry) {
return {};
}
const [element] = entry;
return getLinkAttributes(editor, element);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[editor, selection]
);
return (
<a
{...attributes}
className={buttonVariants({
size: 'sm',
variant: 'ghost',
})}
onMouseOver={(e) => {
e.stopPropagation();
}}
aria-label="Open link in a new tab"
target="_blank"
>
<ExternalLink width={18} />
</a>
);
return (
<a
{...attributes}
className={buttonVariants({
size: "sm",
variant: "ghost",
})}
onMouseOver={(e) => {
e.stopPropagation();
}}
aria-label="Open link in a new tab"
target="_blank"
>
<ExternalLink width={18} />
</a>
);
}

View file

@ -1,29 +1,29 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import { useMarkToolbarButton, useMarkToolbarButtonState } from 'platejs/react';
import { useMarkToolbarButton, useMarkToolbarButtonState } from "platejs/react";
import { ToolbarButton } from './toolbar';
import { ToolbarButton } from "./toolbar";
export function MarkToolbarButton({
clear,
nodeType,
...props
clear,
nodeType,
...props
}: React.ComponentProps<typeof ToolbarButton> & {
nodeType: string;
clear?: string[] | string;
nodeType: string;
clear?: string[] | string;
}) {
const state = useMarkToolbarButtonState({ clear, nodeType });
const { props: buttonProps } = useMarkToolbarButton(state);
const state = useMarkToolbarButtonState({ clear, nodeType });
const { props: buttonProps } = useMarkToolbarButton(state);
return (
<ToolbarButton
{...props}
{...buttonProps}
onMouseDown={(e: React.MouseEvent) => {
e.preventDefault();
}}
/>
);
return (
<ToolbarButton
{...props}
{...buttonProps}
onMouseDown={(e: React.MouseEvent) => {
e.preventDefault();
}}
/>
);
}

View file

@ -1,19 +1,19 @@
'use client';
"use client";
import { BookOpenIcon, PenLineIcon } from 'lucide-react';
import { usePlateState } from 'platejs/react';
import { BookOpenIcon, PenLineIcon } from "lucide-react";
import { usePlateState } from "platejs/react";
import { ToolbarButton } from './toolbar';
import { ToolbarButton } from "./toolbar";
export function ModeToolbarButton() {
const [readOnly, setReadOnly] = usePlateState('readOnly');
const [readOnly, setReadOnly] = usePlateState("readOnly");
return (
<ToolbarButton
tooltip={readOnly ? 'Click to edit' : 'Click to view'}
onClick={() => setReadOnly(!readOnly)}
>
{readOnly ? <BookOpenIcon /> : <PenLineIcon />}
</ToolbarButton>
);
return (
<ToolbarButton
tooltip={readOnly ? "Click to edit" : "Click to view"}
onClick={() => setReadOnly(!readOnly)}
>
{readOnly ? <BookOpenIcon /> : <PenLineIcon />}
</ToolbarButton>
);
}

View file

@ -1,17 +1,17 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { PlateElementProps } from 'platejs/react';
import type { PlateElementProps } from "platejs/react";
import { PlateElement } from 'platejs/react';
import { PlateElement } from "platejs/react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
export function ParagraphElement(props: PlateElementProps) {
return (
<PlateElement {...props} className={cn('m-0 px-0 py-1')}>
{props.children}
</PlateElement>
);
return (
<PlateElement {...props} className={cn("m-0 px-0 py-1")}>
{props.children}
</PlateElement>
);
}

View file

@ -1,89 +1,79 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { VariantProps } from 'class-variance-authority';
import type { VariantProps } from "class-variance-authority";
import {
type ResizeHandle as ResizeHandlePrimitive,
Resizable as ResizablePrimitive,
useResizeHandle,
useResizeHandleState,
} from '@platejs/resizable';
import { cva } from 'class-variance-authority';
type ResizeHandle as ResizeHandlePrimitive,
Resizable as ResizablePrimitive,
useResizeHandle,
useResizeHandleState,
} from "@platejs/resizable";
import { cva } from "class-variance-authority";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
export const mediaResizeHandleVariants = cva(
cn(
'top-0 flex w-6 select-none flex-col justify-center',
"after:flex after:h-16 after:w-[3px] after:rounded-[6px] after:bg-ring after:opacity-0 after:content-['_'] group-hover:after:opacity-100"
),
{
variants: {
direction: {
left: '-left-3 -ml-3 pl-3',
right: '-right-3 -mr-3 items-end pr-3',
},
},
}
cn(
"top-0 flex w-6 select-none flex-col justify-center",
"after:flex after:h-16 after:w-[3px] after:rounded-[6px] after:bg-ring after:opacity-0 after:content-['_'] group-hover:after:opacity-100"
),
{
variants: {
direction: {
left: "-left-3 -ml-3 pl-3",
right: "-right-3 -mr-3 items-end pr-3",
},
},
}
);
const resizeHandleVariants = cva('absolute z-40', {
variants: {
direction: {
bottom: 'w-full cursor-row-resize',
left: 'h-full cursor-col-resize',
right: 'h-full cursor-col-resize',
top: 'w-full cursor-row-resize',
},
},
const resizeHandleVariants = cva("absolute z-40", {
variants: {
direction: {
bottom: "w-full cursor-row-resize",
left: "h-full cursor-col-resize",
right: "h-full cursor-col-resize",
top: "w-full cursor-row-resize",
},
},
});
export function ResizeHandle({
className,
options,
...props
}: React.ComponentProps<typeof ResizeHandlePrimitive> &
VariantProps<typeof resizeHandleVariants>) {
const state = useResizeHandleState(options ?? {});
const resizeHandle = useResizeHandle(state);
className,
options,
...props
}: React.ComponentProps<typeof ResizeHandlePrimitive> & VariantProps<typeof resizeHandleVariants>) {
const state = useResizeHandleState(options ?? {});
const resizeHandle = useResizeHandle(state);
if (state.readOnly) return null;
if (state.readOnly) return null;
return (
<div
className={cn(
resizeHandleVariants({ direction: options?.direction }),
className
)}
data-resizing={state.isResizing}
{...resizeHandle.props}
{...props}
/>
);
return (
<div
className={cn(resizeHandleVariants({ direction: options?.direction }), className)}
data-resizing={state.isResizing}
{...resizeHandle.props}
{...props}
/>
);
}
const resizableVariants = cva('', {
variants: {
align: {
center: 'mx-auto',
left: 'mr-auto',
right: 'ml-auto',
},
},
const resizableVariants = cva("", {
variants: {
align: {
center: "mx-auto",
left: "mr-auto",
right: "ml-auto",
},
},
});
export function Resizable({
align,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive> &
VariantProps<typeof resizableVariants>) {
return (
<ResizablePrimitive
{...props}
className={cn(resizableVariants({ align }), className)}
/>
);
align,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive> & VariantProps<typeof resizableVariants>) {
return <ResizablePrimitive {...props} className={cn(resizableVariants({ align }), className)} />;
}

View file

@ -1,28 +1,28 @@
"use client"
"use client";
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import * as React from "react";
import { Separator as SeparatorPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
);
}
export { Separator }
export { Separator };

View file

@ -1,223 +1,217 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { PlateElementProps } from 'platejs/react';
import type { PlateElementProps } from "platejs/react";
import { SlashInputPlugin } from '@platejs/slash-command/react';
import { SlashInputPlugin } from "@platejs/slash-command/react";
import {
ChevronRightIcon,
Code2Icon,
FileCodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
InfoIcon,
ListIcon,
ListOrderedIcon,
MinusIcon,
PilcrowIcon,
QuoteIcon,
RadicalIcon,
SquareIcon,
TableIcon,
} from 'lucide-react';
import { KEYS } from 'platejs';
import { PlateElement, useEditorRef } from 'platejs/react';
ChevronRightIcon,
Code2Icon,
FileCodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
InfoIcon,
ListIcon,
ListOrderedIcon,
MinusIcon,
PilcrowIcon,
QuoteIcon,
RadicalIcon,
SquareIcon,
TableIcon,
} from "lucide-react";
import { KEYS } from "platejs";
import { PlateElement, useEditorRef } from "platejs/react";
import {
InlineCombobox,
InlineComboboxContent,
InlineComboboxEmpty,
InlineComboboxGroup,
InlineComboboxGroupLabel,
InlineComboboxInput,
InlineComboboxItem,
} from '@/components/ui/inline-combobox';
import { insertBlock, insertInlineElement } from '@/components/editor/transforms';
InlineCombobox,
InlineComboboxContent,
InlineComboboxEmpty,
InlineComboboxGroup,
InlineComboboxGroupLabel,
InlineComboboxInput,
InlineComboboxItem,
} from "@/components/ui/inline-combobox";
import { insertBlock, insertInlineElement } from "@/components/editor/transforms";
interface SlashCommandItem {
icon: React.ReactNode;
keywords: string[];
label: string;
value: string;
onSelect: (editor: any) => void;
icon: React.ReactNode;
keywords: string[];
label: string;
value: string;
onSelect: (editor: any) => void;
}
const slashCommandGroups: { heading: string; items: SlashCommandItem[] }[] = [
{
heading: 'Basic Blocks',
items: [
{
icon: <PilcrowIcon />,
keywords: ['paragraph', 'text', 'plain'],
label: 'Text',
value: 'text',
onSelect: (editor) => insertBlock(editor, KEYS.p),
},
{
icon: <Heading1Icon />,
keywords: ['title', 'h1', 'heading'],
label: 'Heading 1',
value: 'heading1',
onSelect: (editor) => insertBlock(editor, 'h1'),
},
{
icon: <Heading2Icon />,
keywords: ['subtitle', 'h2', 'heading'],
label: 'Heading 2',
value: 'heading2',
onSelect: (editor) => insertBlock(editor, 'h2'),
},
{
icon: <Heading3Icon />,
keywords: ['subtitle', 'h3', 'heading'],
label: 'Heading 3',
value: 'heading3',
onSelect: (editor) => insertBlock(editor, 'h3'),
},
{
icon: <QuoteIcon />,
keywords: ['citation', 'blockquote'],
label: 'Quote',
value: 'quote',
onSelect: (editor) => insertBlock(editor, KEYS.blockquote),
},
{
icon: <MinusIcon />,
keywords: ['divider', 'separator', 'line'],
label: 'Divider',
value: 'divider',
onSelect: (editor) => insertBlock(editor, KEYS.hr),
},
],
},
{
heading: 'Lists',
items: [
{
icon: <ListIcon />,
keywords: ['unordered', 'ul', 'bullet'],
label: 'Bulleted list',
value: 'bulleted-list',
onSelect: (editor) => insertBlock(editor, KEYS.ul),
},
{
icon: <ListOrderedIcon />,
keywords: ['ordered', 'ol', 'numbered'],
label: 'Numbered list',
value: 'numbered-list',
onSelect: (editor) => insertBlock(editor, KEYS.ol),
},
{
icon: <SquareIcon />,
keywords: ['checklist', 'task', 'checkbox', 'todo'],
label: 'To-do list',
value: 'todo-list',
onSelect: (editor) => insertBlock(editor, KEYS.listTodo),
},
],
},
{
heading: 'Advanced',
items: [
{
icon: <TableIcon />,
keywords: ['table', 'grid'],
label: 'Table',
value: 'table',
onSelect: (editor) => insertBlock(editor, KEYS.table),
},
{
icon: <FileCodeIcon />,
keywords: ['code', 'codeblock', 'snippet'],
label: 'Code block',
value: 'code-block',
onSelect: (editor) => insertBlock(editor, KEYS.codeBlock),
},
{
icon: <InfoIcon />,
keywords: ['callout', 'note', 'info', 'warning', 'tip'],
label: 'Callout',
value: 'callout',
onSelect: (editor) => insertBlock(editor, KEYS.callout),
},
{
icon: <ChevronRightIcon />,
keywords: ['toggle', 'collapsible', 'expand'],
label: 'Toggle',
value: 'toggle',
onSelect: (editor) => insertBlock(editor, KEYS.toggle),
},
{
icon: <RadicalIcon />,
keywords: ['equation', 'math', 'formula', 'latex'],
label: 'Equation',
value: 'equation',
onSelect: (editor) => insertInlineElement(editor, KEYS.equation),
},
],
},
{
heading: 'Inline',
items: [
{
icon: <Code2Icon />,
keywords: ['link', 'url', 'href'],
label: 'Link',
value: 'link',
onSelect: (editor) => insertInlineElement(editor, KEYS.link),
},
],
},
{
heading: "Basic Blocks",
items: [
{
icon: <PilcrowIcon />,
keywords: ["paragraph", "text", "plain"],
label: "Text",
value: "text",
onSelect: (editor) => insertBlock(editor, KEYS.p),
},
{
icon: <Heading1Icon />,
keywords: ["title", "h1", "heading"],
label: "Heading 1",
value: "heading1",
onSelect: (editor) => insertBlock(editor, "h1"),
},
{
icon: <Heading2Icon />,
keywords: ["subtitle", "h2", "heading"],
label: "Heading 2",
value: "heading2",
onSelect: (editor) => insertBlock(editor, "h2"),
},
{
icon: <Heading3Icon />,
keywords: ["subtitle", "h3", "heading"],
label: "Heading 3",
value: "heading3",
onSelect: (editor) => insertBlock(editor, "h3"),
},
{
icon: <QuoteIcon />,
keywords: ["citation", "blockquote"],
label: "Quote",
value: "quote",
onSelect: (editor) => insertBlock(editor, KEYS.blockquote),
},
{
icon: <MinusIcon />,
keywords: ["divider", "separator", "line"],
label: "Divider",
value: "divider",
onSelect: (editor) => insertBlock(editor, KEYS.hr),
},
],
},
{
heading: "Lists",
items: [
{
icon: <ListIcon />,
keywords: ["unordered", "ul", "bullet"],
label: "Bulleted list",
value: "bulleted-list",
onSelect: (editor) => insertBlock(editor, KEYS.ul),
},
{
icon: <ListOrderedIcon />,
keywords: ["ordered", "ol", "numbered"],
label: "Numbered list",
value: "numbered-list",
onSelect: (editor) => insertBlock(editor, KEYS.ol),
},
{
icon: <SquareIcon />,
keywords: ["checklist", "task", "checkbox", "todo"],
label: "To-do list",
value: "todo-list",
onSelect: (editor) => insertBlock(editor, KEYS.listTodo),
},
],
},
{
heading: "Advanced",
items: [
{
icon: <TableIcon />,
keywords: ["table", "grid"],
label: "Table",
value: "table",
onSelect: (editor) => insertBlock(editor, KEYS.table),
},
{
icon: <FileCodeIcon />,
keywords: ["code", "codeblock", "snippet"],
label: "Code block",
value: "code-block",
onSelect: (editor) => insertBlock(editor, KEYS.codeBlock),
},
{
icon: <InfoIcon />,
keywords: ["callout", "note", "info", "warning", "tip"],
label: "Callout",
value: "callout",
onSelect: (editor) => insertBlock(editor, KEYS.callout),
},
{
icon: <ChevronRightIcon />,
keywords: ["toggle", "collapsible", "expand"],
label: "Toggle",
value: "toggle",
onSelect: (editor) => insertBlock(editor, KEYS.toggle),
},
{
icon: <RadicalIcon />,
keywords: ["equation", "math", "formula", "latex"],
label: "Equation",
value: "equation",
onSelect: (editor) => insertInlineElement(editor, KEYS.equation),
},
],
},
{
heading: "Inline",
items: [
{
icon: <Code2Icon />,
keywords: ["link", "url", "href"],
label: "Link",
value: "link",
onSelect: (editor) => insertInlineElement(editor, KEYS.link),
},
],
},
];
export function SlashInputElement({
children,
...props
}: PlateElementProps) {
const editor = useEditorRef();
export function SlashInputElement({ children, ...props }: PlateElementProps) {
const editor = useEditorRef();
return (
<PlateElement {...props} as="span">
<InlineCombobox
element={props.element}
trigger="/"
>
<InlineComboboxInput />
return (
<PlateElement {...props} as="span">
<InlineCombobox element={props.element} trigger="/">
<InlineComboboxInput />
<InlineComboboxContent className="dark:bg-neutral-800 dark:border dark:border-neutral-700">
<InlineComboboxEmpty>No results found.</InlineComboboxEmpty>
<InlineComboboxContent className="dark:bg-neutral-800 dark:border dark:border-neutral-700">
<InlineComboboxEmpty>No results found.</InlineComboboxEmpty>
{slashCommandGroups.map(({ heading, items }) => (
<InlineComboboxGroup key={heading}>
<InlineComboboxGroupLabel>{heading}</InlineComboboxGroupLabel>
{slashCommandGroups.map(({ heading, items }) => (
<InlineComboboxGroup key={heading}>
<InlineComboboxGroupLabel>{heading}</InlineComboboxGroupLabel>
{items.map(({ icon, keywords, label, value, onSelect }) => (
<InlineComboboxItem
key={value}
className="flex items-center gap-3 px-2 py-1.5"
keywords={keywords}
label={label}
value={value}
group={heading}
onClick={() => {
onSelect(editor);
editor.tf.focus();
}}
>
<span className="flex size-5 items-center justify-center text-muted-foreground">
{icon}
</span>
{label}
</InlineComboboxItem>
))}
</InlineComboboxGroup>
))}
</InlineComboboxContent>
</InlineCombobox>
{items.map(({ icon, keywords, label, value, onSelect }) => (
<InlineComboboxItem
key={value}
className="flex items-center gap-3 px-2 py-1.5"
keywords={keywords}
label={label}
value={value}
group={heading}
onClick={() => {
onSelect(editor);
editor.tf.focus();
}}
>
<span className="flex size-5 items-center justify-center text-muted-foreground">
{icon}
</span>
{label}
</InlineComboboxItem>
))}
</InlineComboboxGroup>
))}
</InlineComboboxContent>
</InlineCombobox>
{children}
</PlateElement>
);
{children}
</PlateElement>
);
}

File diff suppressed because it is too large Load diff

View file

@ -1,484 +1,439 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import { useDraggable, useDropLine } from '@platejs/dnd';
import { useDraggable, useDropLine } from "@platejs/dnd";
import { BlockSelectionPlugin, useBlockSelected } from "@platejs/selection/react";
import {
BlockSelectionPlugin,
useBlockSelected,
} from '@platejs/selection/react';
TablePlugin,
TableProvider,
useTableCellElement,
useTableCellElementResizable,
useTableElement,
useTableMergeState,
} from "@platejs/table/react";
import { PopoverAnchor } from "@radix-ui/react-popover";
import { cva } from "class-variance-authority";
import {
TablePlugin,
TableProvider,
useTableCellElement,
useTableCellElementResizable,
useTableElement,
useTableMergeState,
} from '@platejs/table/react';
import { PopoverAnchor } from '@radix-ui/react-popover';
import { cva } from 'class-variance-authority';
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
CombineIcon,
GripVertical,
SquareSplitHorizontalIcon,
Trash2Icon,
XIcon,
} from "lucide-react";
import {
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
CombineIcon,
GripVertical,
SquareSplitHorizontalIcon,
Trash2Icon,
XIcon,
} from 'lucide-react';
type TElement,
type TTableCellElement,
type TTableElement,
type TTableRowElement,
KEYS,
PathApi,
} from "platejs";
import {
type TElement,
type TTableCellElement,
type TTableElement,
type TTableRowElement,
KEYS,
PathApi,
} from 'platejs';
import {
type PlateElementProps,
PlateElement,
useComposedRef,
useEditorPlugin,
useEditorRef,
useEditorSelector,
useElement,
useFocusedLast,
usePluginOption,
useReadOnly,
useRemoveNodeButton,
useSelected,
withHOC,
} from 'platejs/react';
import { useElementSelector } from 'platejs/react';
type PlateElementProps,
PlateElement,
useComposedRef,
useEditorPlugin,
useEditorRef,
useEditorSelector,
useElement,
useFocusedLast,
usePluginOption,
useReadOnly,
useRemoveNodeButton,
useSelected,
withHOC,
} from "platejs/react";
import { useElementSelector } from "platejs/react";
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { blockSelectionVariants } from './block-selection';
import { ResizeHandle } from './resize-handle';
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
} from './toolbar';
import { blockSelectionVariants } from "./block-selection";
import { ResizeHandle } from "./resize-handle";
import { Toolbar, ToolbarButton, ToolbarGroup } from "./toolbar";
export const TableElement = withHOC(
TableProvider,
function TableElement({
children,
...props
}: PlateElementProps<TTableElement>) {
const readOnly = useReadOnly();
const isSelectionAreaVisible = usePluginOption(
BlockSelectionPlugin,
'isSelectionAreaVisible'
);
const hasControls = !readOnly && !isSelectionAreaVisible;
const {
isSelectingCell,
marginLeft,
props: tableProps,
} = useTableElement();
TableProvider,
function TableElement({ children, ...props }: PlateElementProps<TTableElement>) {
const readOnly = useReadOnly();
const isSelectionAreaVisible = usePluginOption(BlockSelectionPlugin, "isSelectionAreaVisible");
const hasControls = !readOnly && !isSelectionAreaVisible;
const { isSelectingCell, marginLeft, props: tableProps } = useTableElement();
const isSelectingTable = useBlockSelected(props.element.id as string);
const isSelectingTable = useBlockSelected(props.element.id as string);
const content = (
<PlateElement
{...props}
className={cn(
'overflow-x-auto py-5',
hasControls && '-ml-2 *:data-[slot=block-selection]:left-2'
)}
style={{ paddingLeft: marginLeft }}
>
<div className="group/table relative w-fit">
<table
className={cn(
'mr-0 ml-px table h-px w-full table-fixed border-collapse',
isSelectingCell && 'selection:bg-transparent'
)}
{...tableProps}
>
<tbody className="min-w-full">{children}</tbody>
</table>
const content = (
<PlateElement
{...props}
className={cn(
"overflow-x-auto py-5",
hasControls && "-ml-2 *:data-[slot=block-selection]:left-2"
)}
style={{ paddingLeft: marginLeft }}
>
<div className="group/table relative w-fit">
<table
className={cn(
"mr-0 ml-px table h-px w-full table-fixed border-collapse",
isSelectingCell && "selection:bg-transparent"
)}
{...tableProps}
>
<tbody className="min-w-full">{children}</tbody>
</table>
{isSelectingTable && (
<div className={blockSelectionVariants()} contentEditable={false} />
)}
</div>
</PlateElement>
);
{isSelectingTable && <div className={blockSelectionVariants()} contentEditable={false} />}
</div>
</PlateElement>
);
if (readOnly) {
return content;
}
if (readOnly) {
return content;
}
return <TableFloatingToolbar>{content}</TableFloatingToolbar>;
}
return <TableFloatingToolbar>{content}</TableFloatingToolbar>;
}
);
function TableFloatingToolbar({
children,
...props
}: React.ComponentProps<typeof PopoverContent>) {
const { tf } = useEditorPlugin(TablePlugin);
const selected = useSelected();
const element = useElement<TTableElement>();
const { props: buttonProps } = useRemoveNodeButton({ element });
const collapsedInside = useEditorSelector(
(editor) => selected && editor.api.isCollapsed(),
[selected]
);
const isFocusedLast = useFocusedLast();
function TableFloatingToolbar({ children, ...props }: React.ComponentProps<typeof PopoverContent>) {
const { tf } = useEditorPlugin(TablePlugin);
const selected = useSelected();
const element = useElement<TTableElement>();
const { props: buttonProps } = useRemoveNodeButton({ element });
const collapsedInside = useEditorSelector(
(editor) => selected && editor.api.isCollapsed(),
[selected]
);
const isFocusedLast = useFocusedLast();
const { canMerge, canSplit } = useTableMergeState();
const { canMerge, canSplit } = useTableMergeState();
return (
<Popover
open={isFocusedLast && (canMerge || canSplit || collapsedInside)}
modal={false}
>
<PopoverAnchor asChild>{children}</PopoverAnchor>
<PopoverContent
asChild
onOpenAutoFocus={(e) => e.preventDefault()}
contentEditable={false}
{...props}
>
<Toolbar
className="scrollbar-hide flex w-auto max-w-[80vw] flex-row overflow-x-auto rounded-md border bg-popover p-1 shadow-md print:hidden"
contentEditable={false}
>
<ToolbarGroup>
{canMerge && (
<ToolbarButton
onClick={() => tf.table.merge()}
onMouseDown={(e) => e.preventDefault()}
tooltip="Merge cells"
>
<CombineIcon />
</ToolbarButton>
)}
{canSplit && (
<ToolbarButton
onClick={() => tf.table.split()}
onMouseDown={(e) => e.preventDefault()}
tooltip="Split cell"
>
<SquareSplitHorizontalIcon />
</ToolbarButton>
)}
return (
<Popover open={isFocusedLast && (canMerge || canSplit || collapsedInside)} modal={false}>
<PopoverAnchor asChild>{children}</PopoverAnchor>
<PopoverContent
asChild
onOpenAutoFocus={(e) => e.preventDefault()}
contentEditable={false}
{...props}
>
<Toolbar
className="scrollbar-hide flex w-auto max-w-[80vw] flex-row overflow-x-auto rounded-md border bg-popover p-1 shadow-md print:hidden"
contentEditable={false}
>
<ToolbarGroup>
{canMerge && (
<ToolbarButton
onClick={() => tf.table.merge()}
onMouseDown={(e) => e.preventDefault()}
tooltip="Merge cells"
>
<CombineIcon />
</ToolbarButton>
)}
{canSplit && (
<ToolbarButton
onClick={() => tf.table.split()}
onMouseDown={(e) => e.preventDefault()}
tooltip="Split cell"
>
<SquareSplitHorizontalIcon />
</ToolbarButton>
)}
{collapsedInside && (
<ToolbarGroup>
<ToolbarButton tooltip="Delete table" {...buttonProps}>
<Trash2Icon />
</ToolbarButton>
</ToolbarGroup>
)}
</ToolbarGroup>
{collapsedInside && (
<ToolbarGroup>
<ToolbarButton tooltip="Delete table" {...buttonProps}>
<Trash2Icon />
</ToolbarButton>
</ToolbarGroup>
)}
</ToolbarGroup>
{collapsedInside && (
<ToolbarGroup>
<ToolbarButton
onClick={() => {
tf.insert.tableRow({ before: true });
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Insert row before"
>
<ArrowUp />
</ToolbarButton>
<ToolbarButton
onClick={() => {
tf.insert.tableRow();
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Insert row after"
>
<ArrowDown />
</ToolbarButton>
<ToolbarButton
onClick={() => {
tf.remove.tableRow();
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Delete row"
>
<XIcon />
</ToolbarButton>
</ToolbarGroup>
)}
{collapsedInside && (
<ToolbarGroup>
<ToolbarButton
onClick={() => {
tf.insert.tableRow({ before: true });
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Insert row before"
>
<ArrowUp />
</ToolbarButton>
<ToolbarButton
onClick={() => {
tf.insert.tableRow();
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Insert row after"
>
<ArrowDown />
</ToolbarButton>
<ToolbarButton
onClick={() => {
tf.remove.tableRow();
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Delete row"
>
<XIcon />
</ToolbarButton>
</ToolbarGroup>
)}
{collapsedInside && (
<ToolbarGroup>
<ToolbarButton
onClick={() => {
tf.insert.tableColumn({ before: true });
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Insert column before"
>
<ArrowLeft />
</ToolbarButton>
<ToolbarButton
onClick={() => {
tf.insert.tableColumn();
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Insert column after"
>
<ArrowRight />
</ToolbarButton>
<ToolbarButton
onClick={() => {
tf.remove.tableColumn();
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Delete column"
>
<XIcon />
</ToolbarButton>
</ToolbarGroup>
)}
</Toolbar>
</PopoverContent>
</Popover>
);
{collapsedInside && (
<ToolbarGroup>
<ToolbarButton
onClick={() => {
tf.insert.tableColumn({ before: true });
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Insert column before"
>
<ArrowLeft />
</ToolbarButton>
<ToolbarButton
onClick={() => {
tf.insert.tableColumn();
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Insert column after"
>
<ArrowRight />
</ToolbarButton>
<ToolbarButton
onClick={() => {
tf.remove.tableColumn();
}}
onMouseDown={(e) => e.preventDefault()}
tooltip="Delete column"
>
<XIcon />
</ToolbarButton>
</ToolbarGroup>
)}
</Toolbar>
</PopoverContent>
</Popover>
);
}
export function TableRowElement({
children,
...props
}: PlateElementProps<TTableRowElement>) {
const { element } = props;
const readOnly = useReadOnly();
const selected = useSelected();
const editor = useEditorRef();
const isSelectionAreaVisible = usePluginOption(
BlockSelectionPlugin,
'isSelectionAreaVisible'
);
const hasControls = !readOnly && !isSelectionAreaVisible;
export function TableRowElement({ children, ...props }: PlateElementProps<TTableRowElement>) {
const { element } = props;
const readOnly = useReadOnly();
const selected = useSelected();
const editor = useEditorRef();
const isSelectionAreaVisible = usePluginOption(BlockSelectionPlugin, "isSelectionAreaVisible");
const hasControls = !readOnly && !isSelectionAreaVisible;
const { isDragging, nodeRef, previewRef, handleRef } = useDraggable({
element,
type: element.type,
canDropNode: ({ dragEntry, dropEntry }) =>
PathApi.equals(
PathApi.parent(dragEntry[1]),
PathApi.parent(dropEntry[1])
),
onDropHandler: (_, { dragItem }) => {
const dragElement = (dragItem as { element: TElement }).element;
const { isDragging, nodeRef, previewRef, handleRef } = useDraggable({
element,
type: element.type,
canDropNode: ({ dragEntry, dropEntry }) =>
PathApi.equals(PathApi.parent(dragEntry[1]), PathApi.parent(dropEntry[1])),
onDropHandler: (_, { dragItem }) => {
const dragElement = (dragItem as { element: TElement }).element;
if (dragElement) {
editor.tf.select(dragElement);
}
},
});
if (dragElement) {
editor.tf.select(dragElement);
}
},
});
return (
<PlateElement
{...props}
ref={useComposedRef(props.ref, previewRef, nodeRef)}
as="tr"
className={cn('group/row', isDragging && 'opacity-50')}
attributes={{
...props.attributes,
'data-selected': selected ? 'true' : undefined,
}}
>
{hasControls && (
<td className="w-2 select-none" contentEditable={false}>
<RowDragHandle dragRef={handleRef} />
<RowDropLine />
</td>
)}
return (
<PlateElement
{...props}
ref={useComposedRef(props.ref, previewRef, nodeRef)}
as="tr"
className={cn("group/row", isDragging && "opacity-50")}
attributes={{
...props.attributes,
"data-selected": selected ? "true" : undefined,
}}
>
{hasControls && (
<td className="w-2 select-none" contentEditable={false}>
<RowDragHandle dragRef={handleRef} />
<RowDropLine />
</td>
)}
{children}
</PlateElement>
);
{children}
</PlateElement>
);
}
function RowDragHandle({ dragRef }: { dragRef: React.Ref<any> }) {
const editor = useEditorRef();
const element = useElement();
const editor = useEditorRef();
const element = useElement();
return (
<Button
ref={dragRef}
variant="outline"
className={cn(
'-translate-y-1/2 absolute top-1/2 left-0 z-51 h-6 w-4 p-0 focus-visible:ring-0 focus-visible:ring-offset-0',
'cursor-grab active:cursor-grabbing',
'opacity-0 transition-opacity duration-100 group-hover/row:opacity-100 group-has-data-[resizing="true"]/row:opacity-0'
)}
onClick={() => {
editor.tf.select(element);
}}
>
<GripVertical className="text-muted-foreground" />
</Button>
);
return (
<Button
ref={dragRef}
variant="outline"
className={cn(
"-translate-y-1/2 absolute top-1/2 left-0 z-51 h-6 w-4 p-0 focus-visible:ring-0 focus-visible:ring-offset-0",
"cursor-grab active:cursor-grabbing",
'opacity-0 transition-opacity duration-100 group-hover/row:opacity-100 group-has-data-[resizing="true"]/row:opacity-0'
)}
onClick={() => {
editor.tf.select(element);
}}
>
<GripVertical className="text-muted-foreground" />
</Button>
);
}
function RowDropLine() {
const { dropLine } = useDropLine();
const { dropLine } = useDropLine();
if (!dropLine) return null;
if (!dropLine) return null;
return (
<div
className={cn(
'absolute inset-x-0 left-2 z-50 h-0.5 bg-brand/50',
dropLine === 'top' ? '-top-px' : '-bottom-px'
)}
/>
);
return (
<div
className={cn(
"absolute inset-x-0 left-2 z-50 h-0.5 bg-brand/50",
dropLine === "top" ? "-top-px" : "-bottom-px"
)}
/>
);
}
export function TableCellElement({
isHeader,
...props
isHeader,
...props
}: PlateElementProps<TTableCellElement> & {
isHeader?: boolean;
isHeader?: boolean;
}) {
const { api } = useEditorPlugin(TablePlugin);
const readOnly = useReadOnly();
const element = props.element;
const { api } = useEditorPlugin(TablePlugin);
const readOnly = useReadOnly();
const element = props.element;
const tableId = useElementSelector(([node]) => node.id as string, [], {
key: KEYS.table,
});
const rowId = useElementSelector(([node]) => node.id as string, [], {
key: KEYS.tr,
});
const isSelectingTable = useBlockSelected(tableId);
const isSelectingRow = useBlockSelected(rowId) || isSelectingTable;
const isSelectionAreaVisible = usePluginOption(
BlockSelectionPlugin,
'isSelectionAreaVisible'
);
const tableId = useElementSelector(([node]) => node.id as string, [], {
key: KEYS.table,
});
const rowId = useElementSelector(([node]) => node.id as string, [], {
key: KEYS.tr,
});
const isSelectingTable = useBlockSelected(tableId);
const isSelectingRow = useBlockSelected(rowId) || isSelectingTable;
const isSelectionAreaVisible = usePluginOption(BlockSelectionPlugin, "isSelectionAreaVisible");
const { borders, colIndex, colSpan, minHeight, rowIndex, selected, width } =
useTableCellElement();
const { borders, colIndex, colSpan, minHeight, rowIndex, selected, width } =
useTableCellElement();
const { bottomProps, hiddenLeft, leftProps, rightProps } =
useTableCellElementResizable({
colIndex,
colSpan,
rowIndex,
});
const { bottomProps, hiddenLeft, leftProps, rightProps } = useTableCellElementResizable({
colIndex,
colSpan,
rowIndex,
});
return (
<PlateElement
{...props}
as={isHeader ? 'th' : 'td'}
className={cn(
'h-full overflow-visible border-none bg-background p-0',
element.background ? 'bg-(--cellBackground)' : 'bg-background',
isHeader && 'text-left *:m-0',
'before:size-full',
selected && 'before:z-10 before:bg-brand/5',
"before:absolute before:box-border before:select-none before:content-['']",
borders.bottom?.size && 'before:border-b before:border-b-border',
borders.right?.size && 'before:border-r before:border-r-border',
borders.left?.size && 'before:border-l before:border-l-border',
borders.top?.size && 'before:border-t before:border-t-border'
)}
style={
{
'--cellBackground': element.background,
minWidth: width || 48,
} as React.CSSProperties
}
attributes={{
...props.attributes,
colSpan: api.table.getColSpan(element),
rowSpan: api.table.getRowSpan(element),
}}
>
<div
className="relative z-20 box-border h-full px-3 py-2"
style={{ minHeight }}
>
{props.children}
</div>
return (
<PlateElement
{...props}
as={isHeader ? "th" : "td"}
className={cn(
"h-full overflow-visible border-none bg-background p-0",
element.background ? "bg-(--cellBackground)" : "bg-background",
isHeader && "text-left *:m-0",
"before:size-full",
selected && "before:z-10 before:bg-brand/5",
"before:absolute before:box-border before:select-none before:content-['']",
borders.bottom?.size && "before:border-b before:border-b-border",
borders.right?.size && "before:border-r before:border-r-border",
borders.left?.size && "before:border-l before:border-l-border",
borders.top?.size && "before:border-t before:border-t-border"
)}
style={
{
"--cellBackground": element.background,
minWidth: width || 48,
} as React.CSSProperties
}
attributes={{
...props.attributes,
colSpan: api.table.getColSpan(element),
rowSpan: api.table.getRowSpan(element),
}}
>
<div className="relative z-20 box-border h-full px-3 py-2" style={{ minHeight }}>
{props.children}
</div>
{!isSelectionAreaVisible && (
<div
className="group absolute top-0 size-full select-none"
contentEditable={false}
suppressContentEditableWarning={true}
>
{!readOnly && (
<>
<ResizeHandle
{...rightProps}
className="-top-2 -right-1 h-[calc(100%_+_8px)] w-2"
data-col={colIndex}
/>
<ResizeHandle {...bottomProps} className="-bottom-1 h-2" />
{!hiddenLeft && (
<ResizeHandle
{...leftProps}
className="-left-1 top-0 w-2"
data-resizer-left={colIndex === 0 ? 'true' : undefined}
/>
)}
{!isSelectionAreaVisible && (
<div
className="group absolute top-0 size-full select-none"
contentEditable={false}
suppressContentEditableWarning={true}
>
{!readOnly && (
<>
<ResizeHandle
{...rightProps}
className="-top-2 -right-1 h-[calc(100%_+_8px)] w-2"
data-col={colIndex}
/>
<ResizeHandle {...bottomProps} className="-bottom-1 h-2" />
{!hiddenLeft && (
<ResizeHandle
{...leftProps}
className="-left-1 top-0 w-2"
data-resizer-left={colIndex === 0 ? "true" : undefined}
/>
)}
<div
className={cn(
'absolute top-0 z-30 hidden h-full w-1 bg-ring',
'right-[-1.5px]',
columnResizeVariants({ colIndex: colIndex as any })
)}
/>
{colIndex === 0 && (
<div
className={cn(
'absolute top-0 z-30 h-full w-1 bg-ring',
'left-[-1.5px]',
'fade-in hidden animate-in group-has-[[data-resizer-left]:hover]/table:block group-has-[[data-resizer-left][data-resizing="true"]]/table:block'
)}
/>
)}
</>
)}
</div>
)}
<div
className={cn(
"absolute top-0 z-30 hidden h-full w-1 bg-ring",
"right-[-1.5px]",
columnResizeVariants({ colIndex: colIndex as any })
)}
/>
{colIndex === 0 && (
<div
className={cn(
"absolute top-0 z-30 h-full w-1 bg-ring",
"left-[-1.5px]",
'fade-in hidden animate-in group-has-[[data-resizer-left]:hover]/table:block group-has-[[data-resizer-left][data-resizing="true"]]/table:block'
)}
/>
)}
</>
)}
</div>
)}
{isSelectingRow && (
<div className={blockSelectionVariants()} contentEditable={false} />
)}
</PlateElement>
);
{isSelectingRow && <div className={blockSelectionVariants()} contentEditable={false} />}
</PlateElement>
);
}
export function TableCellHeaderElement(
props: React.ComponentProps<typeof TableCellElement>
) {
return <TableCellElement {...props} isHeader />;
export function TableCellHeaderElement(props: React.ComponentProps<typeof TableCellElement>) {
return <TableCellElement {...props} isHeader />;
}
const columnResizeVariants = cva('fade-in hidden animate-in', {
variants: {
colIndex: {
0: 'group-has-[[data-col="0"]:hover]/table:block group-has-[[data-col="0"][data-resizing="true"]]/table:block',
1: 'group-has-[[data-col="1"]:hover]/table:block group-has-[[data-col="1"][data-resizing="true"]]/table:block',
2: 'group-has-[[data-col="2"]:hover]/table:block group-has-[[data-col="2"][data-resizing="true"]]/table:block',
3: 'group-has-[[data-col="3"]:hover]/table:block group-has-[[data-col="3"][data-resizing="true"]]/table:block',
4: 'group-has-[[data-col="4"]:hover]/table:block group-has-[[data-col="4"][data-resizing="true"]]/table:block',
5: 'group-has-[[data-col="5"]:hover]/table:block group-has-[[data-col="5"][data-resizing="true"]]/table:block',
6: 'group-has-[[data-col="6"]:hover]/table:block group-has-[[data-col="6"][data-resizing="true"]]/table:block',
7: 'group-has-[[data-col="7"]:hover]/table:block group-has-[[data-col="7"][data-resizing="true"]]/table:block',
8: 'group-has-[[data-col="8"]:hover]/table:block group-has-[[data-col="8"][data-resizing="true"]]/table:block',
9: 'group-has-[[data-col="9"]:hover]/table:block group-has-[[data-col="9"][data-resizing="true"]]/table:block',
10: 'group-has-[[data-col="10"]:hover]/table:block group-has-[[data-col="10"][data-resizing="true"]]/table:block',
},
},
const columnResizeVariants = cva("fade-in hidden animate-in", {
variants: {
colIndex: {
0: 'group-has-[[data-col="0"]:hover]/table:block group-has-[[data-col="0"][data-resizing="true"]]/table:block',
1: 'group-has-[[data-col="1"]:hover]/table:block group-has-[[data-col="1"][data-resizing="true"]]/table:block',
2: 'group-has-[[data-col="2"]:hover]/table:block group-has-[[data-col="2"][data-resizing="true"]]/table:block',
3: 'group-has-[[data-col="3"]:hover]/table:block group-has-[[data-col="3"][data-resizing="true"]]/table:block',
4: 'group-has-[[data-col="4"]:hover]/table:block group-has-[[data-col="4"][data-resizing="true"]]/table:block',
5: 'group-has-[[data-col="5"]:hover]/table:block group-has-[[data-col="5"][data-resizing="true"]]/table:block',
6: 'group-has-[[data-col="6"]:hover]/table:block group-has-[[data-col="6"][data-resizing="true"]]/table:block',
7: 'group-has-[[data-col="7"]:hover]/table:block group-has-[[data-col="7"][data-resizing="true"]]/table:block',
8: 'group-has-[[data-col="8"]:hover]/table:block group-has-[[data-col="8"][data-resizing="true"]]/table:block',
9: 'group-has-[[data-col="9"]:hover]/table:block group-has-[[data-col="9"][data-resizing="true"]]/table:block',
10: 'group-has-[[data-col="10"]:hover]/table:block group-has-[[data-col="10"][data-resizing="true"]]/table:block',
},
},
});

View file

@ -1,39 +1,33 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import { useToggleButton, useToggleButtonState } from '@platejs/toggle/react';
import { ChevronRightIcon } from 'lucide-react';
import { type PlateElementProps, PlateElement } from 'platejs/react';
import { useToggleButton, useToggleButtonState } from "@platejs/toggle/react";
import { ChevronRightIcon } from "lucide-react";
import { type PlateElementProps, PlateElement } from "platejs/react";
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils";
export function ToggleElement({
children,
...props
}: PlateElementProps) {
const element = props.element;
const state = useToggleButtonState(element.id as string);
const { buttonProps, open } = useToggleButton(state);
export function ToggleElement({ children, ...props }: PlateElementProps) {
const element = props.element;
const state = useToggleButtonState(element.id as string);
const { buttonProps, open } = useToggleButton(state);
return (
<PlateElement {...props} className="relative py-1 pl-6">
<button
className={cn(
'absolute top-1.5 left-0 flex size-6 cursor-pointer select-none items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground',
)}
contentEditable={false}
type="button"
{...buttonProps}
>
<ChevronRightIcon
className={cn(
'size-4 transition-transform duration-200',
open && 'rotate-90'
)}
/>
</button>
<div>{children}</div>
</PlateElement>
);
return (
<PlateElement {...props} className="relative py-1 pl-6">
<button
className={cn(
"absolute top-1.5 left-0 flex size-6 cursor-pointer select-none items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
)}
contentEditable={false}
type="button"
{...buttonProps}
>
<ChevronRightIcon
className={cn("size-4 transition-transform duration-200", open && "rotate-90")}
/>
</button>
<div>{children}</div>
</PlateElement>
);
}

View file

@ -1,387 +1,363 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import * as ToolbarPrimitive from '@radix-ui/react-toolbar';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { type VariantProps, cva } from 'class-variance-authority';
import { ChevronDown } from 'lucide-react';
import * as ToolbarPrimitive from "@radix-ui/react-toolbar";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { type VariantProps, cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import {
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import { Separator } from '@/components/ui/separator';
import { Tooltip, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
export function Toolbar({
className,
...props
className,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.Root>) {
return (
<ToolbarPrimitive.Root
className={cn('relative flex select-none items-center', className)}
{...props}
/>
);
return (
<ToolbarPrimitive.Root
className={cn("relative flex select-none items-center", className)}
{...props}
/>
);
}
export function ToolbarToggleGroup({
className,
...props
className,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.ToolbarToggleGroup>) {
return (
<ToolbarPrimitive.ToolbarToggleGroup
className={cn('flex items-center', className)}
{...props}
/>
);
return (
<ToolbarPrimitive.ToolbarToggleGroup
className={cn("flex items-center", className)}
{...props}
/>
);
}
export function ToolbarLink({
className,
...props
className,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.Link>) {
return (
<ToolbarPrimitive.Link
className={cn('font-medium underline underline-offset-4', className)}
{...props}
/>
);
return (
<ToolbarPrimitive.Link
className={cn("font-medium underline underline-offset-4", className)}
{...props}
/>
);
}
export function ToolbarSeparator({
className,
...props
className,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.Separator>) {
return (
<ToolbarPrimitive.Separator
className={cn('mx-2 my-1 w-px shrink-0 bg-border', className)}
{...props}
/>
);
return (
<ToolbarPrimitive.Separator
className={cn("mx-2 my-1 w-px shrink-0 bg-border", className)}
{...props}
/>
);
}
// From toggleVariants
const toolbarButtonVariants = cva(
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,box-shadow] hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-checked:bg-accent aria-checked:text-accent-foreground aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
defaultVariants: {
size: 'default',
variant: 'default',
},
variants: {
size: {
default: 'h-9 min-w-9 px-2',
lg: 'h-10 min-w-10 px-2.5',
sm: 'h-8 min-w-8 px-1.5',
},
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
},
},
}
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,box-shadow] hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-checked:bg-accent aria-checked:text-accent-foreground aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
defaultVariants: {
size: "default",
variant: "default",
},
variants: {
size: {
default: "h-9 min-w-9 px-2",
lg: "h-10 min-w-10 px-2.5",
sm: "h-8 min-w-8 px-1.5",
},
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
},
}
);
const dropdownArrowVariants = cva(
cn(
'inline-flex items-center justify-center rounded-r-md font-medium text-foreground text-sm transition-colors disabled:pointer-events-none disabled:opacity-50'
),
{
defaultVariants: {
size: 'sm',
variant: 'default',
},
variants: {
size: {
default: 'h-9 w-6',
lg: 'h-10 w-8',
sm: 'h-8 w-4',
},
variant: {
default:
'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',
outline:
'border border-input border-l-0 bg-transparent hover:bg-accent hover:text-accent-foreground',
},
},
}
cn(
"inline-flex items-center justify-center rounded-r-md font-medium text-foreground text-sm transition-colors disabled:pointer-events-none disabled:opacity-50"
),
{
defaultVariants: {
size: "sm",
variant: "default",
},
variants: {
size: {
default: "h-9 w-6",
lg: "h-10 w-8",
sm: "h-8 w-4",
},
variant: {
default:
"bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground",
outline:
"border border-input border-l-0 bg-transparent hover:bg-accent hover:text-accent-foreground",
},
},
}
);
type ToolbarButtonProps = {
isDropdown?: boolean;
pressed?: boolean;
} & Omit<
React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
'asChild' | 'value'
> &
VariantProps<typeof toolbarButtonVariants>;
isDropdown?: boolean;
pressed?: boolean;
} & Omit<React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>, "asChild" | "value"> &
VariantProps<typeof toolbarButtonVariants>;
export const ToolbarButton = withTooltip(function ToolbarButton({
children,
className,
isDropdown,
pressed,
size = 'sm',
variant,
...props
children,
className,
isDropdown,
pressed,
size = "sm",
variant,
...props
}: ToolbarButtonProps) {
return typeof pressed === 'boolean' ? (
<ToolbarToggleGroup disabled={props.disabled} value="single" type="single">
<ToolbarToggleItem
className={cn(
toolbarButtonVariants({
size,
variant,
}),
isDropdown && 'justify-between gap-1 pr-1',
className
)}
value={pressed ? 'single' : ''}
{...props}
>
{isDropdown ? (
<>
<div className="flex flex-1 items-center gap-2 whitespace-nowrap">
{children}
</div>
<div>
<ChevronDown
className="size-3.5 text-muted-foreground"
data-icon
/>
</div>
</>
) : (
children
)}
</ToolbarToggleItem>
</ToolbarToggleGroup>
) : (
<ToolbarPrimitive.Button
className={cn(
toolbarButtonVariants({
size,
variant,
}),
isDropdown && 'pr-1',
className
)}
{...props}
>
{children}
</ToolbarPrimitive.Button>
);
return typeof pressed === "boolean" ? (
<ToolbarToggleGroup disabled={props.disabled} value="single" type="single">
<ToolbarToggleItem
className={cn(
toolbarButtonVariants({
size,
variant,
}),
isDropdown && "justify-between gap-1 pr-1",
className
)}
value={pressed ? "single" : ""}
{...props}
>
{isDropdown ? (
<>
<div className="flex flex-1 items-center gap-2 whitespace-nowrap">{children}</div>
<div>
<ChevronDown className="size-3.5 text-muted-foreground" data-icon />
</div>
</>
) : (
children
)}
</ToolbarToggleItem>
</ToolbarToggleGroup>
) : (
<ToolbarPrimitive.Button
className={cn(
toolbarButtonVariants({
size,
variant,
}),
isDropdown && "pr-1",
className
)}
{...props}
>
{children}
</ToolbarPrimitive.Button>
);
});
export function ToolbarSplitButton({
className,
...props
className,
...props
}: React.ComponentPropsWithoutRef<typeof ToolbarButton>) {
return (
<ToolbarButton
className={cn('group flex gap-0 px-0 hover:bg-transparent', className)}
{...props}
/>
);
return (
<ToolbarButton
className={cn("group flex gap-0 px-0 hover:bg-transparent", className)}
{...props}
/>
);
}
type ToolbarSplitButtonPrimaryProps = Omit<
React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
'value'
React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
"value"
> &
VariantProps<typeof toolbarButtonVariants>;
VariantProps<typeof toolbarButtonVariants>;
export function ToolbarSplitButtonPrimary({
children,
className,
size = 'sm',
variant,
...props
children,
className,
size = "sm",
variant,
...props
}: ToolbarSplitButtonPrimaryProps) {
return (
<span
className={cn(
toolbarButtonVariants({
size,
variant,
}),
'rounded-r-none',
'group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground',
className
)}
{...props}
>
{children}
</span>
);
return (
<span
className={cn(
toolbarButtonVariants({
size,
variant,
}),
"rounded-r-none",
"group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground",
className
)}
{...props}
>
{children}
</span>
);
}
export function ToolbarSplitButtonSecondary({
className,
size,
variant,
...props
}: React.ComponentPropsWithoutRef<'span'> &
VariantProps<typeof dropdownArrowVariants>) {
return (
<span
className={cn(
dropdownArrowVariants({
size,
variant,
}),
'group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground',
className
)}
onClick={(e) => e.stopPropagation()}
role="button"
{...props}
>
<ChevronDown className="size-3.5 text-muted-foreground" data-icon />
</span>
);
className,
size,
variant,
...props
}: React.ComponentPropsWithoutRef<"span"> & VariantProps<typeof dropdownArrowVariants>) {
return (
<span
className={cn(
dropdownArrowVariants({
size,
variant,
}),
"group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground",
className
)}
onClick={(e) => e.stopPropagation()}
role="button"
{...props}
>
<ChevronDown className="size-3.5 text-muted-foreground" data-icon />
</span>
);
}
export function ToolbarToggleItem({
className,
size = 'sm',
variant,
...props
className,
size = "sm",
variant,
...props
}: React.ComponentProps<typeof ToolbarPrimitive.ToggleItem> &
VariantProps<typeof toolbarButtonVariants>) {
return (
<ToolbarPrimitive.ToggleItem
className={cn(toolbarButtonVariants({ size, variant }), className)}
{...props}
/>
);
VariantProps<typeof toolbarButtonVariants>) {
return (
<ToolbarPrimitive.ToggleItem
className={cn(toolbarButtonVariants({ size, variant }), className)}
{...props}
/>
);
}
export function ToolbarGroup({
children,
className,
}: React.ComponentProps<'div'>) {
return (
<div
className={cn(
'group/toolbar-group',
'relative hidden has-[button]:flex',
className
)}
>
<div className="flex items-center">{children}</div>
export function ToolbarGroup({ children, className }: React.ComponentProps<"div">) {
return (
<div className={cn("group/toolbar-group", "relative hidden has-[button]:flex", className)}>
<div className="flex items-center">{children}</div>
<div className="group-last/toolbar-group:hidden! mx-1.5 py-0.5">
<Separator orientation="vertical" />
</div>
</div>
);
<div className="group-last/toolbar-group:hidden! mx-1.5 py-0.5">
<Separator orientation="vertical" />
</div>
</div>
);
}
type TooltipProps<T extends React.ElementType> = {
tooltip?: React.ReactNode;
tooltipContentProps?: Omit<
React.ComponentPropsWithoutRef<typeof TooltipContent>,
'children'
>;
tooltipProps?: Omit<
React.ComponentPropsWithoutRef<typeof Tooltip>,
'children'
>;
tooltipTriggerProps?: React.ComponentPropsWithoutRef<typeof TooltipTrigger>;
tooltip?: React.ReactNode;
tooltipContentProps?: Omit<React.ComponentPropsWithoutRef<typeof TooltipContent>, "children">;
tooltipProps?: Omit<React.ComponentPropsWithoutRef<typeof Tooltip>, "children">;
tooltipTriggerProps?: React.ComponentPropsWithoutRef<typeof TooltipTrigger>;
} & React.ComponentProps<T>;
function withTooltip<T extends React.ElementType>(Component: T) {
return function ExtendComponent({
tooltip,
tooltipContentProps,
tooltipProps,
tooltipTriggerProps,
...props
}: TooltipProps<T>) {
const [mounted, setMounted] = React.useState(false);
return function ExtendComponent({
tooltip,
tooltipContentProps,
tooltipProps,
tooltipTriggerProps,
...props
}: TooltipProps<T>) {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
React.useEffect(() => {
setMounted(true);
}, []);
const component = <Component {...(props as React.ComponentProps<T>)} />;
const component = <Component {...(props as React.ComponentProps<T>)} />;
if (tooltip && mounted) {
return (
<Tooltip {...tooltipProps}>
<TooltipTrigger asChild {...tooltipTriggerProps}>
{component}
</TooltipTrigger>
if (tooltip && mounted) {
return (
<Tooltip {...tooltipProps}>
<TooltipTrigger asChild {...tooltipTriggerProps}>
{component}
</TooltipTrigger>
<TooltipContent {...tooltipContentProps}>{tooltip}</TooltipContent>
</Tooltip>
);
}
<TooltipContent {...tooltipContentProps}>{tooltip}</TooltipContent>
</Tooltip>
);
}
return component;
};
return component;
};
}
function TooltipContent({
children,
className,
sideOffset = 4,
...props
children,
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
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',
className
)}
data-slot="tooltip-content"
sideOffset={sideOffset}
{...props}
>
{children}
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
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",
className
)}
data-slot="tooltip-content"
sideOffset={sideOffset}
{...props}
>
{children}
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export function ToolbarMenuGroup({
children,
className,
label,
...props
children,
className,
label,
...props
}: React.ComponentProps<typeof DropdownMenuRadioGroup> & { label?: string }) {
return (
<>
<DropdownMenuSeparator
className={cn(
'hidden',
'mb-0 mx-2 shrink-0 peer-has-[[role=menuitem]]/menu-group:block peer-has-[[role=menuitemradio]]/menu-group:block peer-has-[[role=option]]/menu-group:block',
'dark:bg-neutral-700'
)}
/>
return (
<>
<DropdownMenuSeparator
className={cn(
"hidden",
"mb-0 mx-2 shrink-0 peer-has-[[role=menuitem]]/menu-group:block peer-has-[[role=menuitemradio]]/menu-group:block peer-has-[[role=option]]/menu-group:block",
"dark:bg-neutral-700"
)}
/>
<DropdownMenuRadioGroup
{...props}
className={cn(
'hidden',
'peer/menu-group group/menu-group my-1.5 has-[[role=menuitem]]:block has-[[role=menuitemradio]]:block has-[[role=option]]:block',
className
)}
>
{label && (
<DropdownMenuLabel className="select-none font-semibold text-muted-foreground text-xs">
{label}
</DropdownMenuLabel>
)}
{children}
</DropdownMenuRadioGroup>
</>
);
<DropdownMenuRadioGroup
{...props}
className={cn(
"hidden",
"peer/menu-group group/menu-group my-1.5 has-[[role=menuitem]]:block has-[[role=menuitemradio]]:block has-[[role=option]]:block",
className
)}
>
{label && (
<DropdownMenuLabel className="select-none font-semibold text-muted-foreground text-xs">
{label}
</DropdownMenuLabel>
)}
{children}
</DropdownMenuRadioGroup>
</>
);
}

View file

@ -1,62 +1,58 @@
"use client"
"use client";
import * as React from "react"
import { Tooltip as TooltipPrimitive } from "radix-ui"
import * as React from "react";
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
disableHoverableContent = true,
...props
delayDuration = 0,
disableHoverableContent = true,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
disableHoverableContent={disableHoverableContent}
{...props}
/>
)
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
disableHoverableContent={disableHoverableContent}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 4,
children,
...props
className,
sideOffset = 4,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
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",
className
)}
{...props}
>
{children}
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
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",
className
)}
{...props}
>
{children}
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View file

@ -1,191 +1,189 @@
'use client';
"use client";
import * as React from 'react';
import * as React from "react";
import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
import type { TElement } from 'platejs';
import type { DropdownMenuProps } from "@radix-ui/react-dropdown-menu";
import type { TElement } from "platejs";
import { DropdownMenuItemIndicator } from '@radix-ui/react-dropdown-menu';
import { DropdownMenuItemIndicator } from "@radix-ui/react-dropdown-menu";
import {
CheckIcon,
ChevronRightIcon,
FileCodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
Heading4Icon,
Heading5Icon,
Heading6Icon,
InfoIcon,
ListIcon,
ListOrderedIcon,
PilcrowIcon,
QuoteIcon,
SquareIcon,
} from 'lucide-react';
import { KEYS } from 'platejs';
import { useEditorRef, useSelectionFragmentProp } from 'platejs/react';
CheckIcon,
ChevronRightIcon,
FileCodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
Heading4Icon,
Heading5Icon,
Heading6Icon,
InfoIcon,
ListIcon,
ListOrderedIcon,
PilcrowIcon,
QuoteIcon,
SquareIcon,
} from "lucide-react";
import { KEYS } from "platejs";
import { useEditorRef, useSelectionFragmentProp } from "platejs/react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
getBlockType,
setBlockType,
} from '@/components/editor/transforms';
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { getBlockType, setBlockType } from "@/components/editor/transforms";
import { ToolbarButton, ToolbarMenuGroup } from './toolbar';
import { ToolbarButton, ToolbarMenuGroup } from "./toolbar";
export const turnIntoItems = [
{
icon: <PilcrowIcon />,
keywords: ['paragraph'],
label: 'Text',
value: KEYS.p,
},
{
icon: <Heading1Icon />,
keywords: ['title', 'h1'],
label: 'Heading 1',
value: 'h1',
},
{
icon: <Heading2Icon />,
keywords: ['subtitle', 'h2'],
label: 'Heading 2',
value: 'h2',
},
{
icon: <Heading3Icon />,
keywords: ['subtitle', 'h3'],
label: 'Heading 3',
value: 'h3',
},
{
icon: <Heading4Icon />,
keywords: ['subtitle', 'h4'],
label: 'Heading 4',
value: 'h4',
},
{
icon: <Heading5Icon />,
keywords: ['subtitle', 'h5'],
label: 'Heading 5',
value: 'h5',
},
{
icon: <Heading6Icon />,
keywords: ['subtitle', 'h6'],
label: 'Heading 6',
value: 'h6',
},
{
icon: <ListIcon />,
keywords: ['unordered', 'ul', '-'],
label: 'Bulleted list',
value: KEYS.ul,
},
{
icon: <ListOrderedIcon />,
keywords: ['ordered', 'ol', '1'],
label: 'Numbered list',
value: KEYS.ol,
},
{
icon: <SquareIcon />,
keywords: ['checklist', 'task', 'checkbox', '[]'],
label: 'To-do list',
value: KEYS.listTodo,
},
{
icon: <FileCodeIcon />,
keywords: ['```'],
label: 'Code',
value: KEYS.codeBlock,
},
{
icon: <QuoteIcon />,
keywords: ['citation', 'blockquote', '>'],
label: 'Quote',
value: KEYS.blockquote,
},
{
icon: <InfoIcon />,
keywords: ['callout', 'note', 'info', 'warning', 'tip'],
label: 'Callout',
value: KEYS.callout,
},
{
icon: <ChevronRightIcon />,
keywords: ['toggle', 'collapsible', 'expand'],
label: 'Toggle',
value: KEYS.toggle,
},
{
icon: <PilcrowIcon />,
keywords: ["paragraph"],
label: "Text",
value: KEYS.p,
},
{
icon: <Heading1Icon />,
keywords: ["title", "h1"],
label: "Heading 1",
value: "h1",
},
{
icon: <Heading2Icon />,
keywords: ["subtitle", "h2"],
label: "Heading 2",
value: "h2",
},
{
icon: <Heading3Icon />,
keywords: ["subtitle", "h3"],
label: "Heading 3",
value: "h3",
},
{
icon: <Heading4Icon />,
keywords: ["subtitle", "h4"],
label: "Heading 4",
value: "h4",
},
{
icon: <Heading5Icon />,
keywords: ["subtitle", "h5"],
label: "Heading 5",
value: "h5",
},
{
icon: <Heading6Icon />,
keywords: ["subtitle", "h6"],
label: "Heading 6",
value: "h6",
},
{
icon: <ListIcon />,
keywords: ["unordered", "ul", "-"],
label: "Bulleted list",
value: KEYS.ul,
},
{
icon: <ListOrderedIcon />,
keywords: ["ordered", "ol", "1"],
label: "Numbered list",
value: KEYS.ol,
},
{
icon: <SquareIcon />,
keywords: ["checklist", "task", "checkbox", "[]"],
label: "To-do list",
value: KEYS.listTodo,
},
{
icon: <FileCodeIcon />,
keywords: ["```"],
label: "Code",
value: KEYS.codeBlock,
},
{
icon: <QuoteIcon />,
keywords: ["citation", "blockquote", ">"],
label: "Quote",
value: KEYS.blockquote,
},
{
icon: <InfoIcon />,
keywords: ["callout", "note", "info", "warning", "tip"],
label: "Callout",
value: KEYS.callout,
},
{
icon: <ChevronRightIcon />,
keywords: ["toggle", "collapsible", "expand"],
label: "Toggle",
value: KEYS.toggle,
},
];
export function TurnIntoToolbarButton({ tooltip = 'Turn into', ...props }: DropdownMenuProps & { tooltip?: React.ReactNode }) {
const editor = useEditorRef();
const [open, setOpen] = React.useState(false);
export function TurnIntoToolbarButton({
tooltip = "Turn into",
...props
}: DropdownMenuProps & { tooltip?: React.ReactNode }) {
const editor = useEditorRef();
const [open, setOpen] = React.useState(false);
const value = useSelectionFragmentProp({
defaultValue: KEYS.p,
getProp: (node) => getBlockType(node as TElement),
});
const selectedItem = React.useMemo(
() =>
turnIntoItems.find((item) => item.value === (value ?? KEYS.p)) ??
turnIntoItems[0],
[value]
);
const value = useSelectionFragmentProp({
defaultValue: KEYS.p,
getProp: (node) => getBlockType(node as TElement),
});
const selectedItem = React.useMemo(
() => turnIntoItems.find((item) => item.value === (value ?? KEYS.p)) ?? turnIntoItems[0],
[value]
);
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false} {...props}>
<DropdownMenuTrigger asChild>
<ToolbarButton
className="min-w-[80px] sm:min-w-[125px]"
pressed={open}
tooltip={tooltip}
isDropdown
>
{selectedItem.label}
</ToolbarButton>
</DropdownMenuTrigger>
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false} {...props}>
<DropdownMenuTrigger asChild>
<ToolbarButton
className="min-w-[80px] sm:min-w-[125px]"
pressed={open}
tooltip={tooltip}
isDropdown
>
{selectedItem.label}
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="z-[100] ignore-click-outside/toolbar min-w-0 max-h-[60vh] overflow-y-auto dark:bg-neutral-800 dark:border dark:border-neutral-700"
onCloseAutoFocus={(e) => {
e.preventDefault();
editor.tf.focus();
}}
align="start"
>
<ToolbarMenuGroup
value={value}
onValueChange={(type) => {
setBlockType(editor, type);
}}
label="Turn into"
>
{turnIntoItems.map(({ icon, label, value: itemValue }) => (
<DropdownMenuRadioItem
key={itemValue}
className="min-w-[180px] pl-2 *:first:[span]:hidden dark:text-white"
value={itemValue}
>
<span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<CheckIcon />
</DropdownMenuItemIndicator>
</span>
<span className="text-muted-foreground">{icon}</span>
{label}
</DropdownMenuRadioItem>
))}
</ToolbarMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
<DropdownMenuContent
className="z-[100] ignore-click-outside/toolbar min-w-0 max-h-[60vh] overflow-y-auto dark:bg-neutral-800 dark:border dark:border-neutral-700"
onCloseAutoFocus={(e) => {
e.preventDefault();
editor.tf.focus();
}}
align="start"
>
<ToolbarMenuGroup
value={value}
onValueChange={(type) => {
setBlockType(editor, type);
}}
label="Turn into"
>
{turnIntoItems.map(({ icon, label, value: itemValue }) => (
<DropdownMenuRadioItem
key={itemValue}
className="min-w-[180px] pl-2 *:first:[span]:hidden dark:text-white"
value={itemValue}
>
<span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<CheckIcon />
</DropdownMenuItemIndicator>
</span>
<span className="text-muted-foreground">{icon}</span>
{label}
</DropdownMenuRadioItem>
))}
</ToolbarMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}

View file

@ -1,18 +1,18 @@
import * as React from 'react';
import * as React from "react";
export const useDebounce = <T>(value: T, delay = 500) => {
const [debouncedValue, setDebouncedValue] = React.useState(value);
const [debouncedValue, setDebouncedValue] = React.useState(value);
React.useEffect(() => {
const handler: NodeJS.Timeout = setTimeout(() => {
setDebouncedValue(value);
}, delay);
React.useEffect(() => {
const handler: NodeJS.Timeout = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
return () => {
clearTimeout(handler);
};
}, [value, delay]);
// Cancel the timeout if value changes (also on delay change or unmount)
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
return debouncedValue;
};

View file

@ -1,11 +1,11 @@
import * as React from 'react';
import * as React from "react";
export function useMounted() {
const [mounted, setMounted] = React.useState(false);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
React.useEffect(() => {
setMounted(true);
}, []);
return mounted;
return mounted;
}