Snippet
Snippet 从 HeroUI v2 到 v3 的迁移指南。
Snippet 组件在 HeroUI v3 中已移除。请使用原生 HTML 元素配合 Tailwind CSS,并通过 Clipboard API 自行实现复制能力。
主要变化
1. 组件移除
v2: 来自 @heroui/react 的 <Snippet> 组件
v3: 原生 HTML 元素(<pre>、<code>)+ 手动实现复制逻辑
2. 功能对照
v2 的 Snippet 具备以下能力,需要在 v3 中分别替代:
| v2 功能 | v3 替代 | 说明 |
|---|---|---|
| 复制按钮 | Button + Clipboard API | 使用 navigator.clipboard.writeText() |
| 复制提示 | Tooltip 组件 | 使用 v3 的 Tooltip |
| 符号前缀 | 手动渲染 | 将符号作为文本内容输出 |
| 多行支持 | 数组映射 | 对字符串数组做 map |
变体(flat、solid、bordered、shadow) | Tailwind 类 | 使用背景 / 边框等工具类 |
颜色(default、primary 等) | Tailwind 类 | 使用颜色相关工具类 |
尺寸(sm、md、lg) | Tailwind 字号 | 使用 text-sm、text-base、text-lg |
| 圆角 | Tailwind 圆角 | 使用 rounded-* 类 |
结构变化
在 v2 中,Snippet 是带内置复制能力的包装组件:
import { Snippet } from "@heroui/react";
export default function App() {
return (
<Snippet symbol="$">
npm install @heroui/react
</Snippet>
);
}在 v3 中,请使用原生 HTML 元素并手动实现复制:
import { Button, Tooltip } from "@heroui/react";
import { useState } from "react";
export default function App() {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText("npm install @heroui/react");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="flex items-center gap-2 rounded-lg bg-default-100 px-3 py-1.5">
<pre className="text-sm font-mono">
<span className="text-default-500">$ </span>
npm install @heroui/react
</pre>
<Tooltip>
<Button
isIconOnly
aria-label="Copy"
size="sm"
variant="ghost"
onPress={handleCopy}
>
{copied ? "✓" : "📋"}
</Button>
<Tooltip.Content>{copied ? "Copied!" : "Copy to clipboard"}</Tooltip.Content>
</Tooltip>
</div>
);
}迁移示例
多行 Snippet
<Snippet symbol="$">
{[
"npm install @heroui/react",
"yarn add @heroui/react",
"pnpm add @heroui/react"
]}
</Snippet>import { Button, Tooltip } from "@heroui/react";
import { useState } from "react";
function MultiLineSnippet() {
const [copied, setCopied] = useState(false);
const lines = [
"npm install @heroui/react",
"yarn add @heroui/react",
"pnpm add @heroui/react"
];
const codeString = lines.join("\n");
const handleCopy = async () => {
await navigator.clipboard.writeText(codeString);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="flex items-start gap-2 rounded-lg bg-default-100 p-3">
<div className="flex-1 space-y-1">
{lines.map((line, index) => (
<pre key={index} className="text-sm font-mono">
<span className="text-default-500">$ </span>
{line}
</pre>
))}
</div>
<Tooltip>
<Button
isIconOnly
aria-label="Copy"
size="sm"
variant="ghost"
onPress={handleCopy}
>
{copied ? "✓" : "📋"}
</Button>
<Tooltip.Content>{copied ? "Copied!" : "Copy to clipboard"}</Tooltip.Content>
</Tooltip>
</div>
);
}样式选项
{/* With variants */}
<Snippet variant="bordered" color="primary">
npm install @heroui/react
</Snippet>
{/* Without symbol */}
<Snippet hideSymbol>
npm install @heroui/react
</Snippet>
{/* Without copy button */}
<Snippet hideCopyButton>
npm install @heroui/react
</Snippet>{/* With variants */}
<div className="flex items-center gap-2 rounded-lg border border-accent bg-transparent px-3 py-1.5">
<pre className="text-sm font-mono text-accent">
<span className="text-accent/60">$ </span>
npm install @heroui/react
</pre>
{/* Copy button */}
</div>
{/* Without symbol */}
<div className="flex items-center gap-2 rounded-lg bg-default-100 px-3 py-1.5">
<pre className="text-sm font-mono">
npm install @heroui/react
</pre>
{/* Copy button */}
</div>
{/* Without copy button */}
<div className="rounded-lg bg-default-100 px-3 py-1.5">
<pre className="text-sm font-mono">
<span className="text-default-500">$ </span>
npm install @heroui/react
</pre>
</div>创建可复用的 Snippet 组件(推荐)
Snippet 类需求很常见,下面是一个完整的可复用组件示例:
import { Snippet } from "@heroui/react";
<Snippet
symbol="$"
variant="bordered"
color="primary"
size="md"
>
npm install @heroui/react
</Snippet>import { Button, Tooltip } from "@heroui/react";
import { useState, ReactNode } from "react";
import { cn } from "@/lib/utils"; // or your cn utility
interface SnippetProps {
children: string | string[];
symbol?: string | ReactNode;
variant?: "flat" | "solid" | "bordered" | "shadow";
color?: "default" | "primary" | "secondary" | "success" | "warning" | "danger";
size?: "sm" | "md" | "lg";
radius?: "none" | "sm" | "md" | "lg" | "full";
hideSymbol?: boolean;
hideCopyButton?: boolean;
disableCopy?: boolean;
disableTooltip?: boolean;
className?: string;
codeString?: string;
onCopy?: (value: string) => void;
}
const variantClasses = {
flat: "bg-default-100",
solid: "bg-default-200",
bordered: "border border-default-200 bg-transparent",
shadow: "bg-default-100 shadow-sm",
};
const colorClasses = {
default: "text-default-foreground",
primary: "text-accent",
secondary: "text-default-600",
success: "text-success",
warning: "text-warning",
danger: "text-danger",
};
const sizeClasses = {
sm: "px-1.5 py-0.5 text-xs",
md: "px-3 py-1.5 text-sm",
lg: "px-4 py-2 text-base",
};
const radiusClasses = {
none: "rounded-none",
sm: "rounded-sm",
md: "rounded-md",
lg: "rounded-lg",
full: "rounded-full",
};
export function Snippet({
children,
symbol = "$",
variant = "flat",
color = "default",
size = "md",
radius = "md",
hideSymbol = false,
hideCopyButton = false,
disableCopy = false,
disableTooltip = false,
className,
codeString,
onCopy,
}: SnippetProps) {
const [copied, setCopied] = useState(false);
const isMultiLine = Array.isArray(children);
const lines = isMultiLine ? children : [children];
const textToCopy = codeString || (isMultiLine ? lines.join("\n") : String(children));
const handleCopy = async () => {
if (disableCopy) return;
try {
await navigator.clipboard.writeText(textToCopy);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
onCopy?.(textToCopy);
} catch (error) {
console.error("Failed to copy:", error);
}
};
const symbolElement = hideSymbol ? null : (
<span className={cn("text-default-500", colorClasses[color], "opacity-60")}>
{symbol}{typeof symbol === "string" ? " " : ""}
</span>
);
const copyButton = hideCopyButton ? null : (
<Tooltip isDisabled={disableTooltip || disableCopy}>
<Button
isIconOnly
aria-label="Copy"
size="sm"
variant="ghost"
onPress={handleCopy}
isDisabled={disableCopy}
className="shrink-0"
>
{copied ? (
<span className="text-success">✓</span>
) : (
<span>📋</span>
)}
</Button>
<Tooltip.Content>{copied ? "Copied!" : "Copy to clipboard"}</Tooltip.Content>
</Tooltip>
);
return (
<div
className={cn(
"flex items-start gap-2 font-mono",
variantClasses[variant],
sizeClasses[size],
radiusClasses[radius],
className
)}
>
<div className="flex-1 min-w-0">
{isMultiLine ? (
<div className="space-y-1">
{lines.map((line, index) => (
<pre key={index} className={cn("m-0", colorClasses[color])}>
{symbolElement}
{line}
</pre>
))}
</div>
) : (
<pre className={cn("m-0", colorClasses[color])}>
{symbolElement}
{children}
</pre>
)}
</div>
{copyButton}
</div>
);
}
// Usage
<Snippet
symbol="$"
variant="bordered"
color="primary"
size="md"
>
npm install @heroui/react
</Snippet>总结
- 组件已移除:v3 不再提供
Snippet组件。 - 导入调整:移除
import { Snippet } from "@heroui/react"。 - 使用原生元素:改用原生
<pre>、<code>等元素。 - 手动复制:使用 Clipboard API 实现复制。
- 样式:直接用 Tailwind CSS 类表达变体、颜色、尺寸。
- Tooltip:复制按钮的提示请使用 v3 Tooltip。
- Button:复制按钮请使用 v3 Button。
迁移步骤
- 移除导入:从
@heroui/react的导入中删除Snippet。 - 替换组件:将所有
<Snippet>替换为原生 HTML 结构。 - 实现复制:使用
navigator.clipboard.writeText()等方法。 - 添加复制按钮:使用 v3 的 Button 与 Tooltip。
- 应用样式:用 Tailwind CSS 表达变体、颜色、尺寸。
- 处理多行:多行场景对数组做映射渲染。
- (可选) 在应用中封装可复用的 Snippet 组件。
Clipboard API 说明
Clipboard API 需要:
- HTTPS(本地开发可使用 localhost)
- 用户手势触发(不能自动调用)
- 现代浏览器支持
如需兼容旧浏览器,可采用回退方案:
const handleCopy = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
} catch (error) {
// 旧版浏览器回退
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
}
};