import React, { useState } from 'react'; import { Check, Copy } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button } from '../../ui/button'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; // Elegant custom theme inspired by Xertica.ai design system const elegantTheme = { 'code[class*="language-"]': { color: 'var(--foreground)', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', textAlign: 'left' as const, whiteSpace: 'pre' as const, wordSpacing: 'normal', wordBreak: 'normal', lineHeight: '1.6', tabSize: 2, hyphens: 'none' as const, }, 'pre[class*="language-"]': { color: 'var(--foreground)', fontFamily: 'var(--font-mono)', fontSize: '0.875rem', textAlign: 'left' as const, whiteSpace: 'pre' as const, wordSpacing: 'normal', wordBreak: 'normal', lineHeight: '1.6', tabSize: 2, hyphens: 'none' as const, padding: '1rem', margin: 0, overflow: 'auto', background: 'transparent', }, comment: { color: 'var(--muted-foreground)', fontStyle: 'italic', }, prolog: { color: 'var(--muted-foreground)', }, doctype: { color: 'var(--muted-foreground)', }, cdata: { color: 'var(--muted-foreground)', }, punctuation: { color: 'var(--muted-foreground)', }, property: { color: 'var(--chart-4)', }, tag: { color: 'var(--chart-1)', }, boolean: { color: 'var(--chart-5)', }, number: { color: 'var(--chart-5)', }, constant: { color: 'var(--chart-5)', }, symbol: { color: 'var(--chart-5)', }, deleted: { color: 'var(--chart-5)', }, selector: { color: 'var(--chart-2)', }, 'attr-name': { color: 'var(--chart-4)', }, string: { color: 'var(--chart-2)', }, char: { color: 'var(--chart-2)', }, builtin: { color: 'var(--chart-4)', }, inserted: { color: 'var(--chart-2)', }, operator: { color: 'var(--primary)', }, entity: { color: 'var(--chart-4)', }, url: { color: 'var(--chart-1)', }, '.language-css .token.string': { color: 'var(--chart-2)', }, '.style .token.string': { color: 'var(--chart-2)', }, atrule: { color: 'var(--chart-3)', }, 'attr-value': { color: 'var(--chart-2)', }, keyword: { color: 'var(--chart-3)', }, function: { color: 'var(--chart-1)', }, 'class-name': { color: 'var(--chart-4)', }, regex: { color: 'var(--chart-5)', }, important: { color: 'var(--chart-5)', fontWeight: 'bold', }, variable: { color: 'var(--chart-5)', }, }; /** * Props for the CodeBlock component. */ interface CodeBlockProps { /** The code string to be displayed */ code: string; /** Programming language for syntax highlighting */ language?: 'typescript' | 'tsx' | 'css' | 'bash' | 'jsx'; /** Optional filename to display in the header */ filename?: string; /** Whether to show line numbers */ showLineNumbers?: boolean; } /** * Enhanced Syntax Highlighter component with "Copy to Clipboard" functionality. * * @description * Uses `react-syntax-highlighter` with a custom "Elegant" theme inspired by Xertica's * design tokens. It includes a fallback mechanism for the Clipboard API and * optionally displays a header with the filename and language. * * @ai-rules * 1. Theme: Uses a custom `elegantTheme` that maps to system CSS variables. Avoid overriding these colors manually. * 2. Clipboard: The copy functionality is robust with multiple fallback methods. * 3. Content: Ensure `code` is passed as a raw string without pre-formatting indentation if possible. */ export const CodeBlock = ({ code, language = 'tsx', filename, showLineNumbers = false, }: CodeBlockProps) => { const [copied, setCopied] = useState(false); const { t } = useTranslation(); const handleCopy = async () => { try { // Try modern Clipboard API first (with permission check) if (navigator.clipboard && navigator.clipboard.writeText) { try { await navigator.clipboard.writeText(code); } catch (clipboardError: any) { // If clipboard API fails due to permissions, fall back to execCommand if ( clipboardError.name === 'NotAllowedError' || clipboardError.message.includes('permissions policy') ) { throw new Error('Clipboard permission denied, falling back'); } throw clipboardError; } } else { throw new Error('Clipboard API not available'); } setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { // Fallback for browsers that don't support Clipboard API or when permissions are denied try { const textArea = document.createElement('textarea'); textArea.value = code; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); const successful = document.execCommand('copy'); document.body.removeChild(textArea); if (successful) { setCopied(true); setTimeout(() => setCopied(false), 2000); } else { console.error('Failed to copy text: execCommand returned false'); } } catch (fallbackErr) { console.error('All copy methods failed:', fallbackErr); } } }; // Ensure code is a string const codeString = typeof code === 'string' ? code : String(code || ''); return (
{filename && (
{filename} {language}
)}
{codeString}
); };