返回 presentation-ai
ElementPreview.tsx
根目录 / src / components / presentation / edit-panel / sections / ElementPreview.tsx
1 "use client";
2
3 import {
4 CircleAlert,
5 CircleCheck,
6 CircleHelp,
7 CircleUserRound,
8 FileText,
9 Info,
10 List,
11 ListChecks,
12 ListOrdered,
13 Sigma,
14 Star,
15 Tag,
16 ToggleLeft,
17 } from "lucide-react";
18 import { type ReactNode } from "react";
19
20 import * as TemplatePreviews from "@/components/notebook/presentation/utils/template-previews";
21 import { cn } from "@/lib/utils";
22
23 const HEADING_WIDTH_BY_LEVEL = {
24 1: "w-11/12",
25 2: "w-10/12",
26 3: "w-9/12",
27 4: "w-8/12",
28 5: "w-7/12",
29 6: "w-6/12",
30 } satisfies Record<1 | 2 | 3 | 4 | 5 | 6, string>;
31
32 const DEFAULT_STAT_VALUES = [64, 28, 91] as const;
33
34 const STEPS_ARROW_ITEMS = [
35 { id: "first", titleWidth: "w-12", bodyWidth: "w-16" },
36 { id: "second", titleWidth: "w-12", bodyWidth: "w-14" },
37 { id: "third", titleWidth: "w-14", bodyWidth: "w-16" },
38 ] as const;
39
40 const renderVerticalStat = (
41 value: number,
42 visual: ReactNode,
43 className?: string,
44 ) => (
45 <div
46 key={value}
47 className={cn("flex min-w-0 flex-col items-start gap-1.5", className)}
48 >
49 <div className="flex h-8 w-full items-center">{visual}</div>
50 {renderStatValue()}
51 {renderStatLabel()}
52 </div>
53 );
54
55 const renderStatValue = () => (
56 <div className="h-2 w-6 rounded bg-muted-foreground/25" aria-hidden="true" />
57 );
58
59 const renderStatLabel = () => (
60 <div className="h-1.5 w-9 rounded bg-muted" aria-hidden="true" />
61 );
62
63 function HeadingPreview({ level }: { level: 1 | 2 | 3 | 4 | 5 | 6 }) {
64 const heightClass = level <= 2 ? "h-5" : level <= 4 ? "h-4" : "h-3";
65
66 return (
67 <div className="flex size-full items-center p-4">
68 <div className="w-full space-y-2">
69 <div
70 className={cn(
71 heightClass,
72 HEADING_WIDTH_BY_LEVEL[level],
73 "rounded bg-muted-foreground/35",
74 )}
75 />
76 <div className="h-1.5 w-2/3 rounded bg-muted" />
77 </div>
78 </div>
79 );
80 }
81
82 function ParagraphPreview() {
83 return (
84 <div className="flex size-full flex-col justify-center gap-1.5 p-4">
85 <div className="h-1.5 w-full rounded bg-muted-foreground/20" />
86 <div className="h-1.5 w-11/12 rounded bg-muted-foreground/20" />
87 <div className="h-1.5 w-10/12 rounded bg-muted-foreground/20" />
88 <div className="h-1.5 w-7/12 rounded bg-muted-foreground/20" />
89 </div>
90 );
91 }
92
93 function StaircaseShapePreview() {
94 return (
95 <div className="flex size-full flex-col justify-center gap-1.5 p-4">
96 <div className="h-4 w-1/3 rounded bg-muted-foreground/25" />
97 <div className="h-4 w-2/3 rounded bg-muted-foreground/25" />
98 <div className="h-4 w-full rounded bg-muted-foreground/25" />
99 </div>
100 );
101 }
102
103 function FeatureBoxesPreview() {
104 return (
105 <div className="flex size-full items-center justify-center p-3">
106 <div className="grid w-full grid-cols-3 gap-1.5">
107 {[1, 2, 3].map((i) => (
108 <div key={i} className="space-y-1 rounded border border-border p-1.5">
109 <div className="h-1.5 w-3/4 rounded bg-muted-foreground/20" />
110 <div className="h-1 w-full rounded bg-muted" />
111 <div className="h-1 w-5/6 rounded bg-muted" />
112 </div>
113 ))}
114 </div>
115 </div>
116 );
117 }
118
119 function ComparePointsPreview() {
120 return (
121 <div className="flex size-full items-center gap-1 p-3">
122 <div className="flex flex-1 flex-col gap-1">
123 <div className="mb-0.5 h-2 w-3/4 rounded bg-muted-foreground/20" />
124 {[1, 2, 3].map((i) => (
125 <div key={i} className="flex items-center gap-1">
126 <div className="size-1 shrink-0 rounded-full bg-muted-foreground/30" />
127 <div className="h-1 flex-1 rounded bg-muted" />
128 </div>
129 ))}
130 </div>
131 {/* VS badge */}
132 <div className="flex shrink-0 items-center justify-center">
133 <div className="flex size-5 items-center justify-center rounded-full border border-muted-foreground/25 bg-muted/40">
134 <span className="text-[8px] font-bold text-muted-foreground/60">
135 vs
136 </span>
137 </div>
138 </div>
139 <div className="flex flex-1 flex-col gap-1">
140 <div className="mb-0.5 h-2 w-3/4 rounded bg-muted-foreground/20" />
141 {[1, 2, 3].map((i) => (
142 <div key={i} className="flex items-center gap-1">
143 <div className="size-1 shrink-0 rounded-full bg-muted-foreground/30" />
144 <div className="h-1 flex-1 rounded bg-muted" />
145 </div>
146 ))}
147 </div>
148 </div>
149 );
150 }
151
152 function BeforeAfterPreview() {
153 return (
154 <div className="flex size-full items-center gap-1 p-3">
155 <div className="flex flex-1 flex-col gap-1">
156 <div className="mb-0.5 h-2 w-3/4 rounded bg-muted-foreground/20" />
157 {[1, 2, 3].map((i) => (
158 <div key={i} className="flex items-center gap-1">
159 <div className="size-1 shrink-0 rounded-full bg-muted-foreground/30" />
160 <div className="h-1 flex-1 rounded bg-muted" />
161 </div>
162 ))}
163 </div>
164 {/* Arrow right icon */}
165 <div className="flex shrink-0 items-center justify-center">
166 <div className="flex size-5 items-center justify-center rounded-full border border-muted-foreground/25 bg-muted/40">
167 <svg
168 viewBox="0 0 24 24"
169 fill="none"
170 stroke="currentColor"
171 strokeWidth={2}
172 strokeLinecap="round"
173 strokeLinejoin="round"
174 className="size-3 text-muted-foreground/50"
175 >
176 <path d="M5 12h14M12 5l7 7-7 7" />
177 </svg>
178 </div>
179 </div>
180 <div className="flex flex-1 flex-col gap-1">
181 <div className="mb-0.5 h-2 w-3/4 rounded bg-muted-foreground/20" />
182 {[1, 2, 3].map((i) => (
183 <div key={i} className="flex items-center gap-1">
184 <div className="size-1 shrink-0 rounded-full bg-muted-foreground/30" />
185 <div className="h-1 flex-1 rounded bg-muted" />
186 </div>
187 ))}
188 </div>
189 </div>
190 );
191 }
192
193 function ProsConsPreview() {
194 return (
195 <div className="flex size-full gap-0 overflow-hidden p-2">
196 <div className="flex flex-1 flex-col gap-1.5 rounded-l-sm bg-green-500/15 p-2">
197 <div className="h-1.5 w-3/4 rounded bg-green-500/50" />
198 {[1, 2, 3].map((i) => (
199 <div key={i} className="h-1 w-full rounded bg-green-500/25" />
200 ))}
201 </div>
202 <div className="flex flex-1 flex-col gap-1.5 rounded-r-sm bg-red-500/15 p-2">
203 <div className="h-1.5 w-3/4 rounded bg-red-500/50" />
204 {[1, 2, 3].map((i) => (
205 <div key={i} className="h-1 w-full rounded bg-red-500/25" />
206 ))}
207 </div>
208 </div>
209 );
210 }
211
212 function IconListPreview() {
213 return (
214 <div className="flex size-full flex-col justify-center gap-2 p-3">
215 {[1, 2, 3].map((i) => (
216 <div key={i} className="flex items-center gap-2">
217 <div className="size-3.5 shrink-0 rounded bg-muted-foreground/30" />
218 <div className="flex-1 space-y-0.5">
219 <div className="h-1.5 w-full rounded bg-muted-foreground/20" />
220 <div className="h-1 w-5/6 rounded bg-muted" />
221 </div>
222 </div>
223 ))}
224 </div>
225 );
226 }
227
228 function ImagePlaceholderPreview() {
229 return (
230 <div className="flex size-full items-center justify-center p-4">
231 <div className="flex size-full items-center justify-center rounded border-2 border-dashed border-muted-foreground/20 bg-muted/30">
232 <svg
233 className="size-6 text-muted-foreground/30"
234 fill="none"
235 viewBox="0 0 24 24"
236 stroke="currentColor"
237 >
238 <path
239 strokeLinecap="round"
240 strokeLinejoin="round"
241 strokeWidth={1.5}
242 d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
243 />
244 </svg>
245 </div>
246 </div>
247 );
248 }
249
250 function CalloutPreview() {
251 return (
252 <div className="flex size-full items-center p-3">
253 <div className="flex w-full items-start gap-2 rounded bg-amber-500/10 p-2">
254 <Info className="size-4 shrink-0 text-blue-500/70" />
255 <div className="flex-1 space-y-1">
256 <div className="h-1.5 w-full rounded bg-muted-foreground/20" />
257 <div className="h-1 w-5/6 rounded bg-muted" />
258 <div className="h-1 w-4/5 rounded bg-muted" />
259 </div>
260 </div>
261 </div>
262 );
263 }
264
265 function TocPreview() {
266 return (
267 <div className="flex size-full flex-col justify-center gap-2 p-3">
268 {[80, 60, 70].map((w, i) => (
269 <div key={i} className="flex items-center gap-1.5">
270 <div className="size-1.5 shrink-0 rounded-full bg-muted-foreground/40" />
271 <div
272 className="h-1.5 rounded bg-muted-foreground/20"
273 style={{ width: `${w}%` }}
274 />
275 <div className="flex-1 border-b border-dashed border-muted-foreground/10" />
276 <div className="h-1.5 w-3 rounded bg-muted-foreground/20" />
277 </div>
278 ))}
279 </div>
280 );
281 }
282
283 function CodeBlockPreview() {
284 return (
285 <div className="flex size-full items-center p-3">
286 <div className="w-full overflow-hidden rounded bg-muted/60 px-2.5 py-2">
287 <div className="space-y-1.5">
288 <div className="flex items-center gap-1.5">
289 <div className="h-1 w-7 rounded bg-blue-400/50" />
290 <div className="h-1 w-10 rounded bg-muted-foreground/25" />
291 <div className="h-1 w-5 rounded bg-green-400/50" />
292 </div>
293 <div className="ml-3 flex items-center gap-1.5">
294 <div className="h-1 w-8 rounded bg-purple-400/50" />
295 <div className="h-1 w-6 rounded bg-orange-400/40" />
296 </div>
297 <div className="ml-3 flex items-center gap-1.5">
298 <div className="h-1 w-12 rounded bg-muted-foreground/20" />
299 </div>
300 <div className="flex items-center gap-1.5">
301 <div className="h-1 w-3 rounded bg-muted-foreground/25" />
302 </div>
303 </div>
304 </div>
305 </div>
306 );
307 }
308
309 function DividerPreview() {
310 return (
311 <div className="flex size-full items-center justify-center p-4">
312 <div className="h-px w-full bg-muted-foreground/25" />
313 </div>
314 );
315 }
316
317 function ButtonPreview() {
318 return (
319 <div className="flex size-full items-center justify-center p-4">
320 <div className="rounded bg-primary px-4 py-2">
321 <div className="h-1.5 w-16 rounded bg-primary-foreground/60" />
322 </div>
323 </div>
324 );
325 }
326
327 function IconOnlyPreview({ icon }: { icon: ReactNode }) {
328 return (
329 <div className="flex size-full items-center justify-center p-4 text-muted-foreground/70 [&_svg]:size-8">
330 {icon}
331 </div>
332 );
333 }
334
335 function StatsPreview({
336 variant,
337 }: {
338 variant?: "bar" | "circle" | "dot-grid" | "dot-line" | "plain" | "star";
339 }) {
340 if (variant === "circle") {
341 return (
342 <div className="grid size-full grid-cols-3 items-center gap-3 p-4">
343 {[70, 50, 85].map((value) =>
344 renderVerticalStat(
345 value,
346 <div className="flex size-8 items-center justify-center rounded-full border-4 border-muted-foreground/25 text-[7px] font-semibold text-muted-foreground/55">
347 {value}
348 </div>,
349 ),
350 )}
351 </div>
352 );
353 }
354
355 if (variant === "bar") {
356 return (
357 <div className="grid size-full grid-cols-3 items-center gap-3 p-4">
358 {[74, 52, 89].map((value) =>
359 renderVerticalStat(
360 value,
361 <div className="h-4 w-full rounded-sm border border-muted-foreground/25 bg-card">
362 <div
363 className="h-full bg-muted-foreground/35"
364 style={{ width: `${value}%` }}
365 />
366 </div>,
367 ),
368 )}
369 </div>
370 );
371 }
372
373 if (variant === "dot-grid") {
374 return (
375 <div className="grid size-full grid-cols-3 items-center gap-3 p-4">
376 {[68, 41, 96].map((value) =>
377 renderVerticalStat(
378 value,
379 <div className="grid size-8 grid-cols-5 gap-0.5">
380 {Array.from(
381 { length: 25 },
382 (_, index) => `dot-${value}-${index}`,
383 ).map((dotId, index) => (
384 <div
385 key={dotId}
386 className={cn(
387 "size-1 rounded-full",
388 index < Math.round((value / 100) * 25)
389 ? "bg-muted-foreground/35"
390 : "bg-muted",
391 )}
392 />
393 ))}
394 </div>,
395 ),
396 )}
397 </div>
398 );
399 }
400
401 if (variant === "dot-line") {
402 return (
403 <div className="grid size-full grid-cols-3 items-center gap-3 p-4">
404 {[60, 75, 90].map((value) =>
405 renderVerticalStat(
406 value,
407 <div className="flex gap-1.5">
408 <div className="size-2.5 rounded-full bg-muted-foreground/35" />
409 <div className="size-2.5 rounded-full bg-muted-foreground/35" />
410 </div>,
411 ),
412 )}
413 </div>
414 );
415 }
416
417 if (variant === "star") {
418 return (
419 <div className="grid size-full grid-cols-3 items-center gap-3 p-4">
420 {[4, 5, 4].map((value) =>
421 renderVerticalStat(
422 value,
423 <Star className="size-4 fill-muted-foreground/35 text-muted-foreground/35" />,
424 ),
425 )}
426 </div>
427 );
428 }
429
430 return (
431 <div className="grid size-full grid-cols-3 items-center gap-3 p-4">
432 {DEFAULT_STAT_VALUES.map((value) =>
433 renderVerticalStat(
434 value,
435 <div className="h-5 w-8 rounded bg-muted-foreground/35" />,
436 ),
437 )}
438 </div>
439 );
440 }
441
442 function MediaEmbedPreview({ infographic = false }: { infographic?: boolean }) {
443 return (
444 <div className="flex size-full items-center justify-center p-4">
445 <div className="flex size-full items-center justify-center rounded border-2 border-dashed border-muted-foreground/20 bg-muted/30">
446 <div className="flex flex-col items-center gap-2">
447 <div className="h-6 w-8 rounded-sm bg-muted-foreground/25" />
448 <div className="h-1.5 w-16 rounded bg-muted-foreground/20" />
449 {infographic && <div className="h-1 w-10 rounded bg-muted" />}
450 </div>
451 </div>
452 </div>
453 );
454 }
455
456 function LargeQuotePreview() {
457 return (
458 <div className="flex size-full items-center justify-center p-3">
459 <div className="flex w-full items-center justify-center gap-2">
460 <div className="text-2xl font-serif text-amber-300/70">"</div>
461 <div className="flex flex-col items-center gap-1.5">
462 <div className="h-2 w-28 rounded bg-muted-foreground/25" />
463 <div className="h-2 w-24 rounded bg-muted-foreground/20" />
464 <div className="mt-1 h-1.5 w-14 rounded bg-muted-foreground/25" />
465 </div>
466 <div className="text-2xl font-serif text-amber-300/70">"</div>
467 </div>
468 </div>
469 );
470 }
471
472 function QuoteWithIconPreview() {
473 return (
474 <div className="flex size-full items-center p-3">
475 <div className="flex w-full gap-2 rounded bg-amber-500/10 p-2">
476 <div className="w-1 shrink-0 rounded-full bg-amber-500/70" />
477 <div className="flex flex-1 flex-col justify-center gap-1.5">
478 <div className="size-3 rounded bg-amber-500/70" />
479 <div className="h-1.5 w-full rounded bg-muted-foreground/25" />
480 <div className="h-1.5 w-4/5 rounded bg-muted-foreground/20" />
481 <div className="mt-0.5 h-1.5 w-16 rounded bg-muted-foreground/35" />
482 </div>
483 </div>
484 </div>
485 );
486 }
487
488 function SideQuotePreview() {
489 return (
490 <div className="flex size-full items-center justify-center p-4">
491 <div className="flex w-full items-start gap-2">
492 <div className="mt-0.5 h-10 w-1 shrink-0 rounded-full bg-amber-500/70" />
493 <div className="flex flex-col justify-center gap-1.5">
494 <div className="h-1.5 w-28 rounded bg-muted-foreground/20" />
495 <div className="h-1.5 w-24 rounded bg-muted-foreground/15" />
496 <div className="h-1.5 w-20 rounded bg-muted-foreground/15" />
497 <div className="mt-0.5 h-1 w-12 rounded bg-muted-foreground/30" />
498 </div>
499 </div>
500 </div>
501 );
502 }
503
504 function ColumnsPreview() {
505 return (
506 <div className="flex size-full items-center justify-center p-3">
507 <div className="grid w-full grid-cols-3 gap-2">
508 {["left", "middle", "right"].map((column) => (
509 <div key={column} className="space-y-1">
510 <div className="h-2 rounded bg-muted-foreground/20" />
511 <div className="h-1 rounded bg-muted" />
512 <div className="h-1 rounded bg-muted" />
513 <div className="h-1 w-4/5 rounded bg-muted" />
514 </div>
515 ))}
516 </div>
517 </div>
518 );
519 }
520
521 function StepsArrowPreview() {
522 const columns = STEPS_ARROW_ITEMS.length;
523
524 return (
525 <div className="grid size-full grid-cols-3 items-stretch gap-2 p-3">
526 {STEPS_ARROW_ITEMS.map((step, index) => {
527 const topMargin = Math.max(0, (columns - 1 - index) * 1.35);
528
529 return (
530 <div key={step.id} className="flex min-w-0 flex-col">
531 <div
532 className="flex flex-1 flex-col"
533 style={{ marginTop: `${topMargin}rem` }}
534 >
535 <div
536 className="mb-2 flex w-full shrink-0 items-center pr-2 text-muted-foreground/35"
537 aria-hidden="true"
538 >
539 <div className="h-1.5 flex-1 bg-current" />
540 <svg
541 viewBox="0 0 16 24"
542 className="-ml-px h-3.5 w-2.5 shrink-0 fill-current"
543 >
544 <path d="M0 0l16 12-16 12z" />
545 </svg>
546 </div>
547 <div className="space-y-1.5">
548 <div
549 className={cn(
550 "h-2 rounded bg-muted-foreground/30",
551 step.titleWidth,
552 )}
553 />
554 <div className={cn("h-1.5 rounded bg-muted", step.bodyWidth)} />
555 </div>
556 </div>
557 </div>
558 );
559 })}
560 </div>
561 );
562 }
563
564 function ProcessArrowsPreview() {
565 return (
566 <div className="flex size-full items-center justify-center p-4">
567 <div className="flex flex-col gap-1.5 text-muted-foreground/30">
568 {[1, 2, 3].map((item) => (
569 <div key={item} className="flex items-center gap-2">
570 <svg
571 viewBox="0 0 90 72"
572 className="h-5 w-7 shrink-0 overflow-visible"
573 aria-hidden="true"
574 preserveAspectRatio="none"
575 >
576 <path d="M0 54L45 72L90 54L90 0L45 18L0 0Z" fill="currentColor" />
577 </svg>
578 <div className="flex flex-col gap-1">
579 <div className="h-1.5 w-14 rounded bg-muted-foreground/25" />
580 <div className="h-1 w-10 rounded bg-muted-foreground/20" />
581 </div>
582 </div>
583 ))}
584 </div>
585 </div>
586 );
587 }
588
589 const ELEMENT_PREVIEWS: Record<string, ReactNode> = {
590 title: <HeadingPreview level={1} />,
591 "title-display": <HeadingPreview level={1} />,
592 "title-humongous": <HeadingPreview level={1} />,
593 "heading-1": <HeadingPreview level={1} />,
594 "heading-2": <HeadingPreview level={2} />,
595 "heading-3": <HeadingPreview level={3} />,
596 "heading-4": <HeadingPreview level={4} />,
597 heading: <HeadingPreview level={2} />,
598 paragraph: <ParagraphPreview />,
599 blockquote: <SideQuotePreview />,
600 label: <IconOnlyPreview icon={<Tag />} />,
601 bullets: <TemplatePreviews.LargeBulletsPreview />,
602 "bulleted-list": <IconOnlyPreview icon={<List />} />,
603 "numbered-list": <IconOnlyPreview icon={<ListOrdered />} />,
604 "todo-list": <IconOnlyPreview icon={<ListChecks />} />,
605 timeline: <TemplatePreviews.TimelineSequencePreview />,
606 steps: <StepsArrowPreview />,
607 "steps-default": <StepsArrowPreview />,
608 "steps-arrow": <StepsArrowPreview />,
609 "steps-box": <StepsArrowPreview />,
610 arrows: <ProcessArrowsPreview />,
611 "arrow-vertical": <TemplatePreviews.SequenceArrowPreview />,
612 slope: <TemplatePreviews.SlopeDiagramPreview />,
613 snake: <TemplatePreviews.SnakeDiagramPreview />,
614 pyramid: <TemplatePreviews.PyramidOutsideTextPreview />,
615 cycle: <TemplatePreviews.CyclePreview />,
616 "connected-circles": <TemplatePreviews.ConnectedCirclesDiagramPreview />,
617 "circular-grid": <TemplatePreviews.CircularGridDiagramPreview />,
618 staircase: <StaircaseShapePreview />,
619 boxes: <FeatureBoxesPreview />,
620 compare: <ComparePointsPreview />,
621 "before-after": <BeforeAfterPreview />,
622 "pros-cons": <ProsConsPreview />,
623 "icon-list": <IconListPreview />,
624 button: <ButtonPreview />,
625 toggle: <IconOnlyPreview icon={<ToggleLeft />} />,
626 image: <ImagePlaceholderPreview />,
627 columns: <ColumnsPreview />,
628 callout: <CalloutPreview />,
629 "callout-note": <IconOnlyPreview icon={<FileText />} />,
630 "callout-info": <IconOnlyPreview icon={<Info />} />,
631 "callout-warning": <IconOnlyPreview icon={<CircleAlert />} />,
632 "callout-caution": <IconOnlyPreview icon={<CircleAlert />} />,
633 "callout-success": <IconOnlyPreview icon={<CircleCheck />} />,
634 "callout-question": <IconOnlyPreview icon={<CircleHelp />} />,
635 toc: <TocPreview />,
636 "quote-large": <LargeQuotePreview />,
637 "quote-side-icon": <QuoteWithIconPreview />,
638 "quote-side": <SideQuotePreview />,
639 code: <CodeBlockPreview />,
640 math: <IconOnlyPreview icon={<Sigma />} />,
641 contributors: <IconOnlyPreview icon={<CircleUserRound />} />,
642 hr: <DividerPreview />,
643 table: <TemplatePreviews.ThreeRowTablePreview />,
644 "table-2x2": <TemplatePreviews.ThreeRowTablePreview />,
645 "table-3x3": <TemplatePreviews.ThreeRowTablePreview />,
646 "table-4x4": <TemplatePreviews.ThreeRowTablePreview />,
647 "stats-plain": <StatsPreview />,
648 "stats-circle": <StatsPreview variant="circle" />,
649 "stats-star": <StatsPreview variant="star" />,
650 "stats-bar": <StatsPreview variant="bar" />,
651 "stats-dot-grid": <StatsPreview variant="dot-grid" />,
652 "stats-dot-line": <StatsPreview variant="dot-line" />,
653 "media-embed": <MediaEmbedPreview />,
654 infographic: <MediaEmbedPreview infographic />,
655 };
656
657 interface ElementPreviewProps {
658 elementKey: string;
659 className?: string;
660 }
661
662 export function ElementPreview({ elementKey, className }: ElementPreviewProps) {
663 const preview = ELEMENT_PREVIEWS[elementKey] ?? (
664 <TemplatePreviews.LargeBulletsPreview />
665 );
666
667 return (
668 <div
669 className={cn(
670 "pointer-events-none aspect-4/3 w-full overflow-hidden rounded-sm border bg-card select-none **:pointer-events-none **:select-none",
671 className,
672 )}
673 >
674 {preview}
675 </div>
676 );
677 }
678
678 lines Plain Text