| ... | ... | @@ -68,10 +68,11 @@ export function PreviewPanelShell(props: { |
| 68 | 68 | const isInline = props.mode === "inline"; |
| 69 | 69 | const collapsible = isInline && props.open !== undefined; |
| 70 | 70 | const open = props.open ?? true; |
| 71 | const maximized = props.maximized ?? false; |
| 71 | 72 | const hostRef = useRef<HTMLDivElement | null>(null); |
| 72 | 73 | // Only inline non-maximized mode applies `width`/`maxWidth`; skip the |
| 73 | 74 | // container measurement (and its re-renders) everywhere else. |
| 74 | | const maxWidth = useClampedMaxWidth(hostRef, isInline && !props.maximized); |
| 75 | const maxWidth = useClampedMaxWidth(hostRef, isInline && !maximized); |
| 75 | 76 | const { width, handlers } = useResizableWidth({ |
| 76 | 77 | storageKey: props.widthStorageKey ?? PREVIEW_PANEL_WIDTH_STORAGE_KEY, |
| 77 | 78 | defaultWidth: props.defaultWidth ?? PREVIEW_PANEL_DEFAULT_WIDTH, |
| ... | ... | @@ -79,33 +80,50 @@ export function PreviewPanelShell(props: { |
| 79 | 80 | maxWidth, |
| 80 | 81 | edge: "left", |
| 81 | 82 | }); |
| 82 | | const previousLayoutRef = useRef({ open, width }); |
| 83 | // Derive suppression before the layout commits so the browser never creates |
| 84 | // a width transition for resize or maximize changes. |
| 85 | const [layoutTransition, setLayoutTransition] = useState(() => ({ |
| 86 | open, |
| 87 | width, |
| 88 | maximized, |
| 89 | suppressed: false, |
| 90 | })); |
| 91 | if ( |
| 92 | layoutTransition.open !== open || |
| 93 | layoutTransition.width !== width || |
| 94 | layoutTransition.maximized !== maximized |
| 95 | ) { |
| 96 | setLayoutTransition({ |
| 97 | open, |
| 98 | width, |
| 99 | maximized, |
| 100 | suppressed: |
| 101 | collapsible && |
| 102 | layoutTransition.open === open && |
| 103 | (layoutTransition.width !== width || layoutTransition.maximized !== maximized), |
| 104 | }); |
| 105 | } |
| 106 | const suppressWidthTransition = layoutTransition.suppressed; |
| 83 | 107 | useLayoutEffect(() => { |
| 84 | | const previous = previousLayoutRef.current; |
| 85 | | previousLayoutRef.current = { open, width }; |
| 86 | | if (!collapsible || previous.open !== open || previous.width === width) return; |
| 87 | | const host = hostRef.current; |
| 88 | | if (!host?.closest("[data-panel-animations=true]")) return; |
| 89 | | host.style.setProperty("transition-duration", "0ms"); |
| 108 | if (!suppressWidthTransition) return; |
| 90 | 109 | let restoreFrame = 0; |
| 91 | 110 | const paintFrame = window.requestAnimationFrame(() => { |
| 92 | 111 | restoreFrame = window.requestAnimationFrame(() => { |
| 93 | | host.style.removeProperty("transition-duration"); |
| 112 | setLayoutTransition((current) => ({ ...current, suppressed: false })); |
| 94 | 113 | }); |
| 95 | 114 | }); |
| 96 | 115 | return () => { |
| 97 | 116 | window.cancelAnimationFrame(paintFrame); |
| 98 | 117 | window.cancelAnimationFrame(restoreFrame); |
| 99 | | host.style.removeProperty("transition-duration"); |
| 100 | 118 | }; |
| 101 | | }, [collapsible, open, width]); |
| 119 | }, [suppressWidthTransition]); |
| 102 | 120 | return ( |
| 103 | 121 | <div |
| 104 | 122 | ref={hostRef} |
| 105 | 123 | className={cn( |
| 106 | 124 | "relative flex h-full min-h-0 min-w-0 max-w-full flex-col self-stretch bg-background", |
| 107 | 125 | isInline |
| 108 | | ? props.maximized |
| 126 | ? maximized |
| 109 | 127 | ? "flex-1 border-l border-border" |
| 110 | 128 | : "shrink-0 border-l border-border" |
| 111 | 129 | : "w-full", |
| ... | ... | @@ -116,17 +134,20 @@ export function PreviewPanelShell(props: { |
| 116 | 134 | )} |
| 117 | 135 | style={ |
| 118 | 136 | isInline |
| 119 | | ? { width: props.maximized ? "100%" : collapsible && !open ? "0px" : `${width}px` } |
| 137 | ? { |
| 138 | width: maximized ? "100%" : collapsible && !open ? "0px" : `${width}px`, |
| 139 | transitionDuration: suppressWidthTransition ? "0ms" : undefined, |
| 140 | } |
| 120 | 141 | : undefined |
| 121 | 142 | } |
| 122 | 143 | data-preview-panel-mode={props.mode} |
| 123 | | data-preview-panel-maximized={props.maximized ? "true" : "false"} |
| 144 | data-preview-panel-maximized={maximized ? "true" : "false"} |
| 124 | 145 | > |
| 125 | | {isInline && !props.maximized ? <RightPanelResizeHandle handlers={handlers} /> : null} |
| 146 | {isInline && !maximized ? <RightPanelResizeHandle handlers={handlers} /> : null} |
| 126 | 147 | <div className={cn("h-full min-h-0 w-full", collapsible && "overflow-clip")}> |
| 127 | 148 | <div |
| 128 | 149 | className="flex h-full min-h-0 min-w-0 flex-col" |
| 129 | | style={collapsible && !props.maximized ? { width: `calc(${width}px - 1px)` } : undefined} |
| 150 | style={collapsible && !maximized ? { width: `calc(${width}px - 1px)` } : undefined} |
| 130 | 151 | > |
| 131 | 152 | {useDragRegion ? <div className="electron-drag-region h-0 w-full" aria-hidden /> : null} |
| 132 | 153 | {props.children} |