返回 JoyAI-Echo
CodeBlock.tsx
1 import { useCallback, useEffect, useState } from "react";
2 import { Check, Copy } from "lucide-react";
3 import { useTranslation } from "react-i18next";
4 import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
5 import {
6 oneDark,
7 oneLight,
8 } from "react-syntax-highlighter/dist/esm/styles/prism";
9
10 import { cn } from "@/lib/utils";
11
12 interface CodeBlockProps {
13 language?: string;
14 code: string;
15 className?: string;
16 }
17
18 /** Read dark mode straight from the DOM — stays in sync with Tailwind's `dark:`. */
19 function useIsDark() {
20 const [isDark, setIsDark] = useState(() =>
21 typeof document !== "undefined"
22 ? document.documentElement.classList.contains("dark")
23 : true,
24 );
25
26 useEffect(() => {
27 const el = document.documentElement;
28 const observer = new MutationObserver(() => {
29 setIsDark(el.classList.contains("dark"));
30 });
31 observer.observe(el, { attributeFilter: ["class"] });
32 return () => observer.disconnect();
33 }, []);
34
35 return isDark;
36 }
37
38 export function CodeBlock({ language, code, className }: CodeBlockProps) {
39 const { t } = useTranslation();
40 const [copied, setCopied] = useState(false);
41 const isDark = useIsDark();
42
43 const onCopy = useCallback(() => {
44 if (!navigator.clipboard) return;
45 navigator.clipboard.writeText(code).then(() => {
46 setCopied(true);
47 setTimeout(() => setCopied(false), 1_500);
48 });
49 }, [code]);
50
51 return (
52 <div
53 className={cn(
54 "overflow-hidden rounded-lg border",
55 isDark ? "border-white/10" : "border-black/10",
56 className,
57 )}
58 >
59 <div
60 className={cn(
61 "flex items-center justify-between px-4 py-1.5 text-xs font-medium",
62 isDark
63 ? "bg-zinc-800 text-zinc-300"
64 : "bg-zinc-100 text-zinc-600",
65 )}
66 >
67 <span className="lowercase font-mono">
68 {language || t("code.fallbackLanguage")}
69 </span>
70 <button
71 type="button"
72 onClick={onCopy}
73 className={cn(
74 "inline-flex items-center gap-1 rounded px-1.5 py-0.5 font-mono transition-colors",
75 isDark
76 ? "text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200"
77 : "text-zinc-500 hover:bg-zinc-200 hover:text-zinc-700",
78 )}
79 aria-label={t("code.copyAria")}
80 >
81 {copied ? (
82 <Check className="h-3.5 w-3.5" />
83 ) : (
84 <Copy className="h-3.5 w-3.5" />
85 )}
86 <span>{copied ? t("code.copied") : t("code.copy")}</span>
87 </button>
88 </div>
89 <SyntaxHighlighter
90 language={language}
91 style={isDark ? oneDark : oneLight}
92 customStyle={{
93 margin: 0,
94 padding: "1rem",
95 fontSize: "0.875rem",
96 lineHeight: 1.6,
97 }}
98 PreTag="pre"
99 wrapLongLines
100 >
101 {code}
102 </SyntaxHighlighter>
103 </div>
104 );
105 }
106
106 lines Plain Text