feat: fullscreen script diff with scroll sync

This commit is contained in:
Ahmed Darrazi 2026-01-01 22:54:23 +01:00
parent 2f3788372a
commit 72acc8db42
3 changed files with 241 additions and 46 deletions

View File

@ -282,6 +282,51 @@
$isScriptContent = $canHighlightScripts($policyType) && $isScriptKey($name);
$ops = $isScriptContent ? $scriptLineDiff((string) $fromText, (string) $toText) : [];
$useTorchlight = $isScriptContent && class_exists(\Torchlight\Engine\Engine::class);
$rows = [];
if ($isScriptContent) {
$count = count($ops);
for ($i = 0; $i < $count; $i++) {
$op = $ops[$i];
$next = $ops[$i + 1] ?? null;
$type = $op['type'] ?? null;
$line = (string) ($op['line'] ?? '');
if ($type === 'equal') {
$rows[] = [
'left' => ['type' => 'equal', 'line' => $line],
'right' => ['type' => 'equal', 'line' => $line],
];
continue;
}
if ($type === 'delete' && is_array($next) && ($next['type'] ?? null) === 'insert') {
$rows[] = [
'left' => ['type' => 'delete', 'line' => $line],
'right' => ['type' => 'insert', 'line' => (string) ($next['line'] ?? '')],
];
$i++;
continue;
}
if ($type === 'delete') {
$rows[] = [
'left' => ['type' => 'delete', 'line' => $line],
'right' => ['type' => 'blank', 'line' => ''],
];
continue;
}
if ($type === 'insert') {
$rows[] = [
'left' => ['type' => 'blank', 'line' => ''],
'right' => ['type' => 'insert', 'line' => $line],
];
continue;
}
}
}
@endphp
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
<div class="text-sm font-medium text-gray-900 dark:text-white">
@ -291,7 +336,7 @@
@if ($isScriptContent)
<div class="text-sm text-gray-600 dark:text-gray-300 sm:col-span-2">
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Script</span>
<details class="mt-1">
<details class="mt-1" x-data="{ fullscreenOpen: false }">
<summary class="cursor-pointer text-sm text-gray-700 dark:text-gray-200">
View
</summary>
@ -307,54 +352,13 @@
<x-filament::button size="xs" color="gray" type="button" x-on:click="tab = 'after'" x-bind:class="tab === 'after' ? 'ring-1 ring-gray-300 dark:ring-white/20' : ''">
After
</x-filament::button>
<x-filament::button size="xs" color="gray" type="button" x-on:click="fullscreenOpen = true">
Fullscreen
</x-filament::button>
</div>
<div x-show="tab === 'diff'" x-cloak>
@php
$rows = [];
$count = count($ops);
for ($i = 0; $i < $count; $i++) {
$op = $ops[$i];
$next = $ops[$i + 1] ?? null;
$type = $op['type'] ?? null;
$line = (string) ($op['line'] ?? '');
if ($type === 'equal') {
$rows[] = [
'left' => ['type' => 'equal', 'line' => $line],
'right' => ['type' => 'equal', 'line' => $line],
];
continue;
}
if ($type === 'delete' && is_array($next) && ($next['type'] ?? null) === 'insert') {
$rows[] = [
'left' => ['type' => 'delete', 'line' => $line],
'right' => ['type' => 'insert', 'line' => (string) ($next['line'] ?? '')],
];
$i++;
continue;
}
if ($type === 'delete') {
$rows[] = [
'left' => ['type' => 'delete', 'line' => $line],
'right' => ['type' => 'blank', 'line' => ''],
];
continue;
}
if ($type === 'insert') {
$rows[] = [
'left' => ['type' => 'blank', 'line' => ''],
'right' => ['type' => 'insert', 'line' => $line],
];
continue;
}
}
@endphp
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Old</div>
@ -476,6 +480,195 @@
@endif
</div>
</div>
<div
x-show="fullscreenOpen"
x-cloak
x-on:keydown.escape.window="fullscreenOpen = false"
class="fixed inset-0 z-50"
>
<div class="absolute inset-0 bg-gray-950/50"></div>
<div class="relative flex h-full w-full flex-col bg-white dark:bg-gray-900">
<div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-white/10">
<div class="text-sm font-medium text-gray-900 dark:text-white">Script diff</div>
<div class="flex items-center gap-2">
<x-filament::button size="sm" color="gray" type="button" x-on:click="fullscreenOpen = false">
Close
</x-filament::button>
</div>
</div>
<div class="flex-1 overflow-hidden p-4">
<div
x-data="{
tab: 'diff',
syncing: false,
syncHorizontal: true,
sync(from, to) {
if (this.syncing) return;
this.syncing = true;
to.scrollTop = from.scrollTop;
const bothHorizontal = this.syncHorizontal
&& from.scrollWidth > from.clientWidth
&& to.scrollWidth > to.clientWidth;
if (bothHorizontal) {
to.scrollLeft = from.scrollLeft;
}
requestAnimationFrame(() => { this.syncing = false; });
},
}"
x-init="$nextTick(() => {
const left = $refs.left;
const right = $refs.right;
if (!left || !right) return;
left.addEventListener('scroll', () => sync(left, right), { passive: true });
right.addEventListener('scroll', () => sync(right, left), { passive: true });
})"
class="h-full space-y-3"
>
<div class="flex flex-wrap items-center gap-2">
<x-filament::button size="sm" color="gray" type="button" x-on:click="tab = 'diff'" x-bind:class="tab === 'diff' ? 'ring-1 ring-gray-300 dark:ring-white/20' : ''">
Diff
</x-filament::button>
<x-filament::button size="sm" color="gray" type="button" x-on:click="tab = 'before'" x-bind:class="tab === 'before' ? 'ring-1 ring-gray-300 dark:ring-white/20' : ''">
Before
</x-filament::button>
<x-filament::button size="sm" color="gray" type="button" x-on:click="tab = 'after'" x-bind:class="tab === 'after' ? 'ring-1 ring-gray-300 dark:ring-white/20' : ''">
After
</x-filament::button>
</div>
<div x-show="tab === 'diff'" x-cloak class="h-[calc(100%-3rem)]">
<div class="grid h-full grid-cols-1 gap-4 lg:grid-cols-2">
<div class="flex h-full flex-col">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Old</div>
<pre x-ref="left" class="mt-2 flex-1 overflow-auto font-mono text-xs text-gray-800 dark:text-gray-200 whitespace-pre">@php
foreach ($rows as $row) {
$left = $row['left'];
$leftType = $left['type'];
$leftLine = (string) ($left['line'] ?? '');
$leftHighlighted = $useTorchlight ? $highlightInline($policyType, $leftLine) : null;
$leftRendered = (is_string($leftHighlighted) && $leftHighlighted !== '') ? $leftHighlighted : e($leftLine);
if ($leftType === 'equal') {
if ($useTorchlight) {
@endphp
@once
@include('filament.partials.torchlight-dark-overrides')
@endonce
@php
}
echo $leftRendered."\n";
continue;
}
if ($leftType === 'delete') {
if ($useTorchlight) {
@endphp
@once
@include('filament.partials.torchlight-dark-overrides')
@endonce
@php
}
echo '<span class="block bg-danger-50 text-danger-700 dark:bg-danger-950/40 dark:text-danger-200">- '.$leftRendered."</span>\n";
continue;
}
echo "\n";
}
@endphp</pre>
</div>
<div class="flex h-full flex-col">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">New</div>
<pre x-ref="right" class="mt-2 flex-1 overflow-auto font-mono text-xs text-gray-800 dark:text-gray-200 whitespace-pre">@php
foreach ($rows as $row) {
$right = $row['right'];
$rightType = $right['type'];
$rightLine = (string) ($right['line'] ?? '');
$rightHighlighted = $useTorchlight ? $highlightInline($policyType, $rightLine) : null;
$rightRendered = (is_string($rightHighlighted) && $rightHighlighted !== '') ? $rightHighlighted : e($rightLine);
if ($rightType === 'equal') {
if ($useTorchlight) {
@endphp
@once
@include('filament.partials.torchlight-dark-overrides')
@endonce
@php
}
echo $rightRendered."\n";
continue;
}
if ($rightType === 'insert') {
if ($useTorchlight) {
@endphp
@once
@include('filament.partials.torchlight-dark-overrides')
@endonce
@php
}
echo '<span class="block bg-success-50 text-success-700 dark:bg-success-950/40 dark:text-success-200">+ '.$rightRendered."</span>\n";
continue;
}
echo "\n";
}
@endphp</pre>
</div>
</div>
</div>
<div x-show="tab === 'before'" x-cloak class="h-[calc(100%-3rem)]">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Before</div>
@php
$highlightedBeforeFullscreen = $useTorchlight ? $highlight($policyType, (string) $fromText) : null;
@endphp
@if (is_string($highlightedBeforeFullscreen) && $highlightedBeforeFullscreen !== '')
@once
@include('filament.partials.torchlight-dark-overrides')
@endonce
<div class="mt-2 h-full overflow-auto">{!! $highlightedBeforeFullscreen !!}</div>
@else
<pre class="mt-2 h-full overflow-auto font-mono text-xs text-gray-800 dark:text-gray-200 whitespace-pre">{{ (string) $fromText }}</pre>
@endif
</div>
<div x-show="tab === 'after'" x-cloak class="h-[calc(100%-3rem)]">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">After</div>
@php
$highlightedAfterFullscreen = $useTorchlight ? $highlight($policyType, (string) $toText) : null;
@endphp
@if (is_string($highlightedAfterFullscreen) && $highlightedAfterFullscreen !== '')
@once
@include('filament.partials.torchlight-dark-overrides')
@endonce
<div class="mt-2 h-full overflow-auto">{!! $highlightedAfterFullscreen !!}</div>
@else
<pre class="mt-2 h-full overflow-auto font-mono text-xs text-gray-800 dark:text-gray-200 whitespace-pre">{{ (string) $toText }}</pre>
@endif
</div>
</div>
</div>
</div>
</div>
</details>
</div>
@else

View File

@ -22,6 +22,7 @@ ## Phase 4: Script Content Display (Safe)
- [x] T010 Hide script content behind a Show/Hide button (collapsed by default).
- [x] T011 Highlight script content in Normalized Diff view (From/To).
- [x] T012 Enable Torchlight highlighting in Diff + Before/After views.
- [x] T013 Add “Fullscreen” overlay for script diffs (scroll sync).
## Open TODOs (Follow-up)
- None yet.

View File

@ -116,6 +116,7 @@
$this->get($url.'?tab=diff')
->assertSuccessful()
->assertSeeText('Fullscreen')
->assertSeeText("- Write-Host 'one'")
->assertSeeText("+ Write-Host 'two'")
->assertSee('bg-danger-50', false)