11import { useTheme } from '@/app/providers/theme-provider'
22import { Button } from '@/components/ui/button'
33import { DEFAULT_MONACO_CODE_EDITOR_OPTIONS } from '@/components/common/code-editor-defaults'
4+ import { Dialog , DialogContent } from '@/components/ui/dialog'
45import { useIsMobile } from '@/hooks/use-mobile'
56import { cn } from '@/lib/utils'
67import { Maximize2 , Minimize2 } from 'lucide-react'
78import { lazy , Suspense , useCallback , useEffect , useRef , useState , type ReactNode } from 'react'
8- import { createPortal } from 'react-dom'
99import { useTranslation } from 'react-i18next'
1010
1111const MonacoEditor = lazy ( ( ) => import ( '@/components/common/monaco-editor' ) )
@@ -26,7 +26,6 @@ export type CodeEditorPanelProps = {
2626 onDidBlur ?: ( ) => void
2727 /** Show maximize/minimize chrome (matches core-config / client-template modals). */
2828 enableFullscreen ?: boolean
29- fullscreenTitle ?: ReactNode
3029 /** Notified when fullscreen toggles (e.g. hide modal footer while expanded). */
3130 onFullscreenChange ?: ( fullscreen : boolean ) => void
3231 /**
@@ -65,7 +64,6 @@ export function CodeEditorPanel({
6564 onMount,
6665 onDidBlur,
6766 enableFullscreen = false ,
68- fullscreenTitle,
6967 onFullscreenChange,
7068 dialogOpen = true ,
7169 embeddedContainerClassName = 'h-[calc(50vh-1rem)] sm:h-[calc(55vh-1rem)] md:h-[calc(55vh-1rem)]' ,
@@ -214,52 +212,6 @@ export function CodeEditorPanel({
214212 )
215213 }
216214
217- // `PageTransition` uses `transform`, so `position: fixed` is trapped; portal to `document.body`.
218- // Avoid `items-center` on the shell: it sets `align-self: center` on children so they don't stretch,
219- // `h-full` collapses, and Monaco (`height: 100%`) renders as a thin strip.
220- const fullscreenLayer =
221- isEditorFullscreen && typeof document !== 'undefined' ? (
222- < div className = "fixed inset-0 z-[200] flex min-h-0 flex-col bg-background" dir = "ltr" >
223- < div className = "absolute inset-0 bg-background/95 backdrop-blur-sm" onClick = { handleToggleFullscreen } />
224- { ! isEditorReady && (
225- < div className = "absolute inset-0 z-[70] flex items-center justify-center bg-background/80 backdrop-blur-sm" >
226- < span className = "h-8 w-8 animate-spin rounded-full border-b-2 border-t-2 border-primary" />
227- </ div >
228- ) }
229- < div className = "relative z-10 flex min-h-0 w-full flex-1 flex-col bg-background sm:mx-auto sm:my-8 sm:h-[calc(100vh-4rem)] sm:max-w-[95vw] sm:flex-none sm:rounded-lg sm:border sm:shadow-xl" >
230- < div className = "hidden shrink-0 items-center justify-between rounded-t-lg border-b bg-background px-3 py-2.5 sm:flex" >
231- < div className = "flex items-center gap-2" >
232- { fullscreenTitle ? < span className = "text-sm font-medium" > { fullscreenTitle } </ span > : null }
233- </ div >
234- < Button
235- type = "button"
236- size = "icon"
237- variant = "ghost"
238- className = "h-8 w-8 shrink-0"
239- onClick = { handleToggleFullscreen }
240- aria-label = { t ( 'exitFullscreen' , { defaultValue : 'Exit fullscreen' } ) }
241- >
242- < Minimize2 className = "h-4 w-4" />
243- </ Button >
244- </ div >
245- < Button
246- type = "button"
247- size = "icon"
248- variant = "default"
249- className = "absolute right-2 top-2 z-20 h-9 w-9 rounded-full shadow-lg sm:hidden"
250- onClick = { handleToggleFullscreen }
251- aria-label = { t ( 'exitFullscreen' , { defaultValue : 'Exit fullscreen' } ) }
252- >
253- < Minimize2 className = "h-4 w-4" />
254- </ Button >
255- < div className = "relative min-h-0 w-full flex-1" >
256- { renderEditor ( ) }
257- </ div >
258- { footer }
259- </ div >
260- </ div >
261- ) : null
262-
263215 return (
264216 < >
265217 { ! isEditorFullscreen && (
@@ -292,7 +244,53 @@ export function CodeEditorPanel({
292244 { footer }
293245 </ div >
294246 ) }
295- { fullscreenLayer ? createPortal ( fullscreenLayer , document . body ) : null }
247+
248+ { /* Fullscreen uses a nested Radix Dialog so it gets its own focus trap,
249+ which works correctly even when this component is inside another Dialog. */ }
250+ < Dialog open = { isEditorFullscreen } onOpenChange = { open => { if ( ! open ) handleToggleFullscreen ( ) } } >
251+ < DialogContent
252+ className = "flex h-[100dvh] max-h-[100dvh] w-[100dvw] max-w-[100dvw] flex-col gap-0 rounded-none border-none p-0 sm:h-[calc(100vh-4rem)] sm:max-h-[calc(100vh-4rem)] sm:max-w-[95vw] sm:rounded-lg sm:border sm:p-0 [&>button[class*='absolute']]:hidden"
253+ dir = "ltr"
254+ onOpenAutoFocus = { e => {
255+ e . preventDefault ( )
256+ // Focus the editor after the dialog opens
257+ setTimeout ( ( ) => {
258+ if ( editorInstance ) {
259+ if ( typeof editorInstance . focus === 'function' ) editorInstance . focus ( )
260+ }
261+ } , 100 )
262+ } }
263+ onPointerDownOutside = { e => e . preventDefault ( ) }
264+ onInteractOutside = { e => e . preventDefault ( ) }
265+ >
266+ < div className = "hidden shrink-0 items-center justify-between border-b bg-background px-3 py-2.5 sm:flex sm:rounded-t-lg" >
267+ < Button
268+ type = "button"
269+ size = "icon"
270+ variant = "ghost"
271+ className = "h-8 w-8 shrink-0"
272+ onClick = { handleToggleFullscreen }
273+ aria-label = { t ( 'exitFullscreen' , { defaultValue : 'Exit fullscreen' } ) }
274+ >
275+ < Minimize2 className = "h-4 w-4" />
276+ </ Button >
277+ </ div >
278+ < Button
279+ type = "button"
280+ size = "icon"
281+ variant = "default"
282+ className = "absolute right-2 top-2 z-20 h-9 w-9 rounded-full shadow-lg sm:hidden"
283+ onClick = { handleToggleFullscreen }
284+ aria-label = { t ( 'exitFullscreen' , { defaultValue : 'Exit fullscreen' } ) }
285+ >
286+ < Minimize2 className = "h-4 w-4" />
287+ </ Button >
288+ < div className = "relative min-h-0 w-full flex-1" >
289+ { isEditorFullscreen && renderEditor ( ) }
290+ </ div >
291+ { footer }
292+ </ DialogContent >
293+ </ Dialog >
296294 </ >
297295 )
298296}
0 commit comments