"use client"; import { Check, Copy } from "lucide-react"; import { ButtonHTMLAttributes, type ComponentProps, createContext, forwardRef, type HTMLAttributes, ReactElement, type ReactNode, type RefObject, useCallback, useContext, useMemo, useRef, } from "react"; import { cn } from "@/lib/utils"; import { useCopyButton } from "./use-copy-button"; import { buttonVariants } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { mergeRefs } from "@/lib/utils"; import { ScrollArea, ScrollBar, ScrollViewport } from "./scroll-area"; export interface CodeBlockProps extends ComponentProps<"figure"> { /** * Icon of code block * * When passed as a string, it assumes the value is the HTML of icon */ icon?: ReactNode; /** * Allow to copy code with copy button * * @defaultValue true */ allowCopy?: boolean; /** * Keep original background color generated by Shiki or Rehype Code * * @defaultValue false */ keepBackground?: boolean; viewportProps?: HTMLAttributes; /** * show line numbers */ "data-line-numbers"?: boolean; /** * @defaultValue 1 */ "data-line-numbers-start"?: number; Actions?: (props: { className?: string; children?: ReactNode }) => ReactNode; } const TabsContext = createContext<{ containerRef: RefObject; nested: boolean; } | null>(null); export function Pre(props: ComponentProps<"pre">) { return (
			{props.children}
		
); } export function CodeBlock({ ref, title, allowCopy, keepBackground = false, icon, viewportProps = {}, children, Actions = (props) => (
), ...props }: CodeBlockProps) { const isTab = useContext(TabsContext) !== null; const areaRef = useRef(null); allowCopy ??= !isTab; const bg = cn( "bg-fd-secondary", keepBackground && "bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)", ); const onCopy = useCallback(() => { const pre = areaRef.current?.getElementsByTagName("pre").item(0); if (!pre) return; const clone = pre.cloneNode(true) as HTMLElement; clone.querySelectorAll(".nd-copy-ignore").forEach((node) => { node.remove(); }); void navigator.clipboard.writeText(clone.textContent ?? ""); }, []); return (
{title ? (
{typeof icon === "string" ? (
) : ( icon )}
{title}
{Actions({ children: allowCopy && , })}
) : ( Actions({ className: "absolute top-1 right-1 z-2 text-fd-muted-foreground", children: allowCopy && , }) )}
{children}
); } function CopyButton({ className, onCopy, ...props }: ButtonHTMLAttributes & { onCopy: () => void; }): ReactElement { const [checked, onClick] = useCopyButton(onCopy); return ( ); } export function CodeBlockTabs({ ref, ...props }: ComponentProps) { const containerRef = useRef(null); const nested = useContext(TabsContext) !== null; return ( ({ containerRef, nested, }), [nested], )} > {props.children} ); } export function CodeBlockTabsList(props: ComponentProps) { const { containerRef, nested } = useContext(TabsContext)!; return ( {props.children} ); } export function CodeBlockTabsTrigger({ children, ...props }: ComponentProps) { return (
{children} ); } // TODO: currently Vite RSC plugin has problem with adding `asChild` here, maybe revisit this in future export const CodeBlockTab = TabsContent; export const CodeBlockOld = forwardRef( ( { title, allowCopy = true, keepBackground = false, icon, viewportProps, ...props }, ref, ) => { const areaRef = useRef(null); const onCopy = useCallback(() => { const pre = areaRef.current?.getElementsByTagName("pre").item(0); if (!pre) return; const clone = pre.cloneNode(true) as HTMLElement; clone.querySelectorAll(".nd-copy-ignore").forEach((node) => { node.remove(); }); void navigator.clipboard.writeText(clone.textContent ?? ""); }, []); return (
{title ? (
{icon ? (
{typeof icon !== "string" ? icon : null}
) : null}
{title}
{allowCopy ? ( ) : null}
) : ( allowCopy && ( ) )} {props.children}
); }, ); CodeBlockOld.displayName = "CodeBlockOld";