- Add useDarkMode hook that watches for .dark class on html element - Remove hardcoded #24292e color override that broke dark mode syntax highlighting - CodeBlock now auto-detects theme and uses vscDarkPlus style in dark mode - Theme prop is now optional, only override when explicitly needed
135 lines
3.8 KiB
TypeScript
135 lines
3.8 KiB
TypeScript
import { cn } from '@tpmjs/utils/cn';
|
|
import { forwardRef, useEffect, useState } from 'react';
|
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
// eslint-disable-next-line import/no-internal-modules -- react-syntax-highlighter requires deep import for styles
|
|
import { prism, vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
import { Icon } from '../Icon/Icon';
|
|
import type { CodeBlockProps } from './types';
|
|
import {
|
|
codeBlockCodeVariants,
|
|
codeBlockContainerVariants,
|
|
codeBlockCopyButtonVariants,
|
|
} from './variants';
|
|
|
|
/**
|
|
* Hook to detect if dark mode is active by checking for .dark class on html element
|
|
*/
|
|
function useDarkMode(): boolean {
|
|
const [isDark, setIsDark] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Check initial state
|
|
const checkDarkMode = () => {
|
|
setIsDark(document.documentElement.classList.contains('dark'));
|
|
};
|
|
|
|
checkDarkMode();
|
|
|
|
// Watch for class changes on html element
|
|
const observer = new MutationObserver(checkDarkMode);
|
|
observer.observe(document.documentElement, {
|
|
attributes: true,
|
|
attributeFilter: ['class'],
|
|
});
|
|
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
return isDark;
|
|
}
|
|
|
|
/**
|
|
* CodeBlock component
|
|
*
|
|
* Displays formatted code with syntax highlighting and optional copy functionality.
|
|
* Uses Prism syntax highlighter for beautiful code display.
|
|
* Built with React and JSX.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
|
|
*
|
|
* function MyComponent() {
|
|
* return (
|
|
* <CodeBlock
|
|
* code="npm install @tpmjs/registry"
|
|
* language="bash"
|
|
* size="md"
|
|
* showCopy={true}
|
|
* />
|
|
* );
|
|
* }
|
|
* ```
|
|
*/
|
|
export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
({ className, code, language = 'text', size = 'md', showCopy = true, theme, ...props }, ref) => {
|
|
const [copied, setCopied] = useState(false);
|
|
const isDarkMode = useDarkMode();
|
|
|
|
// Use provided theme or auto-detect from system
|
|
const effectiveTheme = theme ?? (isDarkMode ? 'dark' : 'light');
|
|
|
|
const handleCopy = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(code);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
} catch (err) {
|
|
// Silently fail if clipboard API is not available
|
|
console.error('Failed to copy code:', err);
|
|
}
|
|
};
|
|
|
|
// Map language aliases to supported languages
|
|
const languageMap: Record<string, string> = {
|
|
js: 'javascript',
|
|
ts: 'typescript',
|
|
jsx: 'jsx',
|
|
tsx: 'tsx',
|
|
py: 'python',
|
|
rb: 'ruby',
|
|
sh: 'bash',
|
|
yml: 'yaml',
|
|
};
|
|
|
|
const normalizedLanguage = languageMap[language] || language;
|
|
|
|
return (
|
|
<div ref={ref} className={cn(codeBlockContainerVariants(), className)} {...props}>
|
|
<div className={codeBlockCodeVariants({ size })} data-language={normalizedLanguage}>
|
|
<SyntaxHighlighter
|
|
language={normalizedLanguage}
|
|
style={effectiveTheme === 'dark' ? vscDarkPlus : prism}
|
|
customStyle={{
|
|
margin: 0,
|
|
padding: 0,
|
|
background: 'transparent',
|
|
fontSize: 'inherit',
|
|
lineHeight: 'inherit',
|
|
}}
|
|
codeTagProps={{
|
|
style: {
|
|
fontFamily: 'inherit',
|
|
},
|
|
}}
|
|
>
|
|
{code}
|
|
</SyntaxHighlighter>
|
|
</div>
|
|
{showCopy && (
|
|
<button
|
|
type="button"
|
|
className={codeBlockCopyButtonVariants()}
|
|
onClick={handleCopy}
|
|
aria-label={copied ? 'Copied!' : 'Copy code'}
|
|
data-testid="copy-button"
|
|
>
|
|
<Icon icon={copied ? 'check' : 'copy'} size="sm" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
CodeBlock.displayName = 'CodeBlock';
|