feat(editor): opt-in diff preview before save (#855)

* feat(editor): add useComposeDiffPreviewEnabled hook

* feat(editor): add ComposeDiffPreviewDialog component

* fix(editor): replace HTML entity with Unicode arrow in ComposeDiffPreviewDialog

* feat(editor): add diff preview toggle to Appearance settings

Added a new 'Diff preview before save' toggle in the Display section of the Appearance settings panel. Users can now enable or disable the side-by-side diff view before compose and env file edits are saved to disk.

* feat(editor): wire diff preview dialog into compose save flow

* fix(editor): snapshot diff content at open time and fix event name

- Fix useComposeDiffPreviewEnabled and useDeployFeedbackEnabled to use
  the canonical SENCHO_SETTINGS_CHANGED constant from @/lib/events
  instead of the hardcoded string literal (wrong value)
- Snapshot language, original, modified, and fileName into diffPreview
  state at click time to prevent tab-switching from corrupting dialog
  content mid-review
- Remove React.MouseEvent from diffPreview state; pass a no-op stub to
  deployStack in the confirm path (preventDefault/stopPropagation are
  no-ops on an already-settled event anyway)
- Add diff-modal screenshot and document the feature in editor.mdx and
  settings.mdx

* docs(editor): add settings-toggle screenshot for diff preview feature
This commit is contained in:
Anso
2026-04-30 22:01:17 -04:00
committed by GitHub
parent 3e01daf76f
commit a25acbec7c
9 changed files with 290 additions and 5 deletions
@@ -0,0 +1,103 @@
import { Suspense } from 'react';
import { DiffEditor } from '@/lib/monacoLoader';
import { FileDiff, Loader2 } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
export interface ComposeDiffPreviewDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
stackName: string;
fileName: string;
language: 'yaml' | 'ini';
original: string;
modified: string;
actionLabel: 'Save' | 'Save & deploy';
confirming: boolean;
isDarkMode: boolean;
onConfirm: () => void | Promise<void>;
}
export function ComposeDiffPreviewDialog({
open,
onOpenChange,
stackName,
fileName,
language,
original,
modified,
actionLabel,
confirming,
isDarkMode,
onConfirm,
}: ComposeDiffPreviewDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl w-[95vw] p-0 gap-0">
<DialogHeader className="px-6 pt-6 pb-4 border-b border-glass-border">
<DialogTitle className="flex items-center gap-2">
<FileDiff className="w-4 h-4" strokeWidth={1.5} />
<span>Review changes to {stackName}</span>
<span className="font-mono tabular-nums text-xs text-stat-subtitle">{fileName}</span>
</DialogTitle>
<DialogDescription className="sr-only">
Review the diff between on-disk content and unsaved editor changes before saving.
</DialogDescription>
</DialogHeader>
<div className="px-6 pb-4 pt-3">
<div className="h-[55vh] border border-glass-border rounded-md overflow-hidden">
<Suspense fallback={<div className="w-full h-full" aria-busy="true" />}>
<DiffEditor
height="100%"
language={language}
theme={isDarkMode ? 'vs-dark' : 'vs'}
original={original}
modified={modified}
options={{
readOnly: true,
renderSideBySide: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontFamily: "'Geist Mono', monospace",
fontSize: 12,
}}
/>
</Suspense>
</div>
</div>
<DialogFooter className="px-6 py-4 border-t border-glass-border flex flex-row items-center justify-between sm:justify-between gap-4">
<span className="font-mono text-xs text-stat-subtitle">ON DISK UNSAVED</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onOpenChange(false)}
disabled={confirming}
>
Cancel
</Button>
<Button
size="sm"
onClick={() => onConfirm()}
disabled={confirming}
>
{confirming && (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" strokeWidth={1.5} />
)}
{actionLabel}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}