import { cn } from '@ermnvldmr/stl'; import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import { usePress } from 'react-aria'; import { useCodeContext } from './contexts/CodeContext/CodeContext'; import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl'; /** * Props for the Code component. */ export interface CodeProps extends ClassNameProps, TestIdProps { /** The content to display. Can be a string or pre-highlighted HTML elements. */ children: React.ReactNode; /** * Raw string value to copy to clipboard (only used in inline mode). * If omitted in inline mode, the component will attempt to extract text from `children`. */ copyValue?: string; } /** * A component for displaying source code. * * When used inside a `CodeBlock`, it renders a block-level `` element * with support for line numbering. * When used standalone, it renders an inline snippet that is clickable to copy. */ export const Code = memo(function Code({ children, copyValue, className, 'data-testid': testId, }: CodeProps) { const context = useCodeContext(); const [isCopied, setIsCopied] = useState(false); const codeRef = useRef(null); const handleCopy = useCallback(async () => { try { let textToCopy = copyValue; if (!textToCopy && codeRef.current) { textToCopy = codeRef.current.innerText; if (!textToCopy) { textToCopy = codeRef.current.textContent ?? undefined; } } if (textToCopy) { await navigator.clipboard.writeText(textToCopy); setIsCopied(true); } } catch (err) { console.error('Failed to copy code:', err); } }, [copyValue]); const onCopyPress = useCallback(() => { void handleCopy(); }, [handleCopy]); useEffect(() => { if (isCopied) { const timer = setTimeout(() => setIsCopied(false), 2000); return () => clearTimeout(timer); } }, [isCopied]); const { pressProps } = usePress({ onPress: onCopyPress, }); if (context?.isInCodeBlock) { const { showLineNumbers } = context; let content = children; if (typeof children === 'string') { content = children.trimEnd().split('\n').map((line, i) => ( // eslint-disable-next-line react/no-array-index-key {line || '\n'} )); } return ( span]:relative [&>span]:pl-10', '[&>span]:[counter-increment:line]', '[&>span]:before:content-[counter(line)]', '[&>span]:before:absolute [&>span]:before:left-0 [&>span]:before:top-[4px]', '[&>span]:before:inline-block', '[&>span]:before:w-8', '[&>span]:before:text-right', '[&>span]:before:text-[var(--rb-muted-text)]/50', '[&>span]:before:select-none', '[&>span]:before:border-r', '[&>span]:before:border-[var(--rb-outline)]/10', '[&>span]:before:pr-3', '[&>span]:before:font-mono [&>span]:before:text-[10px]', ], className )} data-testid={testId} > {content} ); } return ( {children} ); });