返回 presentation-ai
before-after-side-static.tsx
根目录 / src / components / notebook / presentation / editor / custom-elements / static / before-after-side-static.tsx
1 import { ArrowDown, ArrowRight } from "lucide-react";
2 import { NodeApi, PathApi } from "platejs";
3 import { SlateElement, type SlateElementProps } from "platejs/static";
4
5 import { cn } from "@/lib/utils";
6 import { type TBeforeAfterGroupElement } from "../../plugins/before-after-plugin";
7 import { getAlignmentClasses, getDefaultColumnSize } from "../../utils";
8
9 export function BeforeAfterSideStatic(props: SlateElementProps) {
10 const path = props.editor.api.findPath(props.element) ?? [-1];
11 const parentPath = PathApi.parent(path);
12 const parentElement = NodeApi.get(props.editor, parentPath);
13
14 const { alignment = "center" } = parentElement as {
15 alignment?: "left" | "center" | "right";
16 color?: string;
17 columnSize?: "sm" | "md" | "lg" | "xl";
18 children?: unknown[];
19 };
20 const index = path.at(-1) ?? 0;
21 const childCount =
22 (parentElement as { children?: unknown[] })?.children?.length ?? 0;
23 const columnSize =
24 (parentElement as TBeforeAfterGroupElement | undefined)?.columnSize ??
25 getDefaultColumnSize(childCount);
26 const columns = getColumnCount(columnSize);
27 const isVertical = columnSize === "xl";
28 const showSeparator = shouldShowSeparator({
29 columns,
30 index,
31 itemCount: childCount,
32 });
33 const shouldSpanFullRow =
34 columns === 2 && childCount % columns === 1 && index === childCount - 1;
35 const accentColor =
36 (parentElement?.color as string) || "var(--presentation-primary)";
37
38 return (
39 <div
40 className={cn(
41 "relative flex h-full min-w-0 flex-1 items-stretch",
42 isVertical && "flex-col items-center gap-4",
43 )}
44 style={shouldSpanFullRow ? { gridColumn: "1 / -1" } : undefined}
45 >
46 <div
47 data-bg-export="true"
48 className={cn(
49 "grid h-full w-full rounded-xl border bg-card p-6 shadow-md",
50 "border-t-4",
51 )}
52 style={{
53 backgroundColor: "var(--presentation-background)",
54 color: "var(--presentation-text)",
55 borderColor: "hsl(var(--border))",
56 borderTopColor:
57 (parentElement?.color as string) || "var(--presentation-primary)",
58 }}
59 >
60 <SlateElement
61 {...props}
62 className={cn("h-full", getAlignmentClasses(alignment))}
63 >
64 {props.children}
65 </SlateElement>
66 </div>
67 {showSeparator ? (
68 <BeforeAfterSeparator
69 accentColor={accentColor}
70 isVertical={isVertical}
71 />
72 ) : null}
73 </div>
74 );
75 }
76
77 function BeforeAfterSeparator({
78 accentColor,
79 isVertical,
80 }: {
81 accentColor: string;
82 isVertical: boolean;
83 }) {
84 const ArrowIcon = isVertical ? ArrowDown : ArrowRight;
85
86 return (
87 <div
88 className={cn(
89 "flex shrink-0 items-center justify-center self-center",
90 !isVertical && "absolute -right-9 top-1/2 z-10 -translate-y-1/2",
91 )}
92 aria-hidden
93 >
94 <div
95 data-shape="ellipse"
96 data-shape-text={isVertical ? "↓" : "→"}
97 data-fill-color={accentColor}
98 data-text-color="var(--presentation-background)"
99 className="grid size-12 place-items-center rounded-full text-sm font-bold shadow-xs"
100 style={{
101 backgroundColor: accentColor,
102 color: "var(--presentation-background)",
103 pointerEvents: "none",
104 }}
105 >
106 <ArrowIcon className="size-5" strokeWidth={2.5} />
107 </div>
108 </div>
109 );
110 }
111
112 function getColumnCount(columnSize: TBeforeAfterGroupElement["columnSize"]) {
113 const columnSizeToColumns = {
114 lg: 2,
115 md: 3,
116 sm: 4,
117 xl: 1,
118 } satisfies Record<
119 NonNullable<TBeforeAfterGroupElement["columnSize"]>,
120 number
121 >;
122
123 return columnSize ? columnSizeToColumns[columnSize] : columnSizeToColumns.md;
124 }
125
126 function shouldShowSeparator({
127 columns,
128 index,
129 itemCount,
130 }: {
131 columns: number;
132 index: number;
133 itemCount: number;
134 }) {
135 const isLastItem = index === itemCount - 1;
136 const isRowEnd = (index + 1) % columns === 0;
137
138 return !isLastItem && (columns === 1 || !isRowEnd);
139 }
140
140 lines Plain Text