diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index 7ad86106ae08..d0207b901158 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -143,7 +143,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ > {autoEnvironmentLabel ?? (showEnvironmentIndicator ? (activeEnvironment?.label ?? "Run on") : workspaceLabel)} @@ -274,12 +274,12 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ */ const COMPOSER_CONTEXT_MOTION_DURATION_MS = 180; const COMPOSER_CONTEXT_MOTION_EASING = "cubic-bezier(0.32, 0.72, 0, 1)"; -const COMPOSER_CONTEXT_CONTROL_SELECTOR = "[data-composer-context-control]"; +const COMPOSER_CONTEXT_LABEL_SELECTOR = "[data-composer-label]"; function useLabelsOverflow(element: HTMLDivElement | null): boolean { const [overflows, setOverflows] = useState(false); - const pendingControlRectsRef = useRef | null>(null); - const controlAnimationsRef = useRef(new Map()); + const pendingLabelRectsRef = useRef | null>(null); + const labelAnimationsRef = useRef(new Map()); // A render-synced mirror instead of useEffectEvent: the compiler memoizes // the event callback, which left observers reading the first render's null // element forever. @@ -344,15 +344,9 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { for (const inner of label.querySelectorAll("*")) { textWidth = Math.max(textWidth, inner.scrollWidth); } - if (compact) { - // Compact: the label is squeezed to zero width but keeps reporting - // the full width it would need when expanded. - needed += textWidth; - } else { - // Expanded: the label is in flow; only the clipped remainder is - // missing from the content sum. - needed += Math.max(0, textWidth - label.clientWidth); - } + // Subtract the visible width even during an animation. The content + // sum already includes it; only the hidden text needs reserving. + needed += Math.max(0, textWidth - label.getBoundingClientRect().width); } const nextOverflows = resolveContextStripLabelsCompact({ compact, @@ -360,9 +354,9 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { availableWidth: available, }); if (nextOverflows !== compact) { - pendingControlRectsRef.current = new Map( - Array.from(current.querySelectorAll(COMPOSER_CONTEXT_CONTROL_SELECTOR)).map( - (control) => [control, control.getBoundingClientRect()], + pendingLabelRectsRef.current = new Map( + Array.from(current.querySelectorAll(COMPOSER_CONTEXT_LABEL_SELECTOR)).map( + (label) => [label, label.getBoundingClientRect()], ), ); } @@ -370,28 +364,29 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { }, []); useLayoutEffect(() => { - const previousRects = pendingControlRectsRef.current; + const previousRects = pendingLabelRectsRef.current; if (!previousRects) return; - pendingControlRectsRef.current = null; + pendingLabelRectsRef.current = null; - for (const animation of controlAnimationsRef.current.values()) { + for (const animation of labelAnimationsRef.current.values()) { animation.cancel(); } - controlAnimationsRef.current.clear(); + labelAnimationsRef.current.clear(); if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; - for (const [control, previousRect] of previousRects) { - if (!control.isConnected) continue; - const nextRect = control.getBoundingClientRect(); - const deltaX = previousRect.left - nextRect.left; - const deltaY = previousRect.top - nextRect.top; - if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) continue; + for (const [label, previousRect] of previousRects) { + if (!label.isConnected) continue; + const nextWidth = label.getBoundingClientRect().width; + if (Math.abs(previousRect.width - nextWidth) < 0.5) continue; - const animation = control.animate( + // Animate the space occupied by each label so flex layout keeps the + // trailing controls anchored. Translating the whole group after its + // width snaps sends expanded text beyond the strip's right edge. + const animation = label.animate( [ - { transform: `translate3d(${deltaX}px, ${deltaY}px, 0)` }, - { transform: "translate3d(0, 0, 0)" }, + { width: `${previousRect.width}px`, maxWidth: `${previousRect.width}px` }, + { width: `${nextWidth}px`, maxWidth: `${nextWidth}px` }, ], { duration: COMPOSER_CONTEXT_MOTION_DURATION_MS, @@ -399,12 +394,12 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { fill: "backwards", }, ); - controlAnimationsRef.current.set(control, animation); + labelAnimationsRef.current.set(label, animation); animation.addEventListener( "finish", () => { - if (controlAnimationsRef.current.get(control) === animation) { - controlAnimationsRef.current.delete(control); + if (labelAnimationsRef.current.get(label) === animation) { + labelAnimationsRef.current.delete(label); } }, { once: true }, @@ -414,7 +409,7 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { useEffect( () => () => { - for (const animation of controlAnimationsRef.current.values()) { + for (const animation of labelAnimationsRef.current.values()) { animation.cancel(); } }, diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index c952fab99d6a..94697b9592fd 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -756,7 +756,7 @@ export function BranchToolbarBranchSelector({ > #{branchPr.number} @@ -784,7 +784,7 @@ export function BranchToolbarBranchSelector({ > {triggerLabel} diff --git a/apps/web/src/components/BranchToolbarEnvModeSelector.tsx b/apps/web/src/components/BranchToolbarEnvModeSelector.tsx index 611bd4234a0f..4685dbf1ecd1 100644 --- a/apps/web/src/components/BranchToolbarEnvModeSelector.tsx +++ b/apps/web/src/components/BranchToolbarEnvModeSelector.tsx @@ -66,7 +66,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe > {resolveLockedWorkspaceLabel(activeWorktreePath)} @@ -108,7 +108,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe > diff --git a/apps/web/src/components/BranchToolbarEnvironmentSelector.tsx b/apps/web/src/components/BranchToolbarEnvironmentSelector.tsx index 863cc7312fe6..861acee419e6 100644 --- a/apps/web/src/components/BranchToolbarEnvironmentSelector.tsx +++ b/apps/web/src/components/BranchToolbarEnvironmentSelector.tsx @@ -72,7 +72,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir > {activeEnvironment?.label ?? "Run on"} @@ -111,7 +111,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir >