UI Components Medium
Code Comparison
Side-by-side code diff viewer with syntax-highlighted added and removed lines. Green for additions, red for removals, with line numbers and a dark editor theme.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #f1f5f9;
min-height: 100vh;
display: grid;
place-items: center;
padding: 2rem;
}
.diff-wrapper {
width: min(900px, 100%);
display: flex;
flex-direction: column;
gap: 1rem;
}
.diff-title {
font-size: 1.375rem;
font-weight: 700;
color: #f1f5f9;
}
/* ── Container ── */
.diff-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.875rem;
overflow: hidden;
}
/* ── Panel ── */
.diff-panel {
background: #111318;
display: flex;
flex-direction: column;
min-width: 0;
}
/* ── Header ── */
.diff-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 1rem;
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.diff-filename {
font-family: "Fira Code", "Cascadia Code", monospace;
font-size: 0.8rem;
color: #94a3b8;
}
.diff-badge {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.15rem 0.5rem;
border-radius: 999px;
}
.diff-badge--removed {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
}
.diff-badge--added {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
/* ── Code block ── */
.diff-code {
font-family: "Fira Code", "Cascadia Code", monospace;
font-size: 0.8rem;
line-height: 1.7;
overflow-x: auto;
padding: 0.5rem 0;
}
/* ── Line ── */
.diff-line {
display: flex;
padding: 0 0.75rem;
min-height: 1.7em;
}
.diff-line-number {
flex-shrink: 0;
width: 3ch;
text-align: right;
color: #475569;
margin-right: 1rem;
user-select: none;
}
.diff-line-content {
flex: 1;
white-space: pre;
color: #cbd5e1;
}
/* ── Diff highlights ── */
.diff-line--removed {
background: rgba(239, 68, 68, 0.08);
}
.diff-line--removed .diff-line-content {
color: #fca5a5;
}
.diff-line--removed .diff-line-number {
color: #f87171;
}
.diff-line--added {
background: rgba(34, 197, 94, 0.08);
}
.diff-line--added .diff-line-content {
color: #86efac;
}
.diff-line--added .diff-line-number {
color: #4ade80;
}
.diff-line--empty {
background: rgba(255, 255, 255, 0.02);
}
.diff-line--empty .diff-line-content {
color: transparent;
}
/* ── Responsive ── */
@media (max-width: 640px) {
.diff-container {
grid-template-columns: 1fr;
}
}(function () {
"use strict";
const beforeEl = document.getElementById("diff-before");
const afterEl = document.getElementById("diff-after");
if (!beforeEl || !afterEl) return;
// Sample code strings
const beforeCode = [
'import { useState } from "react";',
"",
"function Counter() {",
" const [count, setCount] = useState(0);",
"",
" function handleClick() {",
" setCount(count + 1);",
" }",
"",
" return (",
' <div className="counter">',
" <p>Count: {count}</p>",
" <button onClick={handleClick}>",
" Increment",
" </button>",
" </div>",
" );",
"}",
"",
"export default Counter;",
];
const afterCode = [
'import { useState, useCallback } from "react";',
"",
"function Counter({ initial = 0, step = 1 }) {",
" const [count, setCount] = useState(initial);",
"",
" const increment = useCallback(() => {",
" setCount((prev) => prev + step);",
" }, [step]);",
"",
" const decrement = useCallback(() => {",
" setCount((prev) => prev - step);",
" }, [step]);",
"",
" return (",
' <div className="counter">',
" <p>Count: {count}</p>",
' <div className="counter-actions">',
" <button onClick={decrement}>-</button>",
" <button onClick={increment}>+</button>",
" </div>",
" </div>",
" );",
"}",
"",
"export default Counter;",
];
// Simple line-by-line diff using LCS approach
function computeDiff(oldLines, newLines) {
const oldSet = new Set(oldLines.map((l, i) => i + ":" + l));
const newSet = new Set(newLines.map((l, i) => i + ":" + l));
// Find common lines (by content only, simple approach)
const oldResult = [];
const newResult = [];
let oi = 0;
let ni = 0;
while (oi < oldLines.length || ni < newLines.length) {
if (oi < oldLines.length && ni < newLines.length && oldLines[oi] === newLines[ni]) {
// Same line
oldResult.push({ text: oldLines[oi], type: "same", num: oi + 1 });
newResult.push({ text: newLines[ni], type: "same", num: ni + 1 });
oi++;
ni++;
} else {
// Check if old line exists later in new
const oldInNew = newLines.indexOf(oldLines[oi], ni);
const newInOld = oldLines.indexOf(newLines[ni], oi);
if (oi < oldLines.length && (oldInNew === -1 || (newInOld !== -1 && newInOld <= oi + 2))) {
// Removed line
oldResult.push({ text: oldLines[oi], type: "removed", num: oi + 1 });
newResult.push({ text: "", type: "empty", num: null });
oi++;
} else if (ni < newLines.length) {
// Added line
oldResult.push({ text: "", type: "empty", num: null });
newResult.push({ text: newLines[ni], type: "added", num: ni + 1 });
ni++;
} else {
oi++;
ni++;
}
}
}
return { oldResult, newResult };
}
function renderLines(container, lines) {
lines.forEach(function (line) {
var div = document.createElement("div");
div.className = "diff-line";
if (line.type !== "same") {
div.classList.add("diff-line--" + line.type);
}
var numSpan = document.createElement("span");
numSpan.className = "diff-line-number";
numSpan.textContent = line.num !== null ? line.num : "";
var contentSpan = document.createElement("span");
contentSpan.className = "diff-line-content";
var prefix = "";
if (line.type === "removed") prefix = "- ";
else if (line.type === "added") prefix = "+ ";
else if (line.type === "empty") prefix = " ";
else prefix = " ";
contentSpan.textContent = prefix + line.text;
div.appendChild(numSpan);
div.appendChild(contentSpan);
container.appendChild(div);
});
}
var diff = computeDiff(beforeCode, afterCode);
renderLines(beforeEl, diff.oldResult);
renderLines(afterEl, diff.newResult);
// Sync scroll
var panels = document.querySelectorAll(".diff-code");
panels.forEach(function (panel) {
panel.addEventListener("scroll", function () {
panels.forEach(function (other) {
if (other !== panel) {
other.scrollTop = panel.scrollTop;
}
});
});
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Code Comparison</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="diff-wrapper">
<h2 class="diff-title">Code Changes</h2>
<div class="diff-container" id="diff-container">
<div class="diff-panel diff-panel--before">
<div class="diff-header">
<span class="diff-filename">utils.ts</span>
<span class="diff-badge diff-badge--removed">Before</span>
</div>
<div class="diff-code" id="diff-before"></div>
</div>
<div class="diff-panel diff-panel--after">
<div class="diff-header">
<span class="diff-filename">utils.ts</span>
<span class="diff-badge diff-badge--added">After</span>
</div>
<div class="diff-code" id="diff-after"></div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useMemo, useRef, useCallback } from "react";
interface CodeComparisonProps {
before?: string;
after?: string;
beforeLabel?: string;
afterLabel?: string;
filename?: string;
}
interface DiffLine {
text: string;
type: "same" | "added" | "removed" | "empty";
num: number | null;
}
function computeDiff(
oldLines: string[],
newLines: string[]
): { oldResult: DiffLine[]; newResult: DiffLine[] } {
const oldResult: DiffLine[] = [];
const newResult: DiffLine[] = [];
let oi = 0;
let ni = 0;
while (oi < oldLines.length || ni < newLines.length) {
if (oi < oldLines.length && ni < newLines.length && oldLines[oi] === newLines[ni]) {
oldResult.push({ text: oldLines[oi], type: "same", num: oi + 1 });
newResult.push({ text: newLines[ni], type: "same", num: ni + 1 });
oi++;
ni++;
} else {
const oldInNew = newLines.indexOf(oldLines[oi], ni);
const newInOld = oldLines.indexOf(newLines[ni], oi);
if (oi < oldLines.length && (oldInNew === -1 || (newInOld !== -1 && newInOld <= oi + 2))) {
oldResult.push({ text: oldLines[oi], type: "removed", num: oi + 1 });
newResult.push({ text: "", type: "empty", num: null });
oi++;
} else if (ni < newLines.length) {
oldResult.push({ text: "", type: "empty", num: null });
newResult.push({ text: newLines[ni], type: "added", num: ni + 1 });
ni++;
} else {
oi++;
ni++;
}
}
}
return { oldResult, newResult };
}
const sampleBefore = `import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div className="counter">
<p>Count: {count}</p>
<button onClick={handleClick}>
Increment
</button>
</div>
);
}
export default Counter;`;
const sampleAfter = `import { useState, useCallback } from "react";
function Counter({ initial = 0, step = 1 }) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => {
setCount((prev) => prev + step);
}, [step]);
const decrement = useCallback(() => {
setCount((prev) => prev - step);
}, [step]);
return (
<div className="counter">
<p>Count: {count}</p>
<div className="counter-actions">
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
</div>
);
}
export default Counter;`;
function DiffPanel({
lines,
label,
labelType,
filename,
onScroll,
scrollRef,
}: {
lines: DiffLine[];
label: string;
labelType: "before" | "after";
filename: string;
onScroll: (scrollTop: number) => void;
scrollRef: React.RefObject<HTMLDivElement>;
}) {
return (
<div style={{ background: "#111318", display: "flex", flexDirection: "column", minWidth: 0 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0.625rem 1rem",
background: "rgba(255,255,255,0.03)",
borderBottom: "1px solid rgba(255,255,255,0.06)",
}}
>
<span
style={{
fontFamily: '"Fira Code", "Cascadia Code", monospace',
fontSize: "0.8rem",
color: "#94a3b8",
}}
>
{filename}
</span>
<span
style={{
fontSize: "0.65rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.05em",
padding: "0.15rem 0.5rem",
borderRadius: 999,
background: labelType === "before" ? "rgba(239,68,68,0.15)" : "rgba(34,197,94,0.15)",
color: labelType === "before" ? "#f87171" : "#4ade80",
}}
>
{label}
</span>
</div>
<div
ref={scrollRef}
onScroll={(e) => onScroll((e.target as HTMLDivElement).scrollTop)}
style={{
fontFamily: '"Fira Code", "Cascadia Code", monospace',
fontSize: "0.8rem",
lineHeight: 1.7,
overflowX: "auto",
overflowY: "auto",
padding: "0.5rem 0",
}}
>
{lines.map((line, i) => {
const bgMap: Record<string, string> = {
removed: "rgba(239,68,68,0.08)",
added: "rgba(34,197,94,0.08)",
empty: "rgba(255,255,255,0.02)",
same: "transparent",
};
const colorMap: Record<string, string> = {
removed: "#fca5a5",
added: "#86efac",
empty: "transparent",
same: "#cbd5e1",
};
const numColorMap: Record<string, string> = {
removed: "#f87171",
added: "#4ade80",
empty: "transparent",
same: "#475569",
};
const prefix = line.type === "removed" ? "- " : line.type === "added" ? "+ " : " ";
return (
<div
key={i}
style={{
display: "flex",
padding: "0 0.75rem",
minHeight: "1.7em",
background: bgMap[line.type],
}}
>
<span
style={{
flexShrink: 0,
width: "3ch",
textAlign: "right",
color: numColorMap[line.type],
marginRight: "1rem",
userSelect: "none",
}}
>
{line.num ?? ""}
</span>
<span
style={{
flex: 1,
whiteSpace: "pre",
color: colorMap[line.type],
}}
>
{prefix}
{line.text}
</span>
</div>
);
})}
</div>
</div>
);
}
export default function CodeComparison({
before = sampleBefore,
after = sampleAfter,
beforeLabel = "Before",
afterLabel = "After",
filename = "utils.ts",
}: CodeComparisonProps) {
const beforeRef = useRef<HTMLDivElement>(null);
const afterRef = useRef<HTMLDivElement>(null);
const syncing = useRef(false);
const diff = useMemo(() => {
return computeDiff(before.split("\n"), after.split("\n"));
}, [before, after]);
const handleScroll = useCallback((source: "before" | "after", scrollTop: number) => {
if (syncing.current) return;
syncing.current = true;
const target = source === "before" ? afterRef.current : beforeRef.current;
if (target) target.scrollTop = scrollTop;
requestAnimationFrame(() => {
syncing.current = false;
});
}, []);
return (
<div
style={{
minHeight: "100vh",
background: "#0a0a0a",
display: "grid",
placeItems: "center",
padding: "2rem",
fontFamily: "system-ui, -apple-system, sans-serif",
color: "#f1f5f9",
}}
>
<div
style={{ width: "min(900px, 100%)", display: "flex", flexDirection: "column", gap: "1rem" }}
>
<h2 style={{ fontSize: "1.375rem", fontWeight: 700 }}>Code Changes</h2>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: 1,
background: "rgba(255,255,255,0.06)",
border: "1px solid rgba(255,255,255,0.08)",
borderRadius: "0.875rem",
overflow: "hidden",
}}
>
<DiffPanel
lines={diff.oldResult}
label={beforeLabel}
labelType="before"
filename={filename}
scrollRef={beforeRef as React.RefObject<HTMLDivElement>}
onScroll={(st) => handleScroll("before", st)}
/>
<DiffPanel
lines={diff.newResult}
label={afterLabel}
labelType="after"
filename={filename}
scrollRef={afterRef as React.RefObject<HTMLDivElement>}
onScroll={(st) => handleScroll("after", st)}
/>
</div>
</div>
</div>
);
}<script setup>
import { ref, computed } from "vue";
const before = ref(`import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div className="counter">
<p>Count: {count}</p>
<button onClick={handleClick}>
Increment
</button>
</div>
);
}
export default Counter;`);
const after = ref(`import { useState, useCallback } from "react";
function Counter({ initial = 0, step = 1 }) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => {
setCount((prev) => prev + step);
}, [step]);
const decrement = useCallback(() => {
setCount((prev) => prev - step);
}, [step]);
return (
<div className="counter">
<p>Count: {count}</p>
<div className="counter-actions">
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
</div>
);
}
export default Counter;`);
const beforeLabel = "Before";
const afterLabel = "After";
const filename = "utils.ts";
const beforeScrollEl = ref(null);
const afterScrollEl = ref(null);
let syncing = false;
function computeDiff(oldLines, newLines) {
const oldResult = [];
const newResult = [];
let oi = 0;
let ni = 0;
while (oi < oldLines.length || ni < newLines.length) {
if (oi < oldLines.length && ni < newLines.length && oldLines[oi] === newLines[ni]) {
oldResult.push({ text: oldLines[oi], type: "same", num: oi + 1 });
newResult.push({ text: newLines[ni], type: "same", num: ni + 1 });
oi++;
ni++;
} else {
const oldInNew = newLines.indexOf(oldLines[oi], ni);
const newInOld = oldLines.indexOf(newLines[ni], oi);
if (oi < oldLines.length && (oldInNew === -1 || (newInOld !== -1 && newInOld <= oi + 2))) {
oldResult.push({ text: oldLines[oi], type: "removed", num: oi + 1 });
newResult.push({ text: "", type: "empty", num: null });
oi++;
} else if (ni < newLines.length) {
oldResult.push({ text: "", type: "empty", num: null });
newResult.push({ text: newLines[ni], type: "added", num: ni + 1 });
ni++;
} else {
oi++;
ni++;
}
}
}
return { oldResult, newResult };
}
const diff = computed(() => computeDiff(before.value.split("\n"), after.value.split("\n")));
const bgMap = {
removed: "rgba(239,68,68,0.08)",
added: "rgba(34,197,94,0.08)",
empty: "rgba(255,255,255,0.02)",
same: "transparent",
};
const colorMap = { removed: "#fca5a5", added: "#86efac", empty: "transparent", same: "#cbd5e1" };
const numColorMap = { removed: "#f87171", added: "#4ade80", empty: "transparent", same: "#475569" };
function getPrefix(type) {
if (type === "removed") return "- ";
if (type === "added") return "+ ";
return " ";
}
function handleScroll(source) {
if (syncing) return;
syncing = true;
if (source === "before" && afterScrollEl.value && beforeScrollEl.value) {
afterScrollEl.value.scrollTop = beforeScrollEl.value.scrollTop;
} else if (source === "after" && beforeScrollEl.value && afterScrollEl.value) {
beforeScrollEl.value.scrollTop = afterScrollEl.value.scrollTop;
}
requestAnimationFrame(() => {
syncing = false;
});
}
</script>
<template>
<div style="min-height: 100vh; background: #0a0a0a; display: grid; place-items: center; padding: 2rem; font-family: system-ui, -apple-system, sans-serif; color: #f1f5f9;">
<div style="width: min(900px, 100%); display: flex; flex-direction: column; gap: 1rem;">
<h2 style="font-size: 1.375rem; font-weight: 700;">Code Changes</h2>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.875rem; overflow: hidden;">
<!-- Before panel -->
<div style="background: #111318; display: flex; flex-direction: column; min-width: 0;">
<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.625rem 1rem; background: rgba(255,255,255,0.03); border-bottom: 1px solid rgba(255,255,255,0.06);">
<span style="font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 0.8rem; color: #94a3b8;">{{ filename }}</span>
<span style="font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.15rem 0.5rem; border-radius: 999px; background: rgba(239,68,68,0.15); color: #f87171;">{{ beforeLabel }}</span>
</div>
<div
ref="beforeScrollEl"
@scroll="handleScroll('before')"
style="font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 0.8rem; line-height: 1.7; overflow-x: auto; overflow-y: auto; padding: 0.5rem 0;"
>
<div
v-for="(line, i) in diff.oldResult"
:key="i"
:style="{ display: 'flex', padding: '0 0.75rem', minHeight: '1.7em', background: bgMap[line.type] }"
>
<span :style="{ flexShrink: 0, width: '3ch', textAlign: 'right', color: numColorMap[line.type], marginRight: '1rem', userSelect: 'none' }">
{{ line.num ?? '' }}
</span>
<span :style="{ flex: 1, whiteSpace: 'pre', color: colorMap[line.type] }">{{ getPrefix(line.type) }}{{ line.text }}</span>
</div>
</div>
</div>
<!-- After panel -->
<div style="background: #111318; display: flex; flex-direction: column; min-width: 0;">
<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.625rem 1rem; background: rgba(255,255,255,0.03); border-bottom: 1px solid rgba(255,255,255,0.06);">
<span style="font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 0.8rem; color: #94a3b8;">{{ filename }}</span>
<span style="font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.15rem 0.5rem; border-radius: 999px; background: rgba(34,197,94,0.15); color: #4ade80;">{{ afterLabel }}</span>
</div>
<div
ref="afterScrollEl"
@scroll="handleScroll('after')"
style="font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 0.8rem; line-height: 1.7; overflow-x: auto; overflow-y: auto; padding: 0.5rem 0;"
>
<div
v-for="(line, i) in diff.newResult"
:key="i"
:style="{ display: 'flex', padding: '0 0.75rem', minHeight: '1.7em', background: bgMap[line.type] }"
>
<span :style="{ flexShrink: 0, width: '3ch', textAlign: 'right', color: numColorMap[line.type], marginRight: '1rem', userSelect: 'none' }">
{{ line.num ?? '' }}
</span>
<span :style="{ flex: 1, whiteSpace: 'pre', color: colorMap[line.type] }">{{ getPrefix(line.type) }}{{ line.text }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template><script>
let before = `import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div className="counter">
<p>Count: {count}</p>
<button onClick={handleClick}>
Increment
</button>
</div>
);
}
export default Counter;`;
let after = `import { useState, useCallback } from "react";
function Counter({ initial = 0, step = 1 }) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => {
setCount((prev) => prev + step);
}, [step]);
const decrement = useCallback(() => {
setCount((prev) => prev - step);
}, [step]);
return (
<div className="counter">
<p>Count: {count}</p>
<div className="counter-actions">
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
</div>
);
}
export default Counter;`;
let beforeLabel = "Before";
let afterLabel = "After";
let filename = "utils.ts";
let beforeScrollEl;
let afterScrollEl;
let syncing = false;
function computeDiff(oldLines, newLines) {
const oldResult = [];
const newResult = [];
let oi = 0;
let ni = 0;
while (oi < oldLines.length || ni < newLines.length) {
if (oi < oldLines.length && ni < newLines.length && oldLines[oi] === newLines[ni]) {
oldResult.push({ text: oldLines[oi], type: "same", num: oi + 1 });
newResult.push({ text: newLines[ni], type: "same", num: ni + 1 });
oi++;
ni++;
} else {
const oldInNew = newLines.indexOf(oldLines[oi], ni);
const newInOld = oldLines.indexOf(newLines[ni], oi);
if (oi < oldLines.length && (oldInNew === -1 || (newInOld !== -1 && newInOld <= oi + 2))) {
oldResult.push({ text: oldLines[oi], type: "removed", num: oi + 1 });
newResult.push({ text: "", type: "empty", num: null });
oi++;
} else if (ni < newLines.length) {
oldResult.push({ text: "", type: "empty", num: null });
newResult.push({ text: newLines[ni], type: "added", num: ni + 1 });
ni++;
} else {
oi++;
ni++;
}
}
}
return { oldResult, newResult };
}
$: diff = computeDiff(before.split("\n"), after.split("\n"));
function handleScroll(source) {
if (syncing) return;
syncing = true;
if (source === "before" && afterScrollEl && beforeScrollEl) {
afterScrollEl.scrollTop = beforeScrollEl.scrollTop;
} else if (source === "after" && beforeScrollEl && afterScrollEl) {
beforeScrollEl.scrollTop = afterScrollEl.scrollTop;
}
requestAnimationFrame(() => {
syncing = false;
});
}
const bgMap = {
removed: "rgba(239,68,68,0.08)",
added: "rgba(34,197,94,0.08)",
empty: "rgba(255,255,255,0.02)",
same: "transparent",
};
const colorMap = { removed: "#fca5a5", added: "#86efac", empty: "transparent", same: "#cbd5e1" };
const numColorMap = { removed: "#f87171", added: "#4ade80", empty: "transparent", same: "#475569" };
function getPrefix(type) {
if (type === "removed") return "- ";
if (type === "added") return "+ ";
return " ";
}
</script>
<div style="min-height: 100vh; background: #0a0a0a; display: grid; place-items: center; padding: 2rem; font-family: system-ui, -apple-system, sans-serif; color: #f1f5f9;">
<div style="width: min(900px, 100%); display: flex; flex-direction: column; gap: 1rem;">
<h2 style="font-size: 1.375rem; font-weight: 700;">Code Changes</h2>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.875rem; overflow: hidden;">
<!-- Before panel -->
<div style="background: #111318; display: flex; flex-direction: column; min-width: 0;">
<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.625rem 1rem; background: rgba(255,255,255,0.03); border-bottom: 1px solid rgba(255,255,255,0.06);">
<span style="font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 0.8rem; color: #94a3b8;">{filename}</span>
<span style="font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.15rem 0.5rem; border-radius: 999px; background: rgba(239,68,68,0.15); color: #f87171;">{beforeLabel}</span>
</div>
<div
bind:this={beforeScrollEl}
on:scroll={() => handleScroll('before')}
style="font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 0.8rem; line-height: 1.7; overflow-x: auto; overflow-y: auto; padding: 0.5rem 0;"
>
{#each diff.oldResult as line, i}
<div style="display: flex; padding: 0 0.75rem; min-height: 1.7em; background: {bgMap[line.type]};">
<span style="flex-shrink: 0; width: 3ch; text-align: right; color: {numColorMap[line.type]}; margin-right: 1rem; user-select: none;">
{line.num ?? ''}
</span>
<span style="flex: 1; white-space: pre; color: {colorMap[line.type]};">
{getPrefix(line.type)}{line.text}
</span>
</div>
{/each}
</div>
</div>
<!-- After panel -->
<div style="background: #111318; display: flex; flex-direction: column; min-width: 0;">
<div style="display: flex; align-items: center; justify-content: space-between; padding: 0.625rem 1rem; background: rgba(255,255,255,0.03); border-bottom: 1px solid rgba(255,255,255,0.06);">
<span style="font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 0.8rem; color: #94a3b8;">{filename}</span>
<span style="font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.15rem 0.5rem; border-radius: 999px; background: rgba(34,197,94,0.15); color: #4ade80;">{afterLabel}</span>
</div>
<div
bind:this={afterScrollEl}
on:scroll={() => handleScroll('after')}
style="font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 0.8rem; line-height: 1.7; overflow-x: auto; overflow-y: auto; padding: 0.5rem 0;"
>
{#each diff.newResult as line, i}
<div style="display: flex; padding: 0 0.75rem; min-height: 1.7em; background: {bgMap[line.type]};">
<span style="flex-shrink: 0; width: 3ch; text-align: right; color: {numColorMap[line.type]}; margin-right: 1rem; user-select: none;">
{line.num ?? ''}
</span>
<span style="flex: 1; white-space: pre; color: {colorMap[line.type]};">
{getPrefix(line.type)}{line.text}
</span>
</div>
{/each}
</div>
</div>
</div>
</div>
</div>Code Comparison
A side-by-side code diff viewer that highlights differences between two code strings. Removed lines are shown in red on the left panel, added lines in green on the right panel, with line numbers and a dark editor aesthetic.
How it works
- Two code strings (before/after) are parsed line by line.
- A simple diff algorithm compares lines and marks them as added, removed, or unchanged.
- Lines are rendered into two panels with appropriate background colors and line number gutters.
Features
- Side-by-side layout with synchronized scrolling
- Green highlights for additions, red for removals
- Monospace font with line numbers
- File name headers on each panel