@groupher/rich-editor 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/rich-editor.es.js +33455 -26238
  2. package/dist/rich-editor.umd.js +77 -101
  3. package/package.json +21 -6
  4. package/src/RichEditor.tsx +204 -92
  5. package/src/components/editor/editor-kit.tsx +27 -0
  6. package/src/components/editor/plugins/autoformat-kit.tsx +99 -0
  7. package/src/components/editor/plugins/callout-kit.tsx +7 -0
  8. package/src/components/editor/plugins/emoji-kit.tsx +14 -0
  9. package/src/components/editor/plugins/indent-kit.tsx +12 -0
  10. package/src/components/editor/plugins/link-kit.tsx +13 -0
  11. package/src/components/editor/plugins/list-kit.tsx +17 -0
  12. package/src/components/editor/plugins/mention-kit.tsx +17 -0
  13. package/src/components/editor/plugins/slash-kit.tsx +15 -0
  14. package/src/components/editor/plugins/toggle-kit.tsx +7 -0
  15. package/src/components/ui/action-bar.tsx +208 -0
  16. package/src/components/ui/block-list.tsx +94 -0
  17. package/src/components/ui/button.tsx +49 -50
  18. package/src/components/ui/callout-node.tsx +65 -0
  19. package/src/components/ui/editor-static.tsx +44 -44
  20. package/src/components/ui/editor.tsx +107 -107
  21. package/src/components/ui/emoji-node.tsx +71 -0
  22. package/src/components/ui/emoji-toolbar-button.tsx +618 -0
  23. package/src/components/ui/floating-toolbar.tsx +86 -0
  24. package/src/components/ui/inline-combobox.tsx +414 -0
  25. package/src/components/ui/link-node.tsx +31 -0
  26. package/src/components/ui/link-toolbar-button.tsx +33 -0
  27. package/src/components/ui/mention-node.tsx +126 -0
  28. package/src/components/ui/slash-node.tsx +191 -0
  29. package/src/components/ui/toggle-node.tsx +36 -0
  30. package/src/components/ui/toolbar.tsx +10 -10
  31. package/src/hooks/use-debounce.ts +15 -0
  32. package/src/hooks/use-mounted.ts +11 -0
  33. package/src/i18n.tsx +155 -0
  34. package/src/main.tsx +35 -14
  35. package/src/mention-context.tsx +32 -0
  36. package/src/vite-env.d.ts +7 -0
@@ -0,0 +1,208 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import type { TElement } from "platejs";
6
+
7
+ import {
8
+ AtSign,
9
+ ChevronUp,
10
+ Clock3,
11
+ Heading1,
12
+ Image as ImageIcon,
13
+ List,
14
+ } from "lucide-react";
15
+ import { KEYS } from "platejs";
16
+ import { useEditorRef } from "platejs/react";
17
+
18
+ import {
19
+ DropdownMenu,
20
+ DropdownMenuContent,
21
+ DropdownMenuItem,
22
+ DropdownMenuSeparator,
23
+ DropdownMenuTrigger,
24
+ } from "@/components/ui/dropdown-menu";
25
+ import { cn } from "@/lib/utils";
26
+
27
+ const listTypes = new Set(["disc", "decimal", "todo"]);
28
+
29
+ const listStyleMap: Record<string, "disc" | "decimal" | "todo"> = {
30
+ [KEYS.ul]: "disc",
31
+ [KEYS.ol]: "decimal",
32
+ [KEYS.listTodo]: "todo",
33
+ };
34
+
35
+ const setBlockType = (
36
+ editor: ReturnType<typeof useEditorRef>,
37
+ type: string,
38
+ ) => {
39
+ editor.tf.withoutNormalizing(() => {
40
+ const entries = editor.api.blocks({ mode: "lowest" });
41
+
42
+ for (const [node, path] of entries) {
43
+ if ((node as TElement)[KEYS.listType]) {
44
+ editor.tf.unsetNodes([KEYS.listType, "indent"], { at: path });
45
+ }
46
+
47
+ editor.tf.setNodes({ type }, { at: path });
48
+ }
49
+ });
50
+ };
51
+
52
+ const setListType = (
53
+ editor: ReturnType<typeof useEditorRef>,
54
+ listStyleType: "disc" | "decimal" | "todo",
55
+ ) => {
56
+ editor.tf.withoutNormalizing(() => {
57
+ const entries = editor.api.blocks({ mode: "lowest" });
58
+
59
+ for (const [node, path] of entries) {
60
+ if (!listTypes.has(listStyleType)) return;
61
+
62
+ editor.tf.setNodes(
63
+ {
64
+ indent: 1,
65
+ listStyleType,
66
+ checked: listStyleType === KEYS.listTodo ? false : undefined,
67
+ },
68
+ { at: path },
69
+ );
70
+
71
+ if ((node as TElement)[KEYS.listType]) {
72
+ editor.tf.setNodes({ listStyleType }, { at: path });
73
+ }
74
+ }
75
+ });
76
+ };
77
+
78
+ const tabs = ["用户", "帖子", "更新日志", "文档"];
79
+
80
+ export function ActionBar({ className }: { className?: string }) {
81
+ const editor = useEditorRef();
82
+ const [activeTab, setActiveTab] = React.useState(tabs[0]);
83
+
84
+ return (
85
+ <div
86
+ className={cn(
87
+ "sticky bottom-0 z-10 mt-4 flex flex-wrap items-center gap-3 bg-card/95 px-16 py-3 text-sm backdrop-blur sm:px-[max(64px,calc(50%-350px))]",
88
+ className,
89
+ )}
90
+ >
91
+ <ActionGroup
92
+ label="标题"
93
+ icon={<Heading1 className="size-4" />}
94
+ items={[
95
+ { label: "H1", onSelect: () => setBlockType(editor, KEYS.h1) },
96
+ { label: "H2", onSelect: () => setBlockType(editor, KEYS.h2) },
97
+ { label: "H3", onSelect: () => setBlockType(editor, KEYS.h3) },
98
+ ]}
99
+ />
100
+ <ActionGroup
101
+ label="列表"
102
+ icon={<List className="size-4" />}
103
+ items={[
104
+ {
105
+ label: "有序列表",
106
+ onSelect: () => setListType(editor, listStyleMap[KEYS.ol]),
107
+ },
108
+ {
109
+ label: "无序列表",
110
+ onSelect: () => setListType(editor, listStyleMap[KEYS.ul]),
111
+ },
112
+ {
113
+ label: "待办列表",
114
+ onSelect: () => setListType(editor, listStyleMap[KEYS.listTodo]),
115
+ },
116
+ ]}
117
+ />
118
+ <DropdownMenu>
119
+ <DropdownMenuTrigger asChild>
120
+ <button type="button" className={actionButtonClassName}>
121
+ <span className="text-muted-foreground">
122
+ <AtSign className="size-3.5" />
123
+ </span>
124
+ <span className="opacity-65 group-hover:opacity-100">提及</span>
125
+ </button>
126
+ </DropdownMenuTrigger>
127
+ <DropdownMenuContent side="top" align="start" className="w-[280px]">
128
+ <div className="p-2">
129
+ <input
130
+ className="h-8 w-full rounded-md border border-input bg-background px-2 text-xs"
131
+ placeholder="搜索..."
132
+ type="search"
133
+ />
134
+ <div className="mt-2 flex flex-wrap gap-1">
135
+ {tabs.map((tab) => (
136
+ <button
137
+ key={tab}
138
+ type="button"
139
+ className={cn(
140
+ "rounded-full border px-2 py-0.5 text-xs",
141
+ tab === activeTab
142
+ ? "border-primary bg-primary/10 text-primary"
143
+ : "border-border text-muted-foreground",
144
+ )}
145
+ onClick={() => setActiveTab(tab)}
146
+ >
147
+ {tab}
148
+ </button>
149
+ ))}
150
+ </div>
151
+ </div>
152
+ <DropdownMenuSeparator />
153
+ <div className="px-2 pb-2 text-xs text-muted-foreground">
154
+ 结果列表由外部搜索提供
155
+ </div>
156
+ </DropdownMenuContent>
157
+ </DropdownMenu>
158
+ <ActionGroup
159
+ label="Image"
160
+ icon={<ImageIcon className="size-4" />}
161
+ items={[{ label: "本地图像" }, { label: "网络资源" }]}
162
+ />
163
+ <ActionGroup
164
+ label="Clock"
165
+ icon={<Clock3 className="size-4" />}
166
+ items={[{ label: "Undo" }, { label: "Redo" }, { label: "历史版本" }]}
167
+ />
168
+ </div>
169
+ );
170
+ }
171
+
172
+ const actionButtonClassName =
173
+ "group inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm text-foreground transition hover:bg-accent";
174
+
175
+ function ActionGroup({
176
+ icon,
177
+ items,
178
+ label,
179
+ }: {
180
+ icon: React.ReactNode;
181
+ label: string;
182
+ items: Array<{ label: string; onSelect?: () => void }>;
183
+ }) {
184
+ return (
185
+ <DropdownMenu>
186
+ <DropdownMenuTrigger asChild>
187
+ <button type="button" className={actionButtonClassName}>
188
+ <span className="text-muted-foreground opacity-60">{icon}</span>
189
+ <span className="opacity-65 group-hover:opacity-100">{label}</span>
190
+ <ChevronUp className="-ml-1.5 size-3 text-muted-foreground" />
191
+ </button>
192
+ </DropdownMenuTrigger>
193
+ <DropdownMenuContent side="top" align="start">
194
+ {items.map((item) => (
195
+ <DropdownMenuItem
196
+ key={item.label}
197
+ onSelect={(event) => {
198
+ event.preventDefault();
199
+ item.onSelect?.();
200
+ }}
201
+ >
202
+ {item.label}
203
+ </DropdownMenuItem>
204
+ ))}
205
+ </DropdownMenuContent>
206
+ </DropdownMenu>
207
+ );
208
+ }
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+
5
+ import type { TListElement } from 'platejs';
6
+
7
+ import { isOrderedList } from '@platejs/list';
8
+ import {
9
+ useTodoListElement,
10
+ useTodoListElementState,
11
+ } from '@platejs/list/react';
12
+ import {
13
+ type PlateElementProps,
14
+ type RenderNodeWrapper,
15
+ useReadOnly,
16
+ } from 'platejs/react';
17
+ import { KEYS } from 'platejs';
18
+
19
+ import { cn } from '@/lib/utils';
20
+
21
+ const todoKey = KEYS.listTodo === 'todo' ? 'todo' : KEYS.listTodo;
22
+
23
+ const config: Record<
24
+ string,
25
+ {
26
+ Li: React.FC<PlateElementProps>;
27
+ Marker: React.FC<PlateElementProps>;
28
+ }
29
+ > = {
30
+ [todoKey]: {
31
+ Li: TodoLi,
32
+ Marker: TodoMarker,
33
+ },
34
+ };
35
+
36
+ export const BlockList: RenderNodeWrapper = (props) => {
37
+ if (!props.element.listStyleType) return;
38
+
39
+ return (props) => <List {...props} />;
40
+ };
41
+
42
+ function List(props: PlateElementProps) {
43
+ const { listStart, listStyleType } = props.element as TListElement;
44
+ const { Li, Marker } = config[listStyleType] ?? {};
45
+ const List = isOrderedList(props.element) ? 'ol' : 'ul';
46
+
47
+ return (
48
+ <List
49
+ className="relative m-0 p-0"
50
+ style={{ listStyleType }}
51
+ start={listStart}
52
+ >
53
+ {Marker && <Marker {...props} />}
54
+ {Li ? <Li {...props} /> : <li>{props.children}</li>}
55
+ </List>
56
+ );
57
+ }
58
+
59
+ function TodoMarker(props: PlateElementProps) {
60
+ const state = useTodoListElementState({ element: props.element });
61
+ const { checkboxProps } = useTodoListElement(state);
62
+ const { checked, onCheckedChange, ...rest } = checkboxProps as {
63
+ checked?: boolean;
64
+ onCheckedChange?: (value: boolean) => void;
65
+ } & React.InputHTMLAttributes<HTMLInputElement>;
66
+ const readOnly = useReadOnly();
67
+
68
+ return (
69
+ <span contentEditable={false}>
70
+ <input
71
+ type="checkbox"
72
+ className={cn('-left-6 absolute top-1 size-4 accent-primary')}
73
+ disabled={readOnly}
74
+ checked={!!checked}
75
+ onChange={(event) => onCheckedChange?.(event.target.checked)}
76
+ {...rest}
77
+ />
78
+ </span>
79
+ );
80
+ }
81
+
82
+ function TodoLi(props: PlateElementProps) {
83
+ return (
84
+ <li
85
+ className={cn(
86
+ 'list-none',
87
+ (props.element.checked as boolean) &&
88
+ 'text-muted-foreground line-through'
89
+ )}
90
+ >
91
+ {props.children}
92
+ </li>
93
+ );
94
+ }
@@ -1,59 +1,58 @@
1
- import * as React from "react"
2
- import { Slot } from "@radix-ui/react-slot"
3
- import { cva, type VariantProps } from "class-variance-authority"
1
+ import { Slot } from "@radix-ui/react-slot";
2
+ import { type VariantProps, cva } from "class-variance-authority";
4
3
 
5
- import { cn } from "@/lib/utils"
4
+ import { cn } from "@/lib/utils";
6
5
 
7
6
  const buttonVariants = cva(
8
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
- {
10
- variants: {
11
- variant: {
12
- default:
13
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14
- destructive:
15
- "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
- outline:
17
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18
- secondary:
19
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20
- ghost:
21
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22
- link: "text-primary underline-offset-4 hover:underline",
23
- },
24
- size: {
25
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
26
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
- icon: "size-9",
29
- },
30
- },
31
- defaultVariants: {
32
- variant: "default",
33
- size: "default",
34
- },
35
- }
36
- )
7
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ },
35
+ );
37
36
 
38
37
  function Button({
39
- className,
40
- variant,
41
- size,
42
- asChild = false,
43
- ...props
38
+ className,
39
+ variant,
40
+ size,
41
+ asChild = false,
42
+ ...props
44
43
  }: React.ComponentProps<"button"> &
45
- VariantProps<typeof buttonVariants> & {
46
- asChild?: boolean
47
- }) {
48
- const Comp = asChild ? Slot : "button"
44
+ VariantProps<typeof buttonVariants> & {
45
+ asChild?: boolean;
46
+ }) {
47
+ const Comp = asChild ? Slot : "button";
49
48
 
50
- return (
51
- <Comp
52
- data-slot="button"
53
- className={cn(buttonVariants({ variant, size, className }))}
54
- {...props}
55
- />
56
- )
49
+ return (
50
+ <Comp
51
+ data-slot="button"
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ {...props}
54
+ />
55
+ );
57
56
  }
58
57
 
59
- export { Button, buttonVariants }
58
+ export { Button, buttonVariants };
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
5
+ import { useCalloutEmojiPicker } from '@platejs/callout/react';
6
+ import { useEmojiDropdownMenuState } from '@platejs/emoji/react';
7
+ import { PlateElement } from 'platejs/react';
8
+
9
+ import { Button } from '@/components/ui/button';
10
+ import { cn } from '@/lib/utils';
11
+
12
+ import { EmojiPicker, EmojiPopover } from './emoji-toolbar-button';
13
+
14
+ export function CalloutElement({
15
+ attributes,
16
+ children,
17
+ className,
18
+ ...props
19
+ }: React.ComponentProps<typeof PlateElement>) {
20
+ const { emojiPickerState, isOpen, setIsOpen } = useEmojiDropdownMenuState({
21
+ closeOnSelect: true,
22
+ });
23
+
24
+ const { emojiToolbarDropdownProps, props: calloutProps } =
25
+ useCalloutEmojiPicker({
26
+ isOpen,
27
+ setIsOpen,
28
+ });
29
+
30
+ return (
31
+ <PlateElement
32
+ className={cn('my-1 flex rounded-sm bg-muted p-4 pl-3', className)}
33
+ style={{
34
+ backgroundColor: props.element.backgroundColor as string | undefined,
35
+ }}
36
+ attributes={{
37
+ ...attributes,
38
+ 'data-plate-open-context-menu': true,
39
+ }}
40
+ {...props}
41
+ >
42
+ <div className="flex w-full gap-2 rounded-md">
43
+ <EmojiPopover
44
+ {...emojiToolbarDropdownProps}
45
+ control={
46
+ <Button
47
+ variant="ghost"
48
+ className="size-6 select-none p-1 text-[18px] hover:bg-muted-foreground/15"
49
+ style={{
50
+ fontFamily:
51
+ '"Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols',
52
+ }}
53
+ contentEditable={false}
54
+ >
55
+ {(props.element.icon as string) || '💡'}
56
+ </Button>
57
+ }
58
+ >
59
+ <EmojiPicker {...emojiPickerState} {...calloutProps} />
60
+ </EmojiPopover>
61
+ <div className="w-full">{children}</div>
62
+ </div>
63
+ </PlateElement>
64
+ );
65
+ }
@@ -1,53 +1,53 @@
1
- import type { VariantProps } from 'class-variance-authority';
1
+ import type { VariantProps } from "class-variance-authority";
2
2
 
3
- import { cva } from 'class-variance-authority';
4
- import { type PlateStaticProps, PlateStatic } from 'platejs';
3
+ import { cva } from "class-variance-authority";
4
+ import { PlateStatic, type PlateStaticProps } from "platejs";
5
5
 
6
- import { cn } from '@/lib/utils';
6
+ import { cn } from "@/lib/utils";
7
7
 
8
8
  export const editorVariants = cva(
9
- cn(
10
- 'group/editor',
11
- 'relative w-full cursor-text overflow-x-hidden break-words whitespace-pre-wrap select-text',
12
- 'rounded-md ring-offset-background focus-visible:outline-none',
13
- 'placeholder:text-muted-foreground/80 **:data-slate-placeholder:top-[auto_!important] **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!',
14
- '[&_strong]:font-bold'
15
- ),
16
- {
17
- defaultVariants: {
18
- variant: 'none',
19
- },
20
- variants: {
21
- disabled: {
22
- true: 'cursor-not-allowed opacity-50',
23
- },
24
- focused: {
25
- true: 'ring-2 ring-ring ring-offset-2',
26
- },
27
- variant: {
28
- ai: 'w-full px-0 text-base md:text-sm',
29
- aiChat:
30
- 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-5 py-3 text-base md:text-sm',
31
- default:
32
- 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
33
- demo: 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
34
- fullWidth: 'size-full px-16 pt-4 pb-72 text-base sm:px-24',
35
- none: '',
36
- select: 'px-3 py-2 text-base data-readonly:w-fit',
37
- },
38
- },
39
- }
9
+ cn(
10
+ "group/editor",
11
+ "relative w-full cursor-text overflow-x-hidden break-words whitespace-pre-wrap select-text",
12
+ "rounded-md ring-offset-background focus-visible:outline-none",
13
+ "placeholder:text-muted-foreground/80 **:data-slate-placeholder:top-[auto_!important] **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!",
14
+ "[&_strong]:font-bold",
15
+ ),
16
+ {
17
+ defaultVariants: {
18
+ variant: "none",
19
+ },
20
+ variants: {
21
+ disabled: {
22
+ true: "cursor-not-allowed opacity-50",
23
+ },
24
+ focused: {
25
+ true: "ring-2 ring-ring ring-offset-2",
26
+ },
27
+ variant: {
28
+ ai: "w-full px-0 text-base md:text-sm",
29
+ aiChat:
30
+ "max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-5 py-3 text-base md:text-sm",
31
+ default:
32
+ "size-full px-16 pt-4 pb-24 text-base sm:px-[max(64px,calc(50%-350px))]",
33
+ demo: "size-full px-16 pt-4 pb-24 text-base sm:px-[max(64px,calc(50%-350px))]",
34
+ fullWidth: "size-full px-16 pt-4 pb-24 text-base sm:px-24",
35
+ none: "",
36
+ select: "px-3 py-2 text-base data-readonly:w-fit",
37
+ },
38
+ },
39
+ },
40
40
  );
41
41
 
42
42
  export function EditorStatic({
43
- className,
44
- variant,
45
- ...props
43
+ className,
44
+ variant,
45
+ ...props
46
46
  }: PlateStaticProps & VariantProps<typeof editorVariants>) {
47
- return (
48
- <PlateStatic
49
- className={cn(editorVariants({ variant }), className)}
50
- {...props}
51
- />
52
- );
47
+ return (
48
+ <PlateStatic
49
+ className={cn(editorVariants({ variant }), className)}
50
+ {...props}
51
+ />
52
+ );
53
53
  }