Content & marketing
Code Diff code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/code-diff.blade.php
@props([
'lines' => [],
'filename' => null,
'lang' => null,
'numbers' => false,
'startLine' => 1,
'copy' => true,
])
@php
use Tempest\Highlight\Highlighter;
/*
| Git-style diff viewer. Removed lines read red and added lines read green,
| with server-side syntax highlighting via tempest/highlight. Theme-aware:
| light surface by default, dark surface under the site's `.dark` theme.
|
| Pass a `:lines` array. Each entry is either:
| - a string prefixed with `+ ` (added) or `- ` (removed); anything else is
| context and keeps its indentation, or
| - an array shape ['type' => 'add'|'del'|'context', 'text' => '…'].
|
| Usage:
| <x-ui.code-diff filename="routes/web.php" lang="php" numbers :lines="[
| 'Route::get(\'/\', fn () => view(\'welcome\'));',
| '- Route::get(\'/old\', [OldController::class, \'index\']);',
| '+ Route::get(\'/new\', [NewController::class, \'index\']);',
| ]" />
|
| A trailing slot is appended after the parsed lines for fully manual rows
| (use .diff-line with .diff-add / .diff-del).
*/
$parsed = [];
foreach ($lines as $line) {
if (is_array($line)) {
$type = $line['type'] ?? 'context';
$text = (string) ($line['text'] ?? '');
} else {
$text = (string) $line;
$first = $text[0] ?? '';
if ($first === '+') {
$type = 'add';
$text = preg_replace('/^\+ ?/', '', $text);
} elseif ($first === '-') {
$type = 'del';
$text = preg_replace('/^- ?/', '', $text);
} else {
$type = 'context';
}
}
$parsed[] = ['type' => $type, 'text' => $text];
}
// Highlight each line independently so a removed/added marker never bleeds
// into the next line's tokenizer state. Falls back to plain (escaped) text
// for unknown languages.
$grammar = $lang ? strtolower($lang) : 'text';
$highlighter = new Highlighter();
foreach ($parsed as $i => $line) {
$parsed[$i]['html'] = $line['text'] === ''
? ''
: $highlighter->parse($line['text'], $grammar);
}
$additions = count(array_filter($parsed, fn ($l) => $l['type'] === 'add'));
$deletions = count(array_filter($parsed, fn ($l) => $l['type'] === 'del'));
// Reconstruct a copy-able unified diff body (keeps the +/- markers).
$plain = implode("\n", array_map(
fn ($l) => ($l['type'] === 'add' ? '+ ' : ($l['type'] === 'del' ? '- ' : ' ')).$l['text'],
$parsed,
));
$rows = [
'add' => 'bg-green-500/10 border-green-500/60 dark:bg-green-500/10',
'del' => 'bg-red-500/10 border-red-500/60 dark:bg-red-500/10',
'context' => 'border-transparent',
];
$signs = [
'add' => ['+', 'text-green-600 dark:text-green-400'],
'del' => ['-', 'text-red-600 dark:text-red-400'],
'context' => ['', 'text-gray-400 dark:text-gray-600'],
];
// Map tempest/highlight token classes onto the Harbour palette so snippets
// read on-brand in both light and dark surfaces.
$tokens = '[&_.hl-keyword]:text-brand-700 dark:[&_.hl-keyword]:text-brand-300 '
.'[&_.hl-type]:text-amber-700 dark:[&_.hl-type]:text-amber-300 '
.'[&_.hl-property]:text-amber-600 dark:[&_.hl-property]:text-amber-200 '
.'[&_.hl-value]:text-green-700 dark:[&_.hl-value]:text-green-300 '
.'[&_.hl-number]:text-amber-600 dark:[&_.hl-number]:text-amber-200 '
.'[&_.hl-literal]:text-amber-600 dark:[&_.hl-literal]:text-amber-200 '
.'[&_.hl-comment]:text-gray-400 dark:[&_.hl-comment]:text-gray-500 [&_.hl-comment]:italic '
.'[&_.hl-operator]:text-gray-500 dark:[&_.hl-operator]:text-gray-400 '
.'[&_.hl-variable]:text-gray-800 dark:[&_.hl-variable]:text-gray-100 '
.'[&_.hl-attribute]:text-brand-700 dark:[&_.hl-attribute]:text-brand-300 '
.'[&_.hl-generic]:text-gray-600 dark:[&_.hl-generic]:text-gray-300 '
.'[&_.hl-injection]:text-brand-700 dark:[&_.hl-injection]:text-brand-300';
$oldNo = (int) $startLine;
$newNo = (int) $startLine;
@endphp
<div
x-data="{
copied: false,
copyDiff() {
navigator.clipboard.writeText(@js($plain)).then(() => {
this.copied = true;
clearTimeout(this._timer);
this._timer = setTimeout(() => (this.copied = false), 1800);
});
},
}"
{{ $attributes->merge(['class' => 'group/diff overflow-hidden rounded-xl border border-gray-200/80 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-2xl shadow-gray-900/10 dark:shadow-gray-950/40 ring-1 ring-gray-900/5 dark:ring-white/5']) }}
>
{{-- Title bar --}}
<div class="flex items-center gap-3 border-b border-gray-200/80 dark:border-gray-800 bg-gray-50/80 dark:bg-gray-900/60 px-4 py-2.5">
@if ($filename)
<span class="flex items-center gap-1.5 text-xs font-medium text-gray-500 dark:text-gray-400">
<x-ui.icon name="file-code" size="sm" />
{{ $filename }}
</span>
@endif
<div class="ml-auto flex items-center gap-3">
@if ($additions || $deletions)
<span class="flex items-center gap-2 font-mono text-[11px] font-semibold">
<span class="text-green-600 dark:text-green-400">+{{ $additions }}</span>
<span class="text-red-600 dark:text-red-400">-{{ $deletions }}</span>
</span>
@endif
@if ($lang)
<span class="hidden text-[11px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-600 sm:inline">{{ $lang }}</span>
@endif
@if ($copy)
<button
type="button"
@click="copyDiff()"
aria-label="Copy diff"
class="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-gray-500 dark:text-gray-400 transition hover:bg-gray-200/70 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer"
>
<x-ui.icon x-show="! copied" name="copy" size="sm" />
<x-ui.icon x-show="copied" x-cloak name="check" weight="bold" size="sm" class="text-brand-500 dark:text-brand-400" />
<span x-text="copied ? 'Copied' : 'Copy'"></span>
</button>
@endif
</div>
</div>
{{-- Diff body --}}
<div class="overflow-x-auto py-2 font-mono text-[13px] leading-relaxed text-gray-700 dark:text-gray-300 {{ $tokens }}">
@foreach ($parsed as $line)
@php
[$sign, $signColor] = $signs[$line['type']] ?? $signs['context'];
$showOld = $line['type'] !== 'add';
$showNew = $line['type'] !== 'del';
$old = $showOld ? $oldNo++ : null;
$new = $showNew ? $newNo++ : null;
@endphp
<div class="diff-line flex border-l-2 {{ $rows[$line['type']] ?? $rows['context'] }}">
@if ($numbers)
<span class="w-10 flex-none select-none px-2 text-right text-[11px] text-gray-400 dark:text-gray-600">{{ $old }}</span>
<span class="w-10 flex-none select-none px-2 text-right text-[11px] text-gray-400 dark:text-gray-600">{{ $new }}</span>
@endif
<span class="w-5 flex-none select-none text-center {{ $signColor }}">{{ $sign }}</span>
<span class="flex-1 whitespace-pre pr-4 sm:pr-5">{!! $line['html'] !!}</span>
</div>
@endforeach
{{ $slot }}
</div>
</div>