diff --git a/apps/web/src/components/PluginDetailsModal.tsx b/apps/web/src/components/PluginDetailsModal.tsx
index 306a75503..3a2a6f13c 100644
--- a/apps/web/src/components/PluginDetailsModal.tsx
+++ b/apps/web/src/components/PluginDetailsModal.tsx
@@ -19,6 +19,7 @@
// same callback wiring.
import type { InstalledPluginRecord } from '@open-design/contracts';
+import { createPortal } from 'react-dom';
import { inferPluginPreview } from './plugins-home/preview';
import { PluginScenarioDetail } from './plugin-details/PluginScenarioDetail';
import { PluginExampleDetail } from './plugin-details/PluginExampleDetail';
@@ -39,9 +40,10 @@ export function PluginDetailsModal({
isApplying,
}: Props) {
const preview = inferPluginPreview(record);
+ let detail: JSX.Element;
if (preview.kind === 'media') {
- return (
+ detail = (
);
- }
-
- if (preview.kind === 'html') {
- return (
+ } else if (preview.kind === 'html') {
+ detail = (
);
- }
-
- if (preview.kind === 'design') {
- return (
+ } else if (preview.kind === 'design') {
+ detail = (
);
+ } else {
+ detail = (
+
+ );
}
- return (
-
- );
+ if (typeof document === 'undefined') return detail;
+ return createPortal(detail, document.body);
}
diff --git a/apps/web/tests/components/PluginDetailsModal.layering.test.tsx b/apps/web/tests/components/PluginDetailsModal.layering.test.tsx
new file mode 100644
index 000000000..4294f7f16
--- /dev/null
+++ b/apps/web/tests/components/PluginDetailsModal.layering.test.tsx
@@ -0,0 +1,86 @@
+// @vitest-environment jsdom
+
+import { cleanup, render } from '@testing-library/react';
+import type { InstalledPluginRecord } from '@open-design/contracts';
+import { afterEach, describe, expect, it } from 'vitest';
+
+import { PluginDetailsModal } from '../../src/components/PluginDetailsModal';
+import { I18nProvider } from '../../src/i18n';
+
+function makePlugin(
+ id: string,
+ preview?: Record,
+): InstalledPluginRecord {
+ return {
+ id,
+ title: id,
+ version: '0.1.0',
+ sourceKind: 'bundled',
+ source: '/tmp',
+ trust: 'bundled',
+ capabilitiesGranted: [],
+ manifest: {
+ name: id,
+ version: '0.1.0',
+ title: id,
+ od: {
+ kind: 'scenario',
+ ...(preview ? { preview } : {}),
+ useCase: {
+ query: 'Generate a preview.',
+ },
+ },
+ },
+ fsPath: '/tmp',
+ installedAt: 0,
+ updatedAt: 0,
+ };
+}
+
+function renderInsideStackingContext(record: InstalledPluginRecord) {
+ const host = document.createElement('div');
+ host.className = 'composer';
+ document.body.appendChild(host);
+
+ render(
+
+
+
{}} onUse={() => {}} />
+
+ ,
+ { container: host },
+ );
+
+ return host;
+}
+
+describe('PluginDetailsModal layering', () => {
+ afterEach(() => {
+ cleanup();
+ document.body.innerHTML = '';
+ });
+
+ it('portals rich preview details to the document body so sticky chat and workspace headers cannot cover them', () => {
+ const host = renderInsideStackingContext(
+ makePlugin('video-plugin', {
+ type: 'video',
+ poster: 'https://cdn.example/poster.jpg',
+ video: 'https://cdn.example/clip.mp4',
+ }),
+ );
+
+ const backdrop = document.body.querySelector('.ds-modal-backdrop');
+ expect(backdrop).toBeTruthy();
+ expect(backdrop?.parentElement).toBe(document.body);
+ expect(host.querySelector('.ds-modal-backdrop')).toBeNull();
+ });
+
+ it('portals fallback scenario details through the same top-level layer', () => {
+ const host = renderInsideStackingContext(makePlugin('scenario-plugin'));
+
+ const backdrop = document.body.querySelector('.plugin-details-modal-backdrop');
+ expect(backdrop).toBeTruthy();
+ expect(backdrop?.parentElement).toBe(document.body);
+ expect(host.querySelector('.plugin-details-modal-backdrop')).toBeNull();
+ });
+});