返回 CodeWhale
install-code-block.tsx
根目录 / web / components / install-code-block.tsx
1 "use client";
2
3 import { useState } from "react";
4 import { Icon } from "./icon";
5 import { recordUsage } from "./usage-counting";
6
7 interface Props {
8 cmd: string;
9 copyLabel?: string;
10 copiedLabel?: string;
11 copyLocale?: string;
12 }
13
14 /**
15 * A command in a code block with one button that copies it: the copy icon
16 * and the word, so the action reads without guessing at the glyph. The check
17 * mark, "Copied" and its announcement appear only after the clipboard write
18 * succeeds; a failed write never shows a false confirmation.
19 */
20 export function InstallCodeBlock({ cmd, copyLabel = "Copy", copiedLabel = "Copied", copyLocale }: Props) {
21 const [copied, setCopied] = useState(false);
22
23 const copy = async () => {
24 if (typeof navigator === "undefined" || !navigator.clipboard) return;
25 try {
26 await navigator.clipboard.writeText(cmd);
27 setCopied(true);
28 recordUsage("install_copy");
29 setTimeout(() => setCopied(false), 1400);
30 } catch {
31 // Clipboard write failed (permissions, non-secure context, or a focused
32 // write race) — never show a false "Copied" confirmation.
33 }
34 };
35
36 const copiedWord = copiedLabel.replace(/\s*✓\s*$/u, "");
37
38 return (
39 <div className="code">
40 <pre tabIndex={0} className="code-block">{cmd}</pre>
41 <button
42 type="button"
43 lang={copyLocale}
44 onClick={copy}
45 data-copied={copied}
46 className="btn btn-sm code-copy"
47 >
48 <Icon name={copied ? "check" : "copy"} className="icon" />
49 <span className="code-copy-label">{copied ? copiedWord : copyLabel}</span>
50 </button>
51 <span className="sr-only" aria-live="polite" lang={copyLocale}>
52 {/* The check mark is drawn by the icon; announce only the word. */}
53 {copied ? copiedWord : ""}
54 </span>
55 </div>
56 );
57 }
58
58 lines Plain Text