diff --git a/.changelog/next/fixed-issue-4152.md b/.changelog/next/fixed-issue-4152.md new file mode 100644 index 0000000000..d6b2eff91a --- /dev/null +++ b/.changelog/next/fixed-issue-4152.md @@ -0,0 +1 @@ +- Fullscreen process-log view no longer gets trapped inside a card on glass themes — its overlay now portals to the document body so it covers the full viewport diff --git a/client/src/components/apps/tabs/ProcessesTab.jsx b/client/src/components/apps/tabs/ProcessesTab.jsx index c75a3036bf..aa4d47b5bc 100644 --- a/client/src/components/apps/tabs/ProcessesTab.jsx +++ b/client/src/components/apps/tabs/ProcessesTab.jsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef, Fragment } from 'react'; +import { createPortal } from 'react-dom'; import { Maximize2, X } from 'lucide-react'; import * as api from '../../../services/api'; import { executeCommand } from '../../../services/api'; @@ -227,8 +228,19 @@ export default function ProcessesTab({ appId, pm2ProcessNames, filterFn }) { - {/* Fullscreen Log Modal */} - {fullscreen && expandedProcess && ( + {/* + Fullscreen Log Modal — portaled to . + + This tab renders inside an app-detail / processes-page card tree, and on + "glass" themes (`--port-backdrop-filter` non-none) `index.css` gives every + bordered/rounded `.bg-port-card` a backdrop-filter, which makes that card + the containing block for `position:fixed` descendants. Rendered inline the + overlay would be sized to the card instead of the viewport. Same escape + GalleryImagePicker / FolderPicker get via 's `usePortal`, and the + same fix MediaLightbox took in #4151 — reached through createPortal + directly because this overlay isn't a . + */} + {fullscreen && expandedProcess && createPortal(
@@ -273,7 +285,8 @@ export default function ProcessesTab({ appId, pm2ProcessNames, filterFn }) { >
-
+
, + document.body )} ); diff --git a/client/src/components/apps/tabs/ProcessesTab.test.jsx b/client/src/components/apps/tabs/ProcessesTab.test.jsx index e6d1ce7365..d05f2a2740 100644 --- a/client/src/components/apps/tabs/ProcessesTab.test.jsx +++ b/client/src/components/apps/tabs/ProcessesTab.test.jsx @@ -38,4 +38,26 @@ describe('ProcessesTab', () => { expect(useProcessLogs).toHaveBeenLastCalledWith('example-api', { lines: 500, appId: 'app-1' }); }); + + // On "glass" themes a bordered/rounded `.bg-port-card` gets a backdrop-filter, + // which makes it the containing block for `position:fixed` descendants — an + // inline overlay would be sized to the card, not the viewport. Portaling to + // is the escape, so assert the overlay leaves the component's subtree. + it('portals the fullscreen log overlay to so glass-theme cards cannot trap it', () => { + const { container } = render(); + + fireEvent.click(screen.getByRole('button', { name: 'Expand details for example-api' })); + fireEvent.click(screen.getByTitle('Fullscreen')); + + const overlay = document.body.querySelector('.fixed.inset-0'); + expect(overlay).toBeTruthy(); + // Rendered out of the tab tree entirely, directly under . + expect(container.contains(overlay)).toBe(false); + expect(overlay.parentElement).toBe(document.body); + expect(screen.getByText('Logs: example-api')).toBeTruthy(); + + // Exiting fullscreen tears the portaled overlay back down. + fireEvent.click(screen.getByRole('button', { name: 'Exit fullscreen' })); + expect(document.body.querySelector('.fixed.inset-0')).toBeNull(); + }); });