| ... | @@ -68,10 +68,11 @@ export function PreviewPanelShell(props: { | ... | @@ -68,10 +68,11 @@ export function PreviewPanelShell(props: { |
| 68 | const isInline = props.mode === "inline"; | 68 | const isInline = props.mode === "inline"; |
| 69 | const collapsible = isInline && props.open !== undefined; | 69 | const collapsible = isInline && props.open !== undefined; |
| 70 | const open = props.open ?? true; | 70 | const open = props.open ?? true; |
| | 71 | const maximized = props.maximized ?? false; |
| 71 | const hostRef = useRef<HTMLDivElement | null>(null); | 72 | const hostRef = useRef<HTMLDivElement | null>(null); |
| 72 | // Only inline non-maximized mode applies `width`/`maxWidth`; skip the | 73 | // Only inline non-maximized mode applies `width`/`maxWidth`; skip the |
| 73 | // container measurement (and its re-renders) everywhere else. | 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 | const { width, handlers } = useResizableWidth({ | 76 | const { width, handlers } = useResizableWidth({ |
| 76 | storageKey: props.widthStorageKey ?? PREVIEW_PANEL_WIDTH_STORAGE_KEY, | 77 | storageKey: props.widthStorageKey ?? PREVIEW_PANEL_WIDTH_STORAGE_KEY, |
| 77 | defaultWidth: props.defaultWidth ?? PREVIEW_PANEL_DEFAULT_WIDTH, | 78 | defaultWidth: props.defaultWidth ?? PREVIEW_PANEL_DEFAULT_WIDTH, |
| ... | @@ -79,33 +80,50 @@ export function PreviewPanelShell(props: { | ... | @@ -79,33 +80,50 @@ export function PreviewPanelShell(props: { |
| 79 | maxWidth, | 80 | maxWidth, |
| 80 | edge: "left", | 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 | useLayoutEffect(() => { | 107 | useLayoutEffect(() => { |
| 84 | const previous = previousLayoutRef.current; | 108 | if (!suppressWidthTransition) return; |
| 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"); | | |
| 90 | let restoreFrame = 0; | 109 | let restoreFrame = 0; |
| 91 | const paintFrame = window.requestAnimationFrame(() => { | 110 | const paintFrame = window.requestAnimationFrame(() => { |
| 92 | restoreFrame = window.requestAnimationFrame(() => { | 111 | restoreFrame = window.requestAnimationFrame(() => { |
| 93 | host.style.removeProperty("transition-duration"); | 112 | setLayoutTransition((current) => ({ ...current, suppressed: false })); |
| 94 | }); | 113 | }); |
| 95 | }); | 114 | }); |
| 96 | return () => { | 115 | return () => { |
| 97 | window.cancelAnimationFrame(paintFrame); | 116 | window.cancelAnimationFrame(paintFrame); |
| 98 | window.cancelAnimationFrame(restoreFrame); | 117 | window.cancelAnimationFrame(restoreFrame); |
| 99 | host.style.removeProperty("transition-duration"); | | |
| 100 | }; | 118 | }; |
| 101 | }, [collapsible, open, width]); | 119 | }, [suppressWidthTransition]); |
| 102 | return ( | 120 | return ( |
| 103 | <div | 121 | <div |
| 104 | ref={hostRef} | 122 | ref={hostRef} |
| 105 | className={cn( | 123 | className={cn( |
| 106 | "relative flex h-full min-h-0 min-w-0 max-w-full flex-col self-stretch bg-background", | 124 | "relative flex h-full min-h-0 min-w-0 max-w-full flex-col self-stretch bg-background", |
| 107 | isInline | 125 | isInline |
| 108 | ? props.maximized | 126 | ? maximized |
| 109 | ? "flex-1 border-l border-border" | 127 | ? "flex-1 border-l border-border" |
| 110 | : "shrink-0 border-l border-border" | 128 | : "shrink-0 border-l border-border" |
| 111 | : "w-full", | 129 | : "w-full", |
| ... | @@ -116,17 +134,20 @@ export function PreviewPanelShell(props: { | ... | @@ -116,17 +134,20 @@ export function PreviewPanelShell(props: { |
| 116 | )} | 134 | )} |
| 117 | style={ | 135 | style={ |
| 118 | isInline | 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 | : undefined | 141 | : undefined |
| 121 | } | 142 | } |
| 122 | data-preview-panel-mode={props.mode} | 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 | <div className={cn("h-full min-h-0 w-full", collapsible && "overflow-clip")}> | 147 | <div className={cn("h-full min-h-0 w-full", collapsible && "overflow-clip")}> |
| 127 | <div | 148 | <div |
| 128 | className="flex h-full min-h-0 min-w-0 flex-col" | 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 | {useDragRegion ? <div className="electron-drag-region h-0 w-full" aria-hidden /> : null} | 152 | {useDragRegion ? <div className="electron-drag-region h-0 w-full" aria-hidden /> : null} |
| 132 | {props.children} | 153 | {props.children} |