feat: fullscreen script diff with scroll sync
This commit is contained in:
parent
2f3788372a
commit
72acc8db42
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user