返回 presentation-ai
snake.tsx
1 "use client";
2
3 import { PlateElement, type PlateElementProps } from "platejs/react";
4
5 import { useForceUpdateChildrenOnLengthChange } from "@/hooks/presentation/useForceUpdateChildrenOnLengthChange";
6 import { cn } from "@/lib/utils";
7 import { type TSnakeGroupElement } from "../plugins/diagram-components-plugin";
8 import { getAlignmentClasses } from "../utils";
9 import { getDiagramFitFrameStyle, useDiagramFitScale } from "./diagram-fit";
10 import { getSmartLayoutStepColor } from "./smart-layout-gradient";
11 import {
12 buildSnakeArrowPath,
13 getSnakeArrowBaselineY,
14 getSnakeArrowEndX,
15 getSnakeArrowStartX,
16 getSvgWidth,
17 SNAKE_COL_WIDTH,
18 SNAKE_EDGE_PADDING_X,
19 SNAKE_END_ARROW_LENGTH,
20 SNAKE_GRID_ROWS,
21 SNAKE_LAYOUT_HEIGHT_PX,
22 SNAKE_START_DOT_GAP,
23 SNAKE_START_DOT_RADIUS,
24 SNAKE_SVG_HEIGHT,
25 } from "./snake-shared";
26
27 export default function Snake(props: PlateElementProps<TSnakeGroupElement>) {
28 const { alignment = "center" } = props.element;
29 const total = props.element.children.length || 1;
30 const svgWidth = getSvgWidth(total);
31 const layoutWidth = svgWidth + SNAKE_EDGE_PADDING_X * 2;
32 const startBaselineY = getSnakeArrowBaselineY(0);
33 const endBaselineY = getSnakeArrowBaselineY(total - 1);
34 const { containerRef, fitStyle, frameStyle, layoutRef } =
35 useDiagramFitScale<HTMLDivElement>(layoutWidth, SNAKE_LAYOUT_HEIGHT_PX);
36
37 useForceUpdateChildrenOnLengthChange(props.editor, props.element);
38
39 return (
40 <PlateElement {...props} className="my-4">
41 <div
42 ref={containerRef}
43 className={cn(
44 "w-full overflow-visible",
45 getAlignmentClasses(alignment),
46 )}
47 >
48 <div
49 style={{
50 ...frameStyle,
51 ...getDiagramFitFrameStyle(alignment),
52 }}
53 >
54 <div
55 ref={layoutRef}
56 className="relative overflow-visible"
57 style={{
58 ...fitStyle,
59 height: SNAKE_LAYOUT_HEIGHT_PX,
60 }}
61 >
62 {/* SVG decorations: alternating arrow lanes */}
63 <svg
64 className="pointer-events-none absolute inset-0 h-full w-full overflow-visible"
65 viewBox={`${-SNAKE_EDGE_PADDING_X} 0 ${layoutWidth} ${SNAKE_SVG_HEIGHT}`}
66 preserveAspectRatio="none"
67 aria-hidden="true"
68 data-decor="true"
69 >
70 <defs>
71 <marker
72 id="presentation-snake-arrow"
73 markerHeight="10"
74 markerWidth="10"
75 orient="auto"
76 refX="8"
77 refY="5"
78 viewBox="0 0 10 10"
79 >
80 <path
81 d="M 1 1 L 9 5 L 1 9"
82 fill="none"
83 stroke="context-stroke"
84 strokeLinecap="round"
85 strokeLinejoin="round"
86 strokeWidth="1.4"
87 />
88 </marker>
89 </defs>
90
91 {/* Start dot before first item */}
92 <circle
93 cx={
94 getSnakeArrowStartX(0) -
95 SNAKE_START_DOT_GAP -
96 SNAKE_START_DOT_RADIUS
97 }
98 cy={startBaselineY}
99 r={SNAKE_START_DOT_RADIUS}
100 fill={getSmartLayoutStepColor(0, total)}
101 />
102 <line
103 x1={
104 getSnakeArrowStartX(0) -
105 SNAKE_START_DOT_GAP -
106 SNAKE_START_DOT_RADIUS
107 }
108 y1={startBaselineY}
109 x2={getSnakeArrowStartX(0)}
110 y2={startBaselineY}
111 stroke={getSmartLayoutStepColor(0, total)}
112 strokeLinecap="round"
113 strokeWidth="3"
114 />
115
116 {Array.from({ length: total }).map((_, index) => {
117 const isLast = index === total - 1;
118
119 return (
120 <path
121 key={`snake-arrow-${index}`}
122 d={buildSnakeArrowPath(index)}
123 fill="none"
124 markerEnd={
125 isLast ? undefined : "url(#presentation-snake-arrow)"
126 }
127 stroke={getSmartLayoutStepColor(index, total)}
128 strokeLinecap="round"
129 strokeWidth="3"
130 />
131 );
132 })}
133
134 {/* End arrow after last item */}
135 {total > 0 && (
136 <line
137 x1={getSnakeArrowEndX(total - 1)}
138 y1={endBaselineY}
139 x2={getSnakeArrowEndX(total - 1) + SNAKE_END_ARROW_LENGTH}
140 y2={endBaselineY}
141 stroke={getSmartLayoutStepColor(total - 1, total)}
142 strokeWidth="3"
143 strokeLinecap="round"
144 markerEnd="url(#presentation-snake-arrow)"
145 />
146 )}
147 </svg>
148
149 {/* CSS Grid layout for the text items */}
150 <div
151 className="absolute top-0 bottom-0"
152 style={{
153 display: "grid",
154 gridTemplateColumns: `repeat(${total}, ${SNAKE_COL_WIDTH}px)`,
155 gridTemplateRows: `repeat(${SNAKE_GRID_ROWS}, 1fr)`,
156 left: SNAKE_EDGE_PADDING_X,
157 width: svgWidth,
158 }}
159 >
160 {props.children}
161 </div>
162 </div>
163 </div>
164 </div>
165 </PlateElement>
166 );
167 }
168
168 lines Plain Text