mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
chore: ran linting
This commit is contained in:
parent
e46b24a2b1
commit
a482cc95de
67 changed files with 4971 additions and 5539 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
}),
|
||||
})
|
||||
),
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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" } },
|
||||
}),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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)];
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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" } },
|
||||
}),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue