feat: Refactor fullscreen editor to use React portals with maximum z-index and improved layout constraints.

This commit is contained in:
Bruno Charest 2026-03-14 12:20:23 -04:00
parent 2a14e4cea2
commit 72e408a3aa
2 changed files with 256 additions and 69 deletions

View File

@ -300,9 +300,43 @@ body {
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 99999 !important;
min-width: 100vw !important;
min-height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
z-index: 2147483647 !important; /* Maximum z-index value */
margin: 0 !important;
padding: 0 !important;
border-radius: 0 !important;
border: none !important;
background: var(--color-surface-900) !important;
transform: none !important;
box-sizing: border-box !important;
overflow: hidden !important;
}
/* Portal-based fullscreen editor - renders directly to body */
.fullscreen-editor-portal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: 2147483647;
background: var(--color-surface-900);
}
/* Ensure fullscreen content fills the space */
.fullscreen-overlay > *,
.fullscreen-editor-portal > * {
max-width: 100% !important;
width: 100% !important;
height: 100% !important;
}
.fullscreen-overlay .cm-editor,
.fullscreen-editor-portal .cm-editor {
height: 100% !important;
}

View File

@ -1,4 +1,5 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
import CodeMirror from '@uiw/react-codemirror';
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
@ -10,6 +11,38 @@ import type {
} from '../api/client';
import { api } from '../api/client';
// ═════════════════════════════════════════════════════════════════════════════════
// Fullscreen Editor Component using Portal for true fullscreen
// ═════════════════════════════════════════════════════════════════════════════════
interface FullscreenEditorProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
function FullscreenEditor({ isOpen, onClose, children }: FullscreenEditorProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
if (!isOpen) return null;
return createPortal(
<div className="fullscreen-editor-portal">
{children}
</div>,
document.body
);
}
// ═════════════════════════════════════════════════════════════════════════════════
// Tab definitions
// ═════════════════════════════════════════════════════════════════════════════════
@ -625,16 +658,14 @@ function AgentsTab() {
})}
</div>
{/* Identity File Editor */}
{activeFile?.agent === agent.name && (
<div className={`border border-glass-border bg-[#282c34] overflow-hidden shadow-inner mt-4 animate-fade-in flex flex-col ${
isFullscreen ? 'fullscreen-overlay' : 'h-[500px] rounded-xl relative'
}`}>
<div className={`flex items-center justify-between bg-surface-900/80 border-b border-glass-border shrink-0 ${isFullscreen ? 'h-16 px-6 py-4' : 'px-3 py-2'}`}>
{/* Identity File Editor - Inline (not fullscreen) */}
{activeFile?.agent === agent.name && !isFullscreen && (
<div className="border border-glass-border bg-[#282c34] overflow-hidden shadow-inner mt-4 animate-fade-in flex flex-col h-[500px] rounded-xl relative">
<div className="flex items-center justify-between bg-surface-900/80 border-b border-glass-border shrink-0 px-3 py-2">
<span className="text-xs font-mono text-purple-300 truncate pr-4">workspace/{agent.name}/{activeFile.file}</span>
<div className="flex gap-2 shrink-0">
<button onClick={() => setIsFullscreen(!isFullscreen)} className="px-2 py-1 rounded text-[10px] text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">
{isFullscreen ? '🗗 Réduire' : '⛶ Plein écran'}
<button onClick={() => setIsFullscreen(true)} className="px-2 py-1 rounded text-[10px] text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">
Plein écran
</button>
<button onClick={() => { setActiveFile(null); setIsFullscreen(false); }} className="px-2 py-1 rounded text-[10px] text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">
Fermer
@ -660,6 +691,43 @@ function AgentsTab() {
)}
</div>
)}
{/* Identity File Editor - Fullscreen via Portal */}
{activeFile?.agent === agent.name && isFullscreen && createPortal(
<div className="fullscreen-editor-portal">
<div className="h-full w-full glass-card flex flex-col overflow-hidden border border-glass-border shadow-[0_40px_80px_rgb(0,0,0,0.9)] min-h-0">
<div className="flex items-center justify-between bg-surface-900/80 border-b border-glass-border shrink-0 h-16 px-6 py-4">
<span className="text-sm font-mono text-purple-300 truncate pr-4">workspace/{agent.name}/{activeFile.file}</span>
<div className="flex gap-2 shrink-0">
<button onClick={() => setIsFullscreen(false)} className="px-3 py-1.5 rounded text-xs text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">
🗗 Réduire
</button>
<button onClick={() => { setActiveFile(null); setIsFullscreen(false); }} className="px-3 py-1.5 rounded text-xs text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">
Fermer
</button>
<button onClick={saveFile} disabled={saving} className="px-4 py-1.5 rounded bg-purple-600/80 hover:bg-purple-500 text-white text-xs font-medium transition-colors">
{saving ? '...' : 'Sauvegarder'}
</button>
</div>
</div>
{fileLoading ? (
<div className="flex-1 flex items-center justify-center text-gray-500 text-sm animate-pulse">Chargement</div>
) : (
<div className="flex-1 overflow-auto bg-[#282c34] min-h-0">
<CodeMirror
value={fileContent}
height="100%"
theme={oneDark}
extensions={[markdown()]}
onChange={(val) => setFileContent(val)}
basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: true, tabSize: 2 }}
/>
</div>
)}
</div>
</div>,
document.body
)}
</div>
)}
@ -1153,8 +1221,9 @@ function ConfigFileEditor({ onClose, onSave }: { onClose: () => void, onSave: ()
}
};
return (
<div className="fullscreen-overlay glass-card flex flex-col overflow-hidden border border-glass-border shadow-[0_40px_80px_rgb(0,0,0,0.9)] backdrop-blur-xl animate-fade-in">
return createPortal(
<div className="fullscreen-editor-portal">
<div className="h-full w-full glass-card flex flex-col overflow-hidden border border-glass-border shadow-[0_40px_80px_rgb(0,0,0,0.9)] backdrop-blur-xl animate-fade-in">
<div className="flex items-center justify-between px-6 py-4 bg-surface-800 border-b border-glass-border shrink-0 h-16">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/20 to-orange-600/10 border border-amber-500/20 flex items-center justify-center text-xl">
@ -1209,6 +1278,8 @@ function ConfigFileEditor({ onClose, onSave }: { onClose: () => void, onSave: ()
)}
</div>
</div>
</div>,
document.body
);
}
@ -1278,6 +1349,89 @@ function FilesystemTab() {
return [];
};
// Fullscreen editor via portal
if (isFullscreen && selectedFile) {
return createPortal(
<div className="fullscreen-editor-portal">
<div className="h-full w-full glass-card flex flex-col overflow-hidden border border-glass-border shadow-[0_30px_60px_rgb(0,0,0,0.9)] min-h-0">
<div className="p-4 bg-surface-800 border-b border-glass-border flex items-center justify-between z-10 shrink-0 h-16 px-6">
<div className="min-w-0 pr-4">
<h3 className="text-white font-medium flex items-center gap-2 text-base truncate">
<span className="text-xl">📄</span> {selectedFile.path.split('/').pop()}
</h3>
<div className="flex items-center gap-3 mt-1 text-[11px] text-gray-400 font-mono">
<span className="truncate max-w-[200px] md:max-w-xs" title={selectedFile.path}>{selectedFile.path}</span>
<span className="opacity-50"></span>
<span>{formatSize(selectedFile.size)}</span>
<span className="opacity-50"></span>
<span className="uppercase text-purple-300 bg-purple-500/10 px-1.5 py-0.5 rounded">{selectedFile.language}</span>
</div>
</div>
<div className="flex gap-2 shrink-0">
<button
onClick={() => setIsFullscreen(false)}
className="px-3 py-1.5 rounded-lg bg-surface-700 text-gray-300 text-sm hover:bg-surface-600 transition-colors flex items-center justify-center font-bold"
title="Réduire"
>
🗗
</button>
{!selectedFile.binary && (
<>
{editMode ? (
<>
<button onClick={() => setEditMode(false)} disabled={saving} className="px-3.5 py-1.5 rounded-lg bg-surface-700 text-gray-300 text-sm hover:bg-surface-600 transition-colors">
Annuler
</button>
<button onClick={handleSave} disabled={saving} className={`px-4 py-1.5 rounded-lg bg-gradient-to-r from-emerald-600 to-teal-600 text-white text-sm font-medium hover:from-emerald-500 hover:to-teal-500 transition-all shadow-[0_0_12px_rgba(16,185,129,0.3)] flex items-center gap-2 ${saving ? 'opacity-50 cursor-not-allowed scale-95' : 'hover:scale-105'}`}>
{saving ? 'Sauvegarde…' : '💾 Sauvegarder'}
</button>
</>
) : (
<button onClick={() => { setEditMode(true); setEditContent(selectedFile.pretty || selectedFile.content || ''); }} className="px-4 py-1.5 rounded-lg bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-sm font-medium hover:from-purple-500 hover:to-indigo-500 transition-all shadow-[0_0_12px_rgba(147,51,234,0.3)] hover:scale-105 flex items-center gap-2">
<span></span> Modifier
</button>
)}
</>
)}
</div>
</div>
<div className="flex-1 overflow-auto bg-[#282c34] min-h-0">
{selectedFile.binary ? (
<div className="flex items-center justify-center text-gray-500 bg-surface-900/60 h-full">
<div className="text-center">
<div className="text-5xl mb-3 opacity-30">📦</div>
<p className="tracking-wide">Fichier binaire. Aperçu non disponible.</p>
</div>
</div>
) : editMode ? (
<CodeMirror
value={editContent}
height="100%"
theme={oneDark}
extensions={getExtensions()}
onChange={(val) => setEditContent(val)}
basicSetup={{
lineNumbers: true,
foldGutter: true,
highlightActiveLine: true,
tabSize: 2,
}}
/>
) : (
<div className="p-4">
<pre className="text-[13px] text-[#abb2bf] font-mono whitespace-pre-wrap leading-relaxed">
{selectedFile.pretty || selectedFile.content}
</pre>
</div>
)}
</div>
</div>
</div>,
document.body
);
}
return (
<div className="flex gap-6 h-[calc(100vh-250px)] min-h-[600px]">
{/* Sidebar Tree */}
@ -1303,11 +1457,9 @@ function FilesystemTab() {
</div>
</div>
{/* Editor Panel */}
<div className={isFullscreen
? "fullscreen-overlay glass-card flex flex-col overflow-hidden border border-glass-border shadow-[0_30px_60px_rgb(0,0,0,0.9)] min-h-0"
: "glass-card w-2/3 flex flex-col overflow-hidden border border-glass-border shadow-[0_8px_30px_rgb(0,0,0,0.5)] min-h-0"
}>
{/* Editor Panel - hidden when in fullscreen */}
{!isFullscreen && (
<div className="glass-card w-2/3 flex flex-col overflow-hidden border border-glass-border shadow-[0_8px_30px_rgb(0,0,0,0.5)] min-h-0">
{fileLoading ? (
<div className="flex-1 flex items-center justify-center bg-surface-900/40">
<div className="text-gray-400 flex flex-col items-center gap-3">
@ -1323,7 +1475,7 @@ function FilesystemTab() {
) : (
<div className="flex flex-col h-full">
{/* Editor Header */}
<div className={`p-4 bg-surface-800 border-b border-glass-border flex items-center justify-between z-10 shrink-0 ${isFullscreen ? 'h-16 px-6' : ''}`}>
<div className="p-4 bg-surface-800 border-b border-glass-border flex items-center justify-between z-10 shrink-0">
<div className="min-w-0 pr-4">
<h3 className="text-white font-medium flex items-center gap-2 text-base truncate">
<span className="text-xl">📄</span> {selectedFile.path.split('/').pop()}
@ -1338,11 +1490,11 @@ function FilesystemTab() {
</div>
<div className="flex gap-2 shrink-0">
<button
onClick={() => setIsFullscreen(!isFullscreen)}
onClick={() => setIsFullscreen(true)}
className="px-3 py-1.5 rounded-lg bg-surface-700 text-gray-300 text-sm hover:bg-surface-600 transition-colors flex items-center justify-center font-bold"
title={isFullscreen ? "Réduire" : "Plein écran"}
title="Plein écran"
>
{isFullscreen ? "🗗" : "⛶"}
</button>
{!selectedFile.binary && (
<>
@ -1399,6 +1551,7 @@ function FilesystemTab() {
</div>
)}
</div>
)}
</div>
);
}