'use client'; import { Check, Clipboard } from 'lucide-react'; import { type ComponentProps, createContext, type HTMLAttributes, type ReactNode, type RefObject, useContext, useMemo, useRef, } from 'react'; import { cn } from '../lib/cn'; import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'; import { buttonVariants } from './ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger, } from './tabs.unstyled'; import { mergeRefs } from '../lib/merge-refs'; 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 = true, keepBackground = false, icon, viewportProps = {}, children, Actions = (props) => (
), ...props }: CodeBlockProps) { const inTab = useContext(TabsContext) !== null; const areaRef = useRef(null); return (
{title ? (
{typeof icon === 'string' ? (
) : ( icon )}
{title}
{Actions({ className: '-me-2', children: allowCopy && , })}
) : ( Actions({ className: 'absolute top-2 right-2 z-2 backdrop-blur-lg rounded-lg text-fd-muted-foreground', children: allowCopy && , }) )}
{children}
); } function CopyButton({ className, containerRef, ...props }: ComponentProps<'button'> & { containerRef: RefObject; }) { const [checked, onClick] = useCopyButton(() => { const pre = containerRef.current?.getElementsByTagName('pre').item(0); if (!pre) return; const clone = pre.cloneNode(true) as HTMLElement; clone.querySelectorAll('.nd-copy-ignore').forEach((node) => { node.replaceWith('\n'); }); void navigator.clipboard.writeText(clone.textContent ?? ''); }); 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) { return ( {props.children} ); } export function CodeBlockTabsTrigger({ children, ...props }: ComponentProps) { return (
{children} ); } // TODO: currently Vite RSC plugin has problem with `asChild` due to children is automatically wrapped in , maybe revisit this in future export function CodeBlockTab(props: ComponentProps) { return ; }