返回 presentation-ai
quote-static.tsx
1 import { cva } from "class-variance-authority";
2 import { SlateElement, type SlateElementProps } from "platejs/static";
3
4 import { cn } from "@/lib/utils";
5
6 // Quote variant styles (matching editor component)
7 const quoteVariants = cva("relative my-3 w-full max-w-full overflow-hidden", {
8 variants: {
9 variant: {
10 large: "flex flex-col items-center py-4 text-center",
11 "sidequote-icon":
12 "rounded-r-lg border-l-4 border-(--presentation-primary) bg-(--presentation-primary)/5 py-3 pr-4 pl-12",
13 sidequote: "border-l-4 border-(--presentation-primary) py-2 pr-4 pl-4",
14 },
15 },
16 defaultVariants: {
17 variant: "large",
18 },
19 });
20
21 // Large decorative quote mark SVG
22 const QuoteMark = ({
23 className,
24 flip = false,
25 }: {
26 className?: string;
27 flip?: boolean;
28 }) => (
29 <svg
30 viewBox="0 0 24 24"
31 fill="currentColor"
32 className={cn(
33 "size-12 text-(--presentation-primary)/30",
34 flip && "rotate-180",
35 className,
36 )}
37 >
38 <path d="M6.5 10c-.223 0-.437.034-.65.065.069-.232.14-.468.254-.68.114-.308.292-.575.469-.844.148-.291.409-.488.601-.737.201-.242.475-.403.692-.604.213-.21.492-.315.714-.463.232-.133.434-.28.65-.35l.539-.222.474-.197-.485-1.938-.597.144c-.191.048-.424.104-.689.171-.271.05-.56.187-.882.312-.317.143-.686.238-1.028.467-.344.218-.741.4-1.091.692-.339.301-.748.562-1.05.944-.33.358-.656.734-.909 1.162-.293.408-.492.856-.702 1.299-.19.443-.343.896-.468 1.336-.237.882-.343 1.72-.384 2.437-.034.718-.014 1.315.028 1.747.015.204.043.402.063.539l.025.168.026-.006A4.5 4.5 0 1 0 6.5 10zm11 0c-.223 0-.437.034-.65.065.069-.232.14-.468.254-.68.114-.308.292-.575.469-.844.148-.291.409-.488.601-.737.201-.242.475-.403.692-.604.213-.21.492-.315.714-.463.232-.133.434-.28.65-.35l.539-.222.474-.197-.485-1.938-.597.144c-.191.048-.424.104-.689.171-.271.05-.56.187-.882.312-.317.143-.686.238-1.028.467-.344.218-.741.4-1.091.692-.339.301-.748.562-1.05.944-.33.358-.656.734-.909 1.162-.293.408-.492.856-.702 1.299-.19.443-.343.896-.468 1.336-.237.882-.343 1.72-.384 2.437-.034.718-.014 1.315.028 1.747.015.204.043.402.063.539l.025.168.026-.006A4.5 4.5 0 1 0 17.5 10z" />
39 </svg>
40 );
41
42 // Quote icon for sidequote-icon variant
43 const QuoteIcon = ({ className }: { className?: string }) => (
44 <svg
45 viewBox="0 0 24 24"
46 fill="currentColor"
47 className={cn("size-6 text-(--presentation-primary)", className)}
48 >
49 <path d="M6.5 10c-.223 0-.437.034-.65.065.069-.232.14-.468.254-.68.114-.308.292-.575.469-.844.148-.291.409-.488.601-.737.201-.242.475-.403.692-.604.213-.21.492-.315.714-.463.232-.133.434-.28.65-.35l.539-.222.474-.197-.485-1.938-.597.144c-.191.048-.424.104-.689.171-.271.05-.56.187-.882.312-.317.143-.686.238-1.028.467-.344.218-.741.4-1.091.692-.339.301-.748.562-1.05.944-.33.358-.656.734-.909 1.162-.293.408-.492.856-.702 1.299-.19.443-.343.896-.468 1.336-.237.882-.343 1.72-.384 2.437-.034.718-.014 1.315.028 1.747.015.204.043.402.063.539l.025.168.026-.006A4.5 4.5 0 1 0 6.5 10zm11 0c-.223 0-.437.034-.65.065.069-.232.14-.468.254-.68.114-.308.292-.575.469-.844.148-.291.409-.488.601-.737.201-.242.475-.403.692-.604.213-.21.492-.315.714-.463.232-.133.434-.28.65-.35l.539-.222.474-.197-.485-1.938-.597.144c-.191.048-.424.104-.689.171-.271.05-.56.187-.882.312-.317.143-.686.238-1.028.467-.344.218-.741.4-1.091.692-.339.301-.748.562-1.05.944-.33.358-.656.734-.909 1.162-.293.408-.492.856-.702 1.299-.19.443-.343.896-.468 1.336-.237.882-.343 1.72-.384 2.437-.034.718-.014 1.315.028 1.747.015.204.043.402.063.539l.025.168.026-.006A4.5 4.5 0 1 0 17.5 10z" />
50 </svg>
51 );
52
53 export function QuoteStatic(props: SlateElementProps) {
54 const { variant = "large", author } = props.element as {
55 variant?: "large" | "sidequote-icon" | "sidequote";
56 author?: string;
57 };
58
59 if (variant === "large") {
60 return (
61 <SlateElement
62 {...props}
63 className={cn(quoteVariants({ variant }), props.className)}
64 >
65 <div className="relative w-full max-w-3xl px-10">
66 {/* Opening quote mark */}
67 <QuoteMark className="absolute top-0 left-0 size-8" />
68
69 <div className="flex flex-1 flex-col items-center">
70 {/* Quote text */}
71 <blockquote className="font-serif text-xl text-(--presentation-foreground) italic md:text-2xl">
72 {props.children}
73 </blockquote>
74
75 {/* Author */}
76 {author && (
77 <p className="mt-4 text-sm font-medium text-(--presentation-muted-foreground)">
78 — {author}
79 </p>
80 )}
81 </div>
82
83 {/* Closing quote mark */}
84 <QuoteMark className="absolute top-0 right-0 size-8" flip />
85 </div>
86 </SlateElement>
87 );
88 }
89
90 if (variant === "sidequote-icon") {
91 return (
92 <SlateElement
93 {...props}
94 className={cn(quoteVariants({ variant }), props.className)}
95 >
96 {/* Quote icon */}
97 <QuoteIcon className="absolute top-3 left-4" />
98
99 {/* Quote text */}
100 <blockquote className="text-base text-(--presentation-foreground) italic md:text-lg">
101 {props.children}
102 </blockquote>
103
104 {/* Author */}
105 {author && (
106 <p className="mt-3 text-sm font-semibold text-(--presentation-foreground)">
107 {author}
108 </p>
109 )}
110 </SlateElement>
111 );
112 }
113
114 // Default: sidequote (simple border left)
115 return (
116 <SlateElement
117 {...props}
118 className={cn(quoteVariants({ variant: "sidequote" }), props.className)}
119 >
120 {/* Quote text */}
121 <blockquote className="text-base text-(--presentation-foreground)">
122 {props.children}
123 </blockquote>
124
125 {/* Author */}
126 {author && (
127 <p className="mt-2 text-sm text-(--presentation-muted-foreground)">
128 — {author}
129 </p>
130 )}
131 </SlateElement>
132 );
133 }
134
134 lines Plain Text