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,46 @@
import { useCallback, useEffect, useState } from 'react';
import { SENCHO_SETTINGS_CHANGED } from '@/lib/events';
export const COMPOSE_DIFF_PREVIEW_KEY = 'sencho.compose-editor.diff-preview.enabled';
function readStored(): boolean {
if (typeof window === 'undefined') return false;
try {
return window.localStorage.getItem(COMPOSE_DIFF_PREVIEW_KEY) === 'true';
} catch {
return false;
}
}
export function useComposeDiffPreviewEnabled(): [boolean, (next: boolean) => void] {
const [enabled, setEnabledState] = useState<boolean>(readStored);
useEffect(() => {
function onSettingsChanged() {
setEnabledState(readStored());
}
window.addEventListener(SENCHO_SETTINGS_CHANGED, onSettingsChanged);
return () => window.removeEventListener(SENCHO_SETTINGS_CHANGED, onSettingsChanged);
}, []);
useEffect(() => {
function onStorage(event: StorageEvent) {
if (event.key !== COMPOSE_DIFF_PREVIEW_KEY) return;
setEnabledState(event.newValue === 'true');
}
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, []);
const setEnabled = useCallback((next: boolean) => {
try {
window.localStorage.setItem(COMPOSE_DIFF_PREVIEW_KEY, next ? 'true' : 'false');
} catch {
// ignore; localStorage may be unavailable (private mode, quota)
}
setEnabledState(next);
window.dispatchEvent(new CustomEvent(SENCHO_SETTINGS_CHANGED));
}, []);
return [enabled, setEnabled];
}
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { SENCHO_SETTINGS_CHANGED } from '@/lib/events';
export const DEPLOY_FEEDBACK_KEY = 'sencho.deploy-feedback.enabled';
@@ -18,8 +19,8 @@ export function useDeployFeedbackEnabled(): [boolean, (next: boolean) => void] {
function onSettingsChanged() {
setEnabledState(readStored());
}
window.addEventListener('SENCHO_SETTINGS_CHANGED', onSettingsChanged);
return () => window.removeEventListener('SENCHO_SETTINGS_CHANGED', onSettingsChanged);
window.addEventListener(SENCHO_SETTINGS_CHANGED, onSettingsChanged);
return () => window.removeEventListener(SENCHO_SETTINGS_CHANGED, onSettingsChanged);
}, []);
useEffect(() => {
@@ -38,7 +39,7 @@ export function useDeployFeedbackEnabled(): [boolean, (next: boolean) => void] {
// ignore; localStorage may be unavailable (private mode, quota)
}
setEnabledState(next);
window.dispatchEvent(new CustomEvent('SENCHO_SETTINGS_CHANGED'));
window.dispatchEvent(new CustomEvent(SENCHO_SETTINGS_CHANGED));
}, []);
return [enabled, setEnabled];