返回 presentation-ai
circular-grid-item.tsx
根目录 / src / components / notebook / presentation / editor / custom-elements / circular-grid-item.tsx
1 "use client";
2
3 import { NodeApi, PathApi } from "platejs";
4 import {
5 PlateElement,
6 useReadOnly,
7 type PlateElementProps,
8 } from "platejs/react";
9
10 import { cn } from "@/lib/utils";
11 import { CIRCULAR_GRID_MAX_ITEMS } from "../lib";
12 import {
13 type TCircularGridGroupElement,
14 type TCircularGridItemElement,
15 } from "../plugins/diagram-components-plugin";
16 import {
17 getCircularGridItemIndex,
18 getCircularGridItemPosition,
19 getCircularGridItemSelfAlignment,
20 getCircularGridItemTransform,
21 getCircularGridPointerDirection,
22 isVisibleCircularGridItem,
23 useCircularGridLayoutContext,
24 type CircularGridPointerDirection,
25 } from "./circular-grid";
26 import { getPresentationAccentColor } from "./color-utils";
27 import { getSiblingIndexContext } from "./sibling-index";
28 import { getSmartLayoutStepColor } from "./smart-layout-gradient";
29
30 /** Size of the rotated square that creates the speech-bubble triangle. */
31 const POINTER_SIZE = 16;
32 const POINTER_HALF = POINTER_SIZE / 2;
33 /** How far inward from the corner the pointer sits. */
34 const POINTER_EDGE_INSET = 20;
35
36 /**
37 * Returns absolute positioning CSS for the speech-bubble pointer
38 * based on which direction it should point.
39 */
40 function getPointerPositionStyle(
41 direction: CircularGridPointerDirection,
42 ): React.CSSProperties {
43 switch (direction) {
44 case "bottom":
45 return { bottom: -POINTER_HALF, left: "50%", marginLeft: -POINTER_HALF };
46 case "bottom-right":
47 return { bottom: -POINTER_HALF, right: POINTER_EDGE_INSET };
48 case "bottom-left":
49 return { bottom: -POINTER_HALF, left: POINTER_EDGE_INSET };
50 case "right":
51 return { right: -POINTER_HALF, top: "50%", marginTop: -POINTER_HALF };
52 case "left":
53 return { left: -POINTER_HALF, top: "50%", marginTop: -POINTER_HALF };
54 case "top-right":
55 return { top: -POINTER_HALF, right: POINTER_EDGE_INSET };
56 case "top-left":
57 return { top: -POINTER_HALF, left: POINTER_EDGE_INSET };
58 }
59 }
60
61 export function CircularGridItem(
62 props: PlateElementProps<TCircularGridItemElement>,
63 ) {
64 const layoutContext = useCircularGridLayoutContext();
65 const { index: fallbackIndex, parentElement } =
66 getSiblingIndexContext<TCircularGridGroupElement>(
67 props.editor,
68 props.element,
69 props.path,
70 );
71 const fallbackParentPath = PathApi.parent(props.path);
72 const fallbackParentElement = NodeApi.get(
73 props.editor,
74 fallbackParentPath,
75 ) as TCircularGridGroupElement | undefined;
76 const resolvedParentElement =
77 layoutContext?.parentElement ?? parentElement ?? fallbackParentElement;
78 const index = getCircularGridItemIndex(
79 resolvedParentElement,
80 props.element,
81 fallbackIndex,
82 );
83 const total = Math.min(
84 layoutContext?.total ?? resolvedParentElement?.children?.length ?? 1,
85 CIRCULAR_GRID_MAX_ITEMS,
86 );
87 const alignment =
88 props.element.alignment ??
89 layoutContext?.alignment ??
90 resolvedParentElement?.alignment ??
91 "center";
92
93 const readOnly = useReadOnly();
94 const position = getCircularGridItemPosition(index, total);
95 const selfAlign = getCircularGridItemSelfAlignment(index, total);
96 // In edit mode, BlockDraggable wrapper already applies the transform via
97 // getGridStyleForElement. Only apply it ourselves in readonly/present mode.
98 const transform = readOnly
99 ? getCircularGridItemTransform(index, total)
100 : undefined;
101 const pointerDir = getCircularGridPointerDirection(index, total);
102 const bgColor = getPresentationAccentColor(
103 props.element,
104 resolvedParentElement,
105 getSmartLayoutStepColor(index, total),
106 );
107 const isVisible = isVisibleCircularGridItem(index);
108
109 return (
110 <PlateElement
111 {...props}
112 className={cn(
113 "relative z-10 min-h-22 min-w-40 overflow-visible",
114 !isVisible && "hidden",
115 )}
116 style={{
117 gridRow: position.gridRow,
118 gridColumn: position.gridColumn,
119 justifySelf: selfAlign.justifySelf,
120 alignSelf: selfAlign.alignSelf,
121 transform,
122 }}
123 >
124 {/* Speech-bubble pointer (sits behind content) */}
125 <div
126 className="absolute z-0 size-4 rotate-45"
127 style={{
128 background: bgColor,
129 ...getPointerPositionStyle(pointerDir),
130 }}
131 aria-hidden="true"
132 data-decor="true"
133 data-bg-export="true"
134 />
135 {/* Content box (above pointer so the inner half is hidden) */}
136 <div
137 className={cn(
138 "relative z-1 min-h-22 min-w-0 rounded-xl px-4 py-3 text-(--presentation-foreground)",
139 alignment === "left" && "text-left",
140 alignment === "center" && "text-center",
141 alignment === "right" && "text-right",
142 "[&_h1]:text-xl [&_h2]:text-xl [&_h3]:text-xl [&_h4]:text-lg [&_p]:mt-1 [&_p]:text-sm",
143 )}
144 style={
145 {
146 background: bgColor,
147 "--presentation-heading": "var(--presentation-card-background)",
148 "--presentation-text": "var(--presentation-card-background)",
149 } as React.CSSProperties
150 }
151 data-bg-export="true"
152 >
153 {props.children}
154 </div>
155 </PlateElement>
156 );
157 }
158
158 lines Plain Text