diff --git a/src/workspaces/officerdev/src/components/Workspace/PanelSlot.tsx b/src/workspaces/officerdev/src/components/Workspace/PanelSlot.tsx index a3001b38..7ff27161 100644 --- a/src/workspaces/officerdev/src/components/Workspace/PanelSlot.tsx +++ b/src/workspaces/officerdev/src/components/Workspace/PanelSlot.tsx @@ -1,6 +1,5 @@ import type { ComponentType } from 'react'; import { useCallback } from 'react'; -import { createPortal } from 'react-dom'; import { ArrowLeftRight, ChevronLeft, X, Minus } from 'lucide-react'; import type { LayoutPanel, AppRegistryMap, PanelComponents, PanelComponentEntry } from './types'; import { useWorkspace } from './WorkspaceContext'; @@ -40,7 +39,15 @@ type PanelContextMenuProps = { const isPanelEntry = (v: ComponentType | PanelComponentEntry): v is PanelComponentEntry => typeof v === 'object' && v !== null && 'component' in v; -const PanelContextMenu = ({ panelId, hasApp, isLastPanel, onSplit, onRemove, onClearApp, children }: PanelContextMenuProps) => { +const PanelContextMenu = ({ + panelId, + hasApp, + isLastPanel, + onSplit, + onRemove, + onClearApp, + children, +}: PanelContextMenuProps) => { const { swapSourceId, setSwapSourceId, maximizedPanelId, setMaximizedPanelId } = useWorkspace(); const isMaximized = maximizedPanelId === panelId; @@ -110,7 +117,17 @@ const SwapSourceIndicator = ({ panelId }: { panelId: string }) => { ); }; -const TrafficLights = ({ panelId, isLastPanel, onRemove, onClearApp }: { panelId: string; isLastPanel: boolean; onRemove: (panelId: string) => void; onClearApp: () => void }) => { +const TrafficLights = ({ + panelId, + isLastPanel, + onRemove, + onClearApp, +}: { + panelId: string; + isLastPanel: boolean; + onRemove: (panelId: string) => void; + onClearApp: () => void; +}) => { const { maximizedPanelId, setMaximizedPanelId } = useWorkspace(); const isMaximized = maximizedPanelId === panelId; @@ -135,7 +152,10 @@ const TrafficLights = ({ panelId, isLastPanel, onRemove, onClearApp }: { panelId className="group/btn h-3 w-3 rounded-full bg-[#febc2e] hover:brightness-90 transition-all cursor-pointer flex items-center justify-center" title="Restore" > - + ); @@ -149,7 +169,10 @@ const TrafficLights = ({ panelId, isLastPanel, onRemove, onClearApp }: { panelId className="group/btn h-3 w-3 rounded-full bg-[#ff5f57] hover:brightness-90 transition-all cursor-pointer flex items-center justify-center" title={isLastPanel ? 'Clear app' : 'Close panel'} > - + @@ -178,9 +204,15 @@ const MaximizeButton = ({ panelId }: { panelId: string }) => { title={isMaximized ? 'Restore' : 'Maximize'} > {isMaximized ? ( - + ) : ( - + )} @@ -223,7 +255,18 @@ const MaximizeContextMenu = ({ panelId, children }: { panelId: string; children: // return
; // }; -export const PanelSlot = ({ panel, registry, components, interactive, locked, noHeader, isLastPanel, onSetApp, onSplit, onRemove }: PanelSlotProps) => { +export const PanelSlot = ({ + panel, + registry, + components, + interactive, + locked, + noHeader, + isLastPanel, + onSetApp, + onSplit, + onRemove, +}: PanelSlotProps) => { const { maximizedPanelId, transitioningPanelId, isMobile, onMobileBack } = useWorkspace(); const isMaximized = maximizedPanelId === panel.id; @@ -241,22 +284,28 @@ export const PanelSlot = ({ panel, registry, components, interactive, locked, no const contextMenu = interactive ? locked - ? (content: React.ReactNode) => ( - {content} - ) + ? (content: React.ReactNode) => {content} : (content: React.ReactNode) => ( - onSetApp(panel.id, null)}> + onSetApp(panel.id, null)} + > {content} ) : (content: React.ReactNode) => <>{content}; - const overlays = interactive && !locked ? ( - <> - - - - ) : null; + const overlays = + interactive && !locked ? ( + <> + + + + ) : null; if (!AppComponent) { if (!interactive || locked) { @@ -289,43 +338,49 @@ export const PanelSlot = ({ panel, registry, components, interactive, locked, no } // All apps get a chrome header — custom HeaderComponent or default from registry icon+name - const DefaultHeader = entry ? () => ( - <> - - {entry.name} - - ) : null; + const DefaultHeader = entry + ? () => ( + <> + + {entry.name} + + ) + : null; const ResolvedHeader = HeaderComponent ?? DefaultHeader; - const trafficLights = interactive && !isMobile ? ( - locked ? ( - - ) : ( - onSetApp(panel.id, null)} /> - ) - ) : null; + const trafficLights = + interactive && !isMobile ? ( + locked ? ( + + ) : ( + onSetApp(panel.id, null)} + /> + ) + ) : null; - const mobileBackButton = isMobile && onMobileBack ? ( - - ) : null; + const mobileBackButton = + isMobile && onMobileBack ? ( + + ) : null; const headerContent = (
{mobileBackButton} {ResolvedHeader && } {!mobileBackButton && onClose && ( - )} @@ -337,11 +392,20 @@ export const PanelSlot = ({ panel, registry, components, interactive, locked, no locked ? ( {headerContent} ) : ( - onSetApp(panel.id, null)}> + onSetApp(panel.id, null)} + > {headerContent} ) - ) : headerContent; + ) : ( + headerContent + ); const body = (
@@ -363,38 +427,31 @@ export const PanelSlot = ({ panel, registry, components, interactive, locked, no ); - if (isMaximized) { - return ( - <> - {/* Placeholder to preserve layout space */} -
-
-
- {/* Maximized overlay — portalled to escape stacking contexts */} - {createPortal( -
-
- {inner} -
-
, - document.body, - )} - - ); - } - - const panelStyle = noHeader - ? { backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(0, 0, 0, 0.25)', ...(transitioningPanelId === panel.id ? { viewTransitionName: `panel-${panel.id}` } : {}) } - : { backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(255, 255, 255, 0.2)', ...(transitioningPanelId === panel.id ? { viewTransitionName: `panel-${panel.id}` } : {}) }; + // Maximize is a CSS state toggle on THE SAME element — no portal, no remount — so the panel's content + // (scroll position, media playback, in-flight state) is preserved exactly across maximize/restore. + // It fills the content REGION (starting below the global Header), not the whole viewport: the panel is + // confined to the layout's `absolute z-2` content stacking context, which sits under the fixed nav + // Header (z-10) — so a full-viewport overlay would hide the panel's own header/Restore behind the nav. + // Clearing the header keeps it visible + the nav accessible. (Maximize only happens in the dashboard + // shell; previews/mobile no-op it, so the header offset is always the right reference.) + const maximizedStyle = { backgroundColor: 'rgba(30, 30, 30, 0.95)', borderColor: 'rgba(255, 255, 255, 0.2)' }; + const normalStyle = noHeader + ? { backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(0, 0, 0, 0.25)' } + : { backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(255, 255, 255, 0.2)' }; + const panelStyle = { + ...(isMaximized ? maximizedStyle : normalStyle), + ...(transitioningPanelId === panel.id ? { viewTransitionName: `panel-${panel.id}` } : {}), + } as React.CSSProperties; return (
{inner}