// Adapted from jalcoui (MIT) — github.com/jal-co/ui // // Line-by-line code diff viewer. Supports unified (single column) and // split (side-by-side) layouts. Syntax highlighting reuses the same // `prism-react-renderer` bundle that the `PrettyCode` tool already // pulls in — no second highlighter is loaded. // // Requires at the host root (no nested providers here). // See CONTRACT.md §5. // // Unlike `PrettyCode` (intentionally always-dark), DiffViewer adapts to // the host theme. Diffs are commonly embedded in documentation pages // where forcing a dark surface on a light page reads as a bug. The // Prism theme is picked per host theme (`themes.vsDark` for dark, // `themes.github` for light) and surface chrome uses semantic tokens. 'use client'; import * as React from 'react'; import { themes, type PrismTheme } from 'prism-react-renderer'; import { cn } from '@djangocfg/ui-core/lib'; import { useResolvedTheme } from '@djangocfg/ui-core/hooks'; import { CopyButton } from './components/CopyButton'; import { SplitView } from './components/SplitView'; import { UnifiedView } from './components/UnifiedView'; import { useDiff } from './hooks/useDiff'; import { useDiffLanguage } from './hooks/useHighlighter'; import { DIFF_TONE_CLASSES, type DiffInput, type DiffViewerProps, } from './types'; export function DiffViewer({ layout = 'unified', language, oldTitle, newTitle, copyable = true, context = 3, className, ...allProps }: DiffViewerProps) { // Split the diff-input keys off the DOM rest props so we don't spread // `oldCode` / `newCode` / `patch` onto the wrapping
. const { oldCode, newCode, patch, ...divProps } = allProps as DiffViewerProps & Record; const input: DiffInput = patch !== undefined ? ({ patch: patch as string } as DiffInput) : ({ oldCode: (oldCode as string) ?? '', newCode: (newCode as string) ?? '', } as DiffInput); const { lines, numWidth, stats } = useDiff({ input, context }); // Pass `null` when no language → cheap plain-text render path. const prismLang = useDiffLanguage(language); const resolvedLanguage = language ? prismLang : null; // Pick a Prism palette to match the host theme. Resolved once at the // viewer root, then forwarded to every line — keeps the hook out of // per-line render paths (could be thousands of rows on big diffs). const resolved = useResolvedTheme(); const prismTheme: PrismTheme = resolved === 'dark' ? themes.vsDark : themes.github; // Full text used for the copy button — concatenates current lines as // displayed, including removed ones. Mirrors jalcoui's behavior. const fullCode = React.useMemo( () => lines.map((l) => l.content).join('\n'), [lines], ); const showHeader = Boolean(oldTitle || newTitle); const addedTone = DIFF_TONE_CLASSES.added.stat; const removedTone = DIFF_TONE_CLASSES.removed.stat; return (
)} > {showHeader && (
{oldTitle && ( {oldTitle} )} {oldTitle && newTitle && ( )} {newTitle && ( {newTitle} )}
{stats.added > 0 && ( +{stats.added} )} {stats.removed > 0 && ( -{stats.removed} )}
{copyable && }
)} {!showHeader && copyable && (
)}
{layout === 'split' ? ( ) : ( )}
); }