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