'use client'; import { Highlight, Language, themes } from 'prism-react-renderer'; import React, { useMemo, useRef } from 'react'; import { useAppT } from '@djangocfg/i18n'; import { useResolvedTheme } from '@djangocfg/ui-core/hooks'; import { CodePanelHeader } from '../../../../common/CodePanelHeader'; // Surface palette — driven by the ui-core `--code*` semantic tokens so the // code panel matches the rest of the theme system (neutral, preset-aware) // instead of a hard-coded GitHub blue. The tokens are already tuned per // theme (`light.css` / `dark.css`): a near-white gray in light, a near-black // gray in dark — so the surface stays a calm neutral in either mode and never // reads blue. Syntax colors still come from Prism's vsDark/github palettes. const CODE_SURFACE_BG = 'var(--code)'; const CODE_SURFACE_BORDER = 'var(--code-border)'; const CODE_INLINE_BG = 'var(--code-inline)'; // Load extra Prism grammars (``bash``, ``ruby``, ``java``, ``php``) // that aren't in ``prism-react-renderer``'s default bundle. The hook // below subscribes to its ready state and re-renders this component // once loading completes, so the first tab click gets highlighted // correctly even if it happened before the dynamic import resolved. import { useEnsurePrismLanguages } from './registerPrismLanguages'; interface PrettyCodeProps { data: string | object; language: Language; className?: string; /** * Force a specific Prism palette + surface. **Defaults to the resolved * host theme** (light → light surface + GitHub palette, dark → dark * surface + vsDark palette), so the panel matches the rest of the UI and * its `--code*` tokens. Pass `mode` explicitly only to pin a palette * regardless of theme — e.g. printing a PDF on a light page. */ mode?: 'dark' | 'light'; inline?: boolean; customBg?: string; // Custom background class isCompact?: boolean; // Compact mode for smaller font sizes /** @deprecated No-op since the toolbar moved into a persistent * `CodePanelHeader` (no scroll-capturing overlay). Kept for API * back-compat; will be removed in a future major. */ scrollIsolation?: boolean; /** * Line count at which the viewer starts to scroll instead of growing. * ``undefined`` (default) = always grows to fit content, no scroll. * Set e.g. ``50`` to cap short snippets inline and scroll long ones. */ maxLines?: number; /** * When set, an expand (⤢) button appears in the header — but ONLY while * the content is actually height-capped (i.e. exceeds `maxLines`). Lets a * host open the full code full-screen. No-op when the code fits. */ onExpand?: () => void; /** * Visual variant. ``"card"`` (default) ships full chrome (border, * background, hover toolbar, optional internal scroll). ``"plain"`` * is chrome-less — use when embedding inside another scroll * container so the surface manages its own chrome and scroll. */ variant?: 'card' | 'plain'; } const PrettyCode = ({ data, language, className, mode, inline = false, customBg, isCompact = false, maxLines, onExpand, variant = 'card' }: PrettyCodeProps) => { const containerRef = useRef(null); const t = useAppT(); // Default to the host theme so the surface (`--code*` tokens) and the Prism // palette move together; an explicit `mode` prop still pins a palette. const resolvedTheme = useResolvedTheme(); const effectiveMode = mode ?? (resolvedTheme === 'light' ? 'light' : 'dark'); // Subscribe to the extra-grammars ready state. When ``bash`` / // ``ruby`` / ``java`` / ``php`` finish loading, this hook triggers a // re-render so the code block picks up the new grammar. useEnsurePrismLanguages(); const labels = useMemo(() => ({ copyCode: t('tools.code.copyCode'), noContent: t('tools.code.noContent'), }), [t]); // Font size based on compact mode const fontSize = isCompact ? '0.75rem' : '0.875rem'; // 12px vs 14px const isDarkMode = effectiveMode !== 'light'; const prismTheme = isDarkMode ? themes.vsDark : themes.github; // The surface now reads the `--code*` tokens in BOTH modes — they already // carry the right neutral per theme, so the panel matches the UI and never // reads blue. (Previously light mode fell back to `bg-card` and dark mode // hard-coded GitHub's #0d1117.) const surfaceStyle = { backgroundColor: CODE_SURFACE_BG, borderColor: CODE_SURFACE_BORDER }; const inlineSurfaceStyle = { backgroundColor: CODE_INLINE_BG }; // Convert form object to JSON string with proper formatting const contentJson = typeof data === 'string' ? data : JSON.stringify(data || {}, null, 2); // Enable scroll only when content exceeds maxLines. Otherwise the block // grows to fit — short snippets feel natural, long ones get a cap. const lineCount = contentJson ? contentJson.split('\n').length : 0; const lineHeightRatio = isCompact ? 1.4 : 1.5; const fontSizePx = isCompact ? 12 : 14; // Vertical padding of the
 (top + bottom, in px) — keep in sync
  // with the padding string below.
  const verticalPadPx = 16 + 12; // 1rem top + 0.75rem bottom (≈)
  const shouldScroll = maxLines !== undefined && lineCount > maxLines;
  const maxHeightPx = maxLines !== undefined
    ? maxLines * fontSizePx * lineHeightRatio + verticalPadPx
    : undefined;
  
  // Handle empty content
  if (!contentJson || contentJson.trim() === '') {
    const emptyBgClass = customBg || '';
    return (
      

{labels.noContent}

); } // Get display name for language badge const getLanguageDisplayName = (lang: string): string => { switch (lang.toLowerCase()) { case 'bash': case 'shell': return 'Bash'; case 'python': case 'py': return 'Python'; case 'javascript': case 'js': return 'JavaScript'; case 'typescript': case 'ts': return 'TypeScript'; case 'json': return 'JSON'; case 'yaml': case 'yml': return 'YAML'; case 'html': return 'HTML'; case 'css': return 'CSS'; case 'sql': return 'SQL'; case 'xml': return 'XML'; case 'markdown': case 'md': return 'Markdown'; case 'plaintext': case 'text': return 'Text'; case 'mermaid': return 'Mermaid'; case 'ruby': case 'rb': return 'Ruby'; case 'java': return 'Java'; case 'php': return 'PHP'; case 'go': case 'golang': return 'Go'; default: return lang.charAt(0).toUpperCase() + lang.slice(1); } }; // Normalize language for Prism - use only basic supported languages const normalizedLanguage = (() => { const lang = language.toLowerCase(); // Try basic languages that are definitely supported switch (lang) { case 'javascript': case 'js': return 'javascript'; case 'typescript': case 'ts': return 'typescript'; // Try TypeScript first case 'python': case 'py': return 'python'; case 'json': return 'json'; case 'css': return 'css'; case 'html': return 'markup'; case 'xml': return 'markup'; case 'bash': case 'shell': case 'sh': return 'bash'; case 'ruby': case 'rb': return 'ruby'; case 'java': return 'java'; case 'php': return 'php'; case 'go': case 'golang': return 'go'; case 'sql': return 'sql'; case 'yaml': case 'yml': return 'yaml'; case 'markdown': case 'md': return 'markdown'; case 'mermaid': return 'text'; // Mermaid is handled separately in MarkdownMessage default: // For unknown languages, try to use the original name first // If it doesn't work, Prism will fallback to plain text return lang || 'text'; } })(); const displayLanguage = getLanguageDisplayName(language); // Plain variant — chrome-less render for embedding inside other // scroll containers. No border, no background, no hover toolbar, no // internal scroll. The caller's ScrollArea/panel owns the chrome // and scroll responsibilities. if (variant === 'plain') { return ( {({ className: prismClassName, style, tokens, getLineProps, getTokenProps }) => { const { backgroundColor: _bg, ...restStyle } = style; return (
              {tokens.map((line, i) => (
                
{line.map((token, key) => ( ))}
))}
); }}
); } if (inline) { // Inline chip surface also comes from `--code-inline` via inlineSurfaceStyle. const inlineBgClass = customBg || ''; return ( {({ className: prismClassName, style, tokens, getTokenProps }) => ( {tokens.map((line, i) => line.map((token, key) => ( )), )} )} ); } // Code surface is fixed (dark by default). Falls back to semantic // Surface comes from the `--code*` tokens via `surfaceStyle` (both modes). const bgClass = customBg || ''; return (
{/* Persistent header bar (GitHub / ChatGPT style): language label left, copy button right, in flow — never floats over the code. */}
{({ className: prismClassName, style, tokens, getLineProps, getTokenProps }) => { // Remove background from Prism theme - we use our own via CSS const { backgroundColor: _bg, ...restStyle } = style; return (
              {tokens.map((line, i) => (
                
{line.map((token, key) => ( ))}
))}
)}}
); }; export default PrettyCode;