返回 presentation-ai
callout-node.tsx
根目录 / src / components / plate / ui / callout-node.tsx
1 "use client";
2
3 import { useBlockSelected } from "@platejs/selection/react";
4 import { PlateElement } from "platejs/react";
5 import type * as React from "react";
6
7 import { PresentationIcon } from "@/components/notebook/presentation/editor/custom-elements/presentation-icon";
8 import { IconPicker } from "@/components/ui/icon-picker";
9 import { cn } from "@/lib/utils";
10 import { CALLOUT_VARIANTS, getCalloutVariant } from "./callout-variants";
11
12 type CalloutElementData = {
13 alignment?: unknown;
14 backgroundColor?: unknown;
15 color?: unknown;
16 icon?: unknown;
17 textColor?: unknown;
18 variant?: unknown;
19 };
20
21 function getCalloutElementData(element: unknown): CalloutElementData {
22 return typeof element === "object" && element !== null
23 ? (element as CalloutElementData)
24 : {};
25 }
26
27 function isIconPickerName(value: unknown): value is string {
28 return typeof value === "string" && /^[A-Z][A-Za-z0-9]+$/.test(value.trim());
29 }
30
31 function getCalloutJustifyClass(alignment: unknown) {
32 if (alignment === "left") return "justify-start";
33 if (alignment === "right") return "justify-end";
34 return "justify-start";
35 }
36
37 function getCalloutTextAlignClass(alignment: unknown) {
38 if (alignment === "left") return "text-left";
39 if (alignment === "right") return "text-right";
40 return "text-left";
41 }
42
43 export function CalloutElement({
44 attributes,
45 children,
46 className,
47 ...props
48 }: React.ComponentProps<typeof PlateElement>) {
49 const isBlockSelected = useBlockSelected();
50 const elementData = getCalloutElementData(props.element);
51 const variant = getCalloutVariant(elementData.variant);
52 const variantConfig = CALLOUT_VARIANTS[variant];
53 const icon = isIconPickerName(elementData.icon)
54 ? elementData.icon
55 : variantConfig.icon;
56 const textColor =
57 typeof elementData.textColor === "string"
58 ? elementData.textColor
59 : typeof elementData.color === "string"
60 ? elementData.color
61 : undefined;
62 const backgroundColor =
63 typeof elementData.backgroundColor === "string"
64 ? elementData.backgroundColor
65 : variantConfig.backgroundColor;
66 const alignment = elementData.alignment;
67
68 const handleIconSelect = (iconName: string) => {
69 const path = props.editor.api.findPath(props.element);
70 if (!path) return;
71
72 props.editor.tf.setNodes({ icon: iconName }, { at: path });
73 };
74
75 return (
76 <PlateElement
77 className={cn(
78 "slate-selectable relative my-1 flex rounded-sm bg-muted p-4",
79 getCalloutJustifyClass(alignment),
80 !textColor && variantConfig.textClassName,
81 isBlockSelected &&
82 "before:pointer-events-none before:absolute before:inset-0 before:z-1 before:rounded-sm before:bg-brand/13 before:content-['']",
83 "caret-current! **:caret-current!",
84 "**:data-[slate-node='element']:text-current!",
85 "**:[[placeholder]]:before:text-current! **:[[placeholder]]:before:opacity-70!",
86 className,
87 )}
88 style={{
89 backgroundColor,
90 color: textColor,
91 }}
92 attributes={{
93 ...attributes,
94 "data-plate-open-context-menu": true,
95 }}
96 {...props}
97 >
98 <div
99 className={cn(
100 "relative z-2 flex w-full gap-2 rounded-md",
101 getCalloutJustifyClass(alignment),
102 )}
103 >
104 <IconPicker
105 className={cn(
106 "size-6 shrink-0 border-0 bg-transparent p-1 shadow-none hover:bg-muted-foreground/15",
107 !textColor && variantConfig.textClassName,
108 )}
109 style={{ color: textColor }}
110 defaultIcon={icon}
111 onIconSelect={handleIconSelect}
112 onIconRemove={() => handleIconSelect(variantConfig.icon)}
113 placeholder={
114 <PresentationIcon
115 icon={variantConfig.icon}
116 className="flex items-center justify-center"
117 size={16}
118 />
119 }
120 size="sm"
121 />
122 <div
123 className={cn(
124 "min-w-0 max-w-[calc(100%-2rem)]",
125 getCalloutTextAlignClass(alignment),
126 )}
127 >
128 {children}
129 </div>
130 </div>
131 </PlateElement>
132 );
133 }
134
134 lines Plain Text