Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@radix-ui/react-radio-group": "^1.4.2",
"@radix-ui/react-select": "^2.3.1",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-switch": "^1.3.3",
"@radix-ui/react-tabs": "^1.1.17",
"@radix-ui/react-toast": "^1.2.18",
"@tanstack/react-query": "^5.101.1",
Expand Down
31 changes: 31 additions & 0 deletions frontend/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 95 additions & 16 deletions frontend/web/src/components/ui/edi-editor-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,26 @@ export interface EdiEditorPaneProps {
cornerPlaceholder?: string;
className?: string;
acceptedFileExtensions?: string;
extraActions?: React.ReactNode;
fontSize?: number;
showCertDetected?: boolean;
/** When false, suppresses the big "Drag and drop" heading. Buttons are always shown. */
showEmptyState?: boolean;
}

export function EdiEditorPane({ value, onChange, language = 'edi', placeholder, cornerPlaceholder, className = '', acceptedFileExtensions = ".edi,.json,.txt,.x12" }: EdiEditorPaneProps) {
export function EdiEditorPane({
value,
onChange,
language = 'edi',
placeholder,
cornerPlaceholder,
className = '',
acceptedFileExtensions = '.edi,.json,.txt,.x12',
extraActions,
fontSize = 13,
showCertDetected = false,
showEmptyState = true,
}: EdiEditorPaneProps) {
const [isDragging, setIsDragging] = useState(false);
const [error, setError] = useState<string | null>(null);

Expand Down Expand Up @@ -60,13 +77,43 @@ export function EdiEditorPane({ value, onChange, language = 'edi', placeholder,
registerEdiLanguageAndTheme(monaco);
};

// Monaco's content area intercepts drag events, so we attach listeners
// directly to its DOM node to ensure drag-and-drop always works.
const handleEditorMount = (editor: any) => {
const domNode = editor.getDomNode() as HTMLElement | null;
if (!domNode) return;

const onDragOver = (e: DragEvent) => { e.preventDefault(); setIsDragging(true); };
const onDragLeave = (e: DragEvent) => { e.preventDefault(); setIsDragging(false); };
const onDrop = async (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
await applyFile(e.dataTransfer.files[0]);
}
};

domNode.addEventListener('dragover', onDragOver);
domNode.addEventListener('dragleave', onDragLeave);
domNode.addEventListener('drop', onDrop);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Cleanup handled via Monaco's own disposal lifecycle
editor.onDidDispose(() => {
domNode.removeEventListener('dragover', onDragOver);
domNode.removeEventListener('dragleave', onDragLeave);
domNode.removeEventListener('drop', onDrop);
});
};

return (
<div
className={`flex-1 p-0 bg-white min-h-[250px] flex flex-col relative group ${className}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* Error banner */}
{error && (
<div className="absolute top-2 right-2 left-2 z-30 p-3 bg-red-50 border border-red-200 text-red-700 text-sm rounded-md shadow-sm flex items-center justify-between">
<span>{error}</span>
Expand All @@ -75,43 +122,59 @@ export function EdiEditorPane({ value, onChange, language = 'edi', placeholder,
</button>
</div>
)}

{/* Certificate detected — bottom status bar (when value present) */}
{!error && value && showCertDetected && value.includes('-----BEGIN CERTIFICATE-----') && value.includes('-----END CERTIFICATE-----') && (
<div className="absolute bottom-0 left-0 right-0 z-30 px-4 py-2 bg-green-50 border-t border-green-200 text-green-700 text-xs font-semibold flex items-center gap-2 pointer-events-none">
<svg className="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Certificate detected — ready to save
</div>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

{/* Drag-over full-screen overlay */}
{isDragging && (
<div className="absolute inset-0 bg-indigo-50/90 z-20 flex flex-col items-center justify-center border-2 border-indigo-400 border-dashed m-2 rounded-lg backdrop-blur-sm transition-all duration-200">
<div className="absolute inset-0 bg-indigo-50/90 z-20 flex flex-col items-center justify-center border-2 border-indigo-400 border-dashed m-2 rounded-lg backdrop-blur-sm transition-all duration-200 pointer-events-none">
<UploadCloud className="w-10 h-10 text-indigo-500 mb-3 animate-bounce" />
<span className="text-indigo-700 font-semibold text-lg tracking-tight">Drop file to load</span>
</div>
)}
{!value && !isDragging && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center p-6 text-center pointer-events-none">
{cornerPlaceholder && (
<div className="absolute top-4 left-6 text-xs text-slate-400 font-mono text-left whitespace-pre-wrap pointer-events-none opacity-90">
{cornerPlaceholder}
</div>
)}
<UploadCloud className="w-12 h-12 text-slate-200 mb-4" />
<h3 className="text-lg font-medium text-slate-900 mb-1">Drag and drop your file here</h3>
<p className="text-sm text-slate-500 mb-6">{placeholder || `or upload a file, or click anywhere to paste raw ${language.toUpperCase()}`}</p>
<label className="pointer-events-auto cursor-pointer inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 transition-colors">
Upload File
<input type="file" className="hidden" accept={acceptedFileExtensions} onChange={handleFileUpload} />
</label>

{/* Corner placeholder (e.g. PEM hint) */}
{!value && !isDragging && cornerPlaceholder && (
<div className="absolute top-4 left-6 z-10 text-xs text-slate-400 font-mono text-left whitespace-pre-wrap pointer-events-none opacity-90">
{cornerPlaceholder}
</div>
)}

{/* Optional "Drag and drop" heading for the EDI editor */}
{showEmptyState && !value && !isDragging && (
<div className="absolute inset-x-0 top-1/3 z-10 flex flex-col items-center gap-1 pointer-events-none text-center px-6">
<UploadCloud className="w-10 h-10 text-slate-150 mb-1" />
<p className="text-sm font-medium text-slate-400">
{placeholder || `Drag & drop or paste ${language.toUpperCase()}`}
</p>
</div>
)}

{/* Monaco editor — always rendered */}
<Editor
height="100%"
language={language}
value={value}
onChange={(val) => onChange(val || '')}
theme="soopa-theme"
beforeMount={handleEditorWillMount}
onMount={handleEditorMount}
options={{
automaticLayout: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
lineNumbersMinChars: 3,
wordWrap: 'on',
folding: true,
fontSize,
padding: { top: 16, bottom: 16 },
renderLineHighlight: 'none',
hideCursorInOverviewRuler: true,
Expand All @@ -122,6 +185,22 @@ export function EdiEditorPane({ value, onChange, language = 'edi', placeholder,
},
}}
/>

{/* Bottom toolbar — always shown when editor is empty, sits below Monaco */}
{!value && !isDragging && (
<div className="absolute bottom-0 left-0 right-0 z-10 flex items-center justify-center gap-3 px-4 py-2.5 bg-white/90 backdrop-blur-sm border-t border-slate-100">
<span className="text-xs text-slate-400 flex items-center gap-1">
<UploadCloud className="w-3 h-3" />
Drag &amp; drop or
</span>
<label className="cursor-pointer inline-flex items-center justify-center px-3 py-1.5 border border-transparent text-xs font-semibold rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 transition-colors">
<UploadCloud className="w-3.5 h-3.5 mr-1.5" />
Upload File
<input type="file" className="hidden" accept={acceptedFileExtensions} onChange={handleFileUpload} />
</label>
{extraActions}
</div>
)}
</div>
);
}
27 changes: 27 additions & 0 deletions frontend/web/src/components/ui/switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"

import { cn } from "@/lib/utils"

const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName

export { Switch }
Loading
Loading