返回 presentation-ai
stats-item-static.tsx
根目录 / src / components / notebook / presentation / editor / custom-elements / static / stats-item-static.tsx
1 import { Star } 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 {
7 type TStatsGroupElement,
8 type TStatsItemElement,
9 } from "../../plugins/stats-plugin";
10 import { getAlignmentClasses } from "../../utils";
11
12 export function StatsItemStatic(props: SlateElementProps<TStatsItemElement>) {
13 const path = props.editor.api.findPath(props.element) ?? [-1];
14 const parentPath = PathApi.parent(path);
15 const parentElement = NodeApi.get(
16 props.editor,
17 parentPath,
18 ) as TStatsGroupElement;
19
20 const statsType = parentElement?.statsType ?? "plain";
21 const { stat = "0" } = props.element;
22
23 // Get alignment - use item alignment if set, otherwise inherit from parent
24 const itemAlignment = props.element.alignment;
25 const parentAlignment = parentElement?.alignment;
26 const alignment = itemAlignment ?? parentAlignment ?? "left";
27
28 const renderStatVisual = () => {
29 const statValue = parseFloat(stat) || 0;
30 const percentage = Math.min(Math.max(statValue, 0), 100);
31 const primaryColor = parentElement?.color || "var(--presentation-primary)";
32 const ringTrackColor =
33 "var(--presentation-card-background, var(--presentation-primary))";
34 const ringProgressColor =
35 parentElement?.color ||
36 "var(--presentation-smart-layout, var(--presentation-primary))";
37
38 switch (statsType) {
39 case "plain":
40 return (
41 <div
42 className={cn(
43 "w-full min-w-0 text-6xl font-bold text-primary wrap-anywhere",
44 getAlignmentClasses(alignment),
45 )}
46 style={{
47 color: primaryColor,
48 }}
49 >
50 {stat}
51 </div>
52 );
53
54 case "circle":
55 return (
56 <div className="relative size-32">
57 <svg className="h-full w-full" viewBox="0 0 100 100">
58 <circle
59 cx="50"
60 cy="50"
61 r="45"
62 fill="none"
63 stroke={ringTrackColor}
64 strokeWidth="8"
65 />
66 <circle
67 cx="50"
68 cy="50"
69 r="45"
70 fill="none"
71 stroke={ringProgressColor}
72 strokeWidth="8"
73 strokeDasharray={`${2 * Math.PI * 45}`}
74 strokeDashoffset={`${2 * Math.PI * 45 * (1 - percentage / 100)}`}
75 transform="rotate(-90 50 50)"
76 />
77 </svg>
78 <div className="absolute inset-0 flex items-center justify-center">
79 <span
80 className="text-2xl font-bold"
81 style={{
82 color: ringProgressColor,
83 }}
84 >
85 {stat}
86 </span>
87 </div>
88 </div>
89 );
90
91 case "circle-bold":
92 return (
93 <div className="relative size-35">
94 <svg className="h-full w-full" viewBox="0 0 100 100">
95 <circle
96 cx="50"
97 cy="50"
98 r="40"
99 fill="none"
100 stroke={ringTrackColor}
101 strokeWidth="12"
102 />
103 <circle
104 cx="50"
105 cy="50"
106 r="40"
107 fill="none"
108 stroke={ringProgressColor}
109 strokeWidth="12"
110 strokeDasharray={`${2 * Math.PI * 40}`}
111 strokeDashoffset={`${2 * Math.PI * 40 * (1 - percentage / 100)}`}
112 transform="rotate(-90 50 50)"
113 />
114 <circle
115 cx="50"
116 cy="50"
117 r="30"
118 fill="none"
119 stroke={ringTrackColor}
120 strokeWidth="4"
121 opacity="0.4"
122 />
123 </svg>
124 <div className="absolute inset-0 flex items-center justify-center">
125 <span
126 className="text-2xl font-bold"
127 style={{
128 color: ringProgressColor,
129 }}
130 >
131 {stat}
132 </span>
133 </div>
134 </div>
135 );
136
137 case "star":
138 const filledStars = statValue % 5 || 5;
139 return (
140 <div className="flex items-center gap-1">
141 <div className="flex gap-1">
142 {Array.from({ length: 5 }).map((_, i) => (
143 <Star
144 key={i}
145 className={cn(
146 "h-8 w-8",
147 i < filledStars
148 ? "fill-current text-primary"
149 : "text-gray-300",
150 )}
151 style={{
152 color:
153 i < filledStars
154 ? parentElement?.color || "var(--presentation-primary)"
155 : "var(--presentation-primary)",
156 }}
157 />
158 ))}
159 </div>
160 <span
161 className="text-2xl font-bold"
162 style={{
163 color: parentElement?.color || "var(--presentation-primary)",
164 }}
165 >
166 {stat}
167 </span>
168 </div>
169 );
170
171 case "bar":
172 return (
173 <div className="flex w-full items-center gap-1">
174 <div className="h-8 w-full flex-1 border border-(--presentation-primary) bg-(--presentation-background)">
175 <div
176 className="h-8 transition-all duration-300"
177 style={{
178 width: `${percentage}%`,
179 backgroundColor:
180 parentElement?.color || "var(--presentation-secondary)",
181 }}
182 />
183 </div>
184 <span
185 className="w-20 text-center text-2xl font-bold"
186 style={{
187 color: parentElement?.color || "var(--presentation-primary)",
188 }}
189 >
190 {stat}
191 </span>
192 </div>
193 );
194
195 case "dot-grid":
196 const filledDots = Math.round((percentage / 100) * 100);
197 return (
198 <div className="flex flex-col gap-2">
199 <div className="grid size-32 grid-cols-10 gap-1">
200 {Array.from({ length: 100 }).map((_, i) => (
201 <div
202 key={i}
203 className="size-2 rounded-full"
204 style={{
205 backgroundColor:
206 i < filledDots
207 ? "var(--presentation-secondary)"
208 : parentElement?.color || "var(--presentation-primary)",
209 }}
210 />
211 ))}
212 </div>
213 <span
214 className="w-full text-2xl font-bold"
215 style={{
216 color: parentElement?.color || "var(--presentation-primary)",
217 }}
218 >
219 {stat}
220 </span>
221 </div>
222 );
223
224 case "dot-line":
225 const filledLineDots = Math.round((percentage / 100) * 20);
226 return (
227 <div className="flex items-center gap-2">
228 <div className="flex gap-1">
229 {Array.from({ length: 5 }).map((_, i) => (
230 <div
231 key={i}
232 className="size-4 rounded-full"
233 style={{
234 backgroundColor:
235 i < filledLineDots
236 ? "var(--presentation-secondary)"
237 : parentElement?.color || "var(--presentation-primary)",
238 }}
239 />
240 ))}
241 </div>
242 <span
243 className="w-full text-2xl font-bold"
244 style={{
245 color: parentElement?.color || "var(--presentation-primary)",
246 }}
247 >
248 {stat}
249 </span>
250 </div>
251 );
252
253 default:
254 return null;
255 }
256 };
257
258 return (
259 <div className={cn("grid min-w-0 grid-flow-row grid-cols-1 gap-4 p-6")}>
260 <div
261 className="flex h-max w-full min-w-0"
262 contentEditable={false}
263 data-decor="true"
264 data-slate-void="true"
265 >
266 {renderStatVisual()}
267 </div>
268
269 <SlateElement {...props} className={cn(getAlignmentClasses(alignment))}>
270 <div>{props.children}</div>
271 </SlateElement>
272 </div>
273 );
274 }
275
275 lines Plain Text