mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-07 01:14:14 +00:00
refactor(frontend): migrate diff dialogs to Modal chrome (D-5) (#909)
* refactor(frontend): extend Modal size system with wide variant Adds 'wide' (max-w-5xl w-[95vw]) to ModalSize for dialogs that render Monaco DiffEditor side-by-side, which need ~1024px to display both panels clearly. The existing 'xl' cap at max-w-xl (576px) is too narrow for that use case. * refactor(frontend): migrate ComposeDiffPreviewDialog to Modal chrome (D-5) Replaces raw Dialog/DialogHeader/DialogFooter with Modal size="wide", ModalHeader, and ModalFooter. Kicker carries the stack name and file type context; title is the file name (accessible dialog name for tests). Footer hint reuses the existing "ON DISK → UNSAVED" label via the hint prop. * refactor(frontend): migrate GitSourceDiffDialog to Modal chrome (D-5) Replaces Dialog + nested AlertDialog with Modal size="wide", ModalHeader, ModalFooter, and ConfirmModal. Kicker is "GIT · PULL PREVIEW"; title is the stack name. Short SHA moves to the sr-only description. The "Deploy after apply" checkbox in the footer hint slot uses normal-case and tracking-normal on the Label to prevent KICKER_CLASS uppercase/tracking from being inherited. The overwrite confirmation uses ConfirmModal with variant="destructive" (rose rail) since it replaces local file content.
This commit is contained in:
@@ -1,14 +1,7 @@
|
||||
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 { Loader2 } from 'lucide-react';
|
||||
import { Modal, ModalHeader, ModalFooter } from '@/components/ui/modal';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export interface ComposeDiffPreviewDialogProps {
|
||||
@@ -39,65 +32,60 @@ export function ComposeDiffPreviewDialog({
|
||||
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>
|
||||
<Modal size="wide" open={open} onOpenChange={onOpenChange}>
|
||||
<ModalHeader
|
||||
kicker={`${stackName} · COMPOSE DIFF`}
|
||||
title={fileName}
|
||||
description="Review the diff between on-disk content and unsaved editor changes before saving."
|
||||
/>
|
||||
|
||||
<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 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>
|
||||
<ModalFooter
|
||||
hint="ON DISK → UNSAVED"
|
||||
secondary={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={confirming}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
primary={
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onConfirm()}
|
||||
disabled={confirming}
|
||||
>
|
||||
{confirming && (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" strokeWidth={1.5} />
|
||||
)}
|
||||
{actionLabel}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useState, Suspense } from 'react';
|
||||
import { DiffEditor } from '@/lib/monacoLoader';
|
||||
import { AlertTriangle, GitBranch, Loader2 } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||
import { AlertTriangle, Loader2 } from 'lucide-react';
|
||||
import { Modal, ModalHeader, ModalFooter, ConfirmModal } from '@/components/ui/modal';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsHighlight, TabsHighlightItem } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -72,79 +71,73 @@ export function GitSourceDiffDialog({
|
||||
|
||||
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">
|
||||
<GitBranch className="w-4 h-4" strokeWidth={1.5} />
|
||||
<span>Review update for</span>
|
||||
<span className="font-mono tabular-nums">{stackName}</span>
|
||||
<span className="font-mono tabular-nums text-xs text-stat-subtitle">@{shortSha}</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Review the diff between the current on-disk stack files and the incoming Git commit.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Modal size="wide" open={open} onOpenChange={onOpenChange}>
|
||||
<ModalHeader
|
||||
kicker="GIT · PULL PREVIEW"
|
||||
title={stackName}
|
||||
description={`Incoming commit ${shortSha}. Review the diff between the current on-disk stack files and the incoming Git commit.`}
|
||||
/>
|
||||
|
||||
<div className="px-6 pt-4 space-y-3">
|
||||
{!pull.validation.ok && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" strokeWidth={1.5} />
|
||||
<div>
|
||||
<p className="font-medium">Incoming compose failed validation</p>
|
||||
<pre className="font-mono text-[11px] whitespace-pre-wrap mt-1">{pull.validation.error}</pre>
|
||||
</div>
|
||||
<div className="px-6 pt-4 space-y-3">
|
||||
{!pull.validation.ok && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" strokeWidth={1.5} />
|
||||
<div>
|
||||
<p className="font-medium">Incoming compose failed validation</p>
|
||||
<pre className="font-mono text-[11px] whitespace-pre-wrap mt-1">{pull.validation.error}</pre>
|
||||
</div>
|
||||
)}
|
||||
{pull.hasLocalChanges && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" strokeWidth={1.5} />
|
||||
<div>
|
||||
<p className="font-medium">Local edits detected on disk</p>
|
||||
<p className="mt-0.5">Applying will overwrite changes that differ from the last applied commit.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{envAvailable && (
|
||||
<Tabs value={diffTab} onValueChange={(v) => setDiffTab(v as 'compose' | 'env')}>
|
||||
<TabsList>
|
||||
<TabsHighlight className="rounded-md bg-glass-highlight" transition={springs.snappy}>
|
||||
<TabsHighlightItem value="compose">
|
||||
<TabsTrigger value="compose">compose.yaml</TabsTrigger>
|
||||
</TabsHighlightItem>
|
||||
<TabsHighlightItem value="env">
|
||||
<TabsTrigger value="env">.env</TabsTrigger>
|
||||
</TabsHighlightItem>
|
||||
</TabsHighlight>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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={effectiveTab === 'compose' ? 'yaml' : 'ini'}
|
||||
theme={isDarkMode ? 'vs-dark' : 'vs'}
|
||||
original={currentValue}
|
||||
modified={incomingValue}
|
||||
options={{
|
||||
readOnly: true,
|
||||
renderSideBySide: true,
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
fontFamily: "'Geist Mono', monospace",
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pull.hasLocalChanges && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" strokeWidth={1.5} />
|
||||
<div>
|
||||
<p className="font-medium">Local edits detected on disk</p>
|
||||
<p className="mt-0.5">Applying will overwrite changes that differ from the last applied commit.</p>
|
||||
</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">
|
||||
{envAvailable && (
|
||||
<Tabs value={diffTab} onValueChange={(v) => setDiffTab(v as 'compose' | 'env')}>
|
||||
<TabsList>
|
||||
<TabsHighlight className="rounded-md bg-glass-highlight" transition={springs.snappy}>
|
||||
<TabsHighlightItem value="compose">
|
||||
<TabsTrigger value="compose">compose.yaml</TabsTrigger>
|
||||
</TabsHighlightItem>
|
||||
<TabsHighlightItem value="env">
|
||||
<TabsTrigger value="env">.env</TabsTrigger>
|
||||
</TabsHighlightItem>
|
||||
</TabsHighlight>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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={effectiveTab === 'compose' ? 'yaml' : 'ini'}
|
||||
theme={isDarkMode ? 'vs-dark' : 'vs'}
|
||||
original={currentValue}
|
||||
modified={incomingValue}
|
||||
options={{
|
||||
readOnly: true,
|
||||
renderSideBySide: true,
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
fontFamily: "'Geist Mono', monospace",
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalFooter
|
||||
hint={
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="git-source-deploy-after"
|
||||
@@ -152,60 +145,57 @@ export function GitSourceDiffDialog({
|
||||
onCheckedChange={(checked) => setDeployAfter(checked === true)}
|
||||
disabled={applying || !pull.validation.ok}
|
||||
/>
|
||||
<Label htmlFor="git-source-deploy-after" className="text-xs cursor-pointer">
|
||||
<Label
|
||||
htmlFor="git-source-deploy-after"
|
||||
className="text-xs normal-case tracking-normal cursor-pointer"
|
||||
>
|
||||
Deploy after apply
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onDismiss()}
|
||||
disabled={applying}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleApplyClick}
|
||||
disabled={applying || !pull.validation.ok}
|
||||
>
|
||||
{applying ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" strokeWidth={1.5} />
|
||||
Applying...
|
||||
</>
|
||||
) : (
|
||||
'Apply'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Overwrite local edits?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
The on-disk stack files differ from the last applied commit. Applying this pull will replace them with the incoming content.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={applying}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
setConfirmOpen(false);
|
||||
await apply();
|
||||
}}
|
||||
}
|
||||
secondary={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onDismiss()}
|
||||
disabled={applying}
|
||||
>
|
||||
Overwrite and apply
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
Dismiss
|
||||
</Button>
|
||||
}
|
||||
primary={
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleApplyClick}
|
||||
disabled={applying || !pull.validation.ok}
|
||||
>
|
||||
{applying ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" strokeWidth={1.5} />
|
||||
Applying...
|
||||
</>
|
||||
) : (
|
||||
'Apply'
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<ConfirmModal
|
||||
open={confirmOpen}
|
||||
onOpenChange={setConfirmOpen}
|
||||
variant="destructive"
|
||||
kicker="GIT · LOCAL CHANGES"
|
||||
title="Overwrite local edits?"
|
||||
description="The on-disk stack files differ from the last applied commit. Applying this pull will replace them with the incoming content."
|
||||
confirmLabel="Overwrite and apply"
|
||||
confirming={applying}
|
||||
onConfirm={async () => {
|
||||
setConfirmOpen(false);
|
||||
await apply();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@ import {
|
||||
|
||||
const KICKER_CLASS = 'font-mono text-[10px] uppercase tracking-[0.22em]';
|
||||
|
||||
type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'wide';
|
||||
|
||||
const SIZE_CLASS: Record<ModalSize, string> = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl w-[95vw]',
|
||||
wide: 'max-w-5xl w-[95vw]',
|
||||
};
|
||||
|
||||
interface ModalProps {
|
||||
|
||||
Reference in New Issue
Block a user