@groupher/rich-editor 0.0.8 → 0.0.10
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.
- package/dist/rich-editor.es.js +31268 -20037
- package/dist/rich-editor.umd.js +77 -96
- package/package.json +17 -2
- package/src/RichEditor.tsx +204 -92
- package/src/components/editor/editor-kit.tsx +27 -0
- package/src/components/editor/plugins/autoformat-kit.tsx +99 -0
- package/src/components/editor/plugins/callout-kit.tsx +7 -0
- package/src/components/editor/plugins/emoji-kit.tsx +14 -0
- package/src/components/editor/plugins/indent-kit.tsx +12 -0
- package/src/components/editor/plugins/link-kit.tsx +13 -0
- package/src/components/editor/plugins/list-kit.tsx +17 -0
- package/src/components/editor/plugins/mention-kit.tsx +17 -0
- package/src/components/editor/plugins/slash-kit.tsx +15 -0
- package/src/components/editor/plugins/toggle-kit.tsx +7 -0
- package/src/components/ui/action-bar.tsx +208 -0
- package/src/components/ui/block-list.tsx +94 -0
- package/src/components/ui/button.tsx +49 -50
- package/src/components/ui/callout-node.tsx +65 -0
- package/src/components/ui/editor-static.tsx +44 -44
- package/src/components/ui/editor.tsx +107 -107
- package/src/components/ui/emoji-node.tsx +71 -0
- package/src/components/ui/emoji-toolbar-button.tsx +618 -0
- package/src/components/ui/floating-toolbar.tsx +86 -0
- package/src/components/ui/inline-combobox.tsx +414 -0
- package/src/components/ui/link-node.tsx +31 -0
- package/src/components/ui/link-toolbar-button.tsx +33 -0
- package/src/components/ui/mention-node.tsx +126 -0
- package/src/components/ui/slash-node.tsx +191 -0
- package/src/components/ui/toggle-node.tsx +36 -0
- package/src/components/ui/toolbar.tsx +10 -10
- package/src/hooks/use-debounce.ts +15 -0
- package/src/hooks/use-mounted.ts +11 -0
- package/src/i18n.tsx +155 -0
- package/src/main.tsx +35 -14
- package/src/mention-context.tsx +32 -0
- 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
|
|
2
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
className,
|
|
39
|
+
variant,
|
|
40
|
+
size,
|
|
41
|
+
asChild = false,
|
|
42
|
+
...props
|
|
44
43
|
}: React.ComponentProps<"button"> &
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
VariantProps<typeof buttonVariants> & {
|
|
45
|
+
asChild?: boolean;
|
|
46
|
+
}) {
|
|
47
|
+
const Comp = asChild ? Slot : "button";
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
1
|
+
import type { VariantProps } from "class-variance-authority";
|
|
2
2
|
|
|
3
|
-
import { cva } from
|
|
4
|
-
import { type PlateStaticProps
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import { PlateStatic, type PlateStaticProps } from "platejs";
|
|
5
5
|
|
|
6
|
-
import { cn } from
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
7
|
|
|
8
8
|
export const editorVariants = cva(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
className,
|
|
44
|
+
variant,
|
|
45
|
+
...props
|
|
46
46
|
}: PlateStaticProps & VariantProps<typeof editorVariants>) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
return (
|
|
48
|
+
<PlateStatic
|
|
49
|
+
className={cn(editorVariants({ variant }), className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
53
|
}
|