| ... | ... | @@ -143,7 +143,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 143 | 143 | > |
| 144 | 144 | <span |
| 145 | 145 | data-composer-label-motion |
| 146 | | className="block w-full min-w-0 max-w-[240px] origin-left truncate transition-[opacity,transform] duration-180 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[compact]/composer-context:[transform:translateX(-0.25rem)_scaleX(0.95)] group-data-[compact]/composer-context:opacity-0 motion-reduce:transform-none motion-reduce:transition-opacity" |
| 146 | className="block w-full min-w-0 max-w-[240px] truncate transition-opacity duration-180 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none" |
| 147 | 147 | > |
| 148 | 148 | {autoEnvironmentLabel ?? |
| 149 | 149 | (showEnvironmentIndicator ? (activeEnvironment?.label ?? "Run on") : workspaceLabel)} |
| ... | ... | @@ -274,12 +274,12 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({ |
| 274 | 274 | */ |
| 275 | 275 | const COMPOSER_CONTEXT_MOTION_DURATION_MS = 180; |
| 276 | 276 | const COMPOSER_CONTEXT_MOTION_EASING = "cubic-bezier(0.32, 0.72, 0, 1)"; |
| 277 | | const COMPOSER_CONTEXT_CONTROL_SELECTOR = "[data-composer-context-control]"; |
| 277 | const COMPOSER_CONTEXT_LABEL_SELECTOR = "[data-composer-label]"; |
| 278 | 278 | |
| 279 | 279 | function useLabelsOverflow(element: HTMLDivElement | null): boolean { |
| 280 | 280 | const [overflows, setOverflows] = useState(false); |
| 281 | | const pendingControlRectsRef = useRef<Map<HTMLElement, DOMRect> | null>(null); |
| 282 | | const controlAnimationsRef = useRef(new Map<HTMLElement, Animation>()); |
| 281 | const pendingLabelRectsRef = useRef<Map<HTMLElement, DOMRect> | null>(null); |
| 282 | const labelAnimationsRef = useRef(new Map<HTMLElement, Animation>()); |
| 283 | 283 | // A render-synced mirror instead of useEffectEvent: the compiler memoizes |
| 284 | 284 | // the event callback, which left observers reading the first render's null |
| 285 | 285 | // element forever. |
| ... | ... | @@ -344,15 +344,9 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { |
| 344 | 344 | for (const inner of label.querySelectorAll<HTMLElement>("*")) { |
| 345 | 345 | textWidth = Math.max(textWidth, inner.scrollWidth); |
| 346 | 346 | } |
| 347 | | if (compact) { |
| 348 | | // Compact: the label is squeezed to zero width but keeps reporting |
| 349 | | // the full width it would need when expanded. |
| 350 | | needed += textWidth; |
| 351 | | } else { |
| 352 | | // Expanded: the label is in flow; only the clipped remainder is |
| 353 | | // missing from the content sum. |
| 354 | | needed += Math.max(0, textWidth - label.clientWidth); |
| 355 | | } |
| 347 | // Subtract the visible width even during an animation. The content |
| 348 | // sum already includes it; only the hidden text needs reserving. |
| 349 | needed += Math.max(0, textWidth - label.getBoundingClientRect().width); |
| 356 | 350 | } |
| 357 | 351 | const nextOverflows = resolveContextStripLabelsCompact({ |
| 358 | 352 | compact, |
| ... | ... | @@ -360,9 +354,9 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { |
| 360 | 354 | availableWidth: available, |
| 361 | 355 | }); |
| 362 | 356 | if (nextOverflows !== compact) { |
| 363 | | pendingControlRectsRef.current = new Map( |
| 364 | | Array.from(current.querySelectorAll<HTMLElement>(COMPOSER_CONTEXT_CONTROL_SELECTOR)).map( |
| 365 | | (control) => [control, control.getBoundingClientRect()], |
| 357 | pendingLabelRectsRef.current = new Map( |
| 358 | Array.from(current.querySelectorAll<HTMLElement>(COMPOSER_CONTEXT_LABEL_SELECTOR)).map( |
| 359 | (label) => [label, label.getBoundingClientRect()], |
| 366 | 360 | ), |
| 367 | 361 | ); |
| 368 | 362 | } |
| ... | ... | @@ -370,28 +364,29 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { |
| 370 | 364 | }, []); |
| 371 | 365 | |
| 372 | 366 | useLayoutEffect(() => { |
| 373 | | const previousRects = pendingControlRectsRef.current; |
| 367 | const previousRects = pendingLabelRectsRef.current; |
| 374 | 368 | if (!previousRects) return; |
| 375 | | pendingControlRectsRef.current = null; |
| 369 | pendingLabelRectsRef.current = null; |
| 376 | 370 | |
| 377 | | for (const animation of controlAnimationsRef.current.values()) { |
| 371 | for (const animation of labelAnimationsRef.current.values()) { |
| 378 | 372 | animation.cancel(); |
| 379 | 373 | } |
| 380 | | controlAnimationsRef.current.clear(); |
| 374 | labelAnimationsRef.current.clear(); |
| 381 | 375 | |
| 382 | 376 | if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; |
| 383 | 377 | |
| 384 | | for (const [control, previousRect] of previousRects) { |
| 385 | | if (!control.isConnected) continue; |
| 386 | | const nextRect = control.getBoundingClientRect(); |
| 387 | | const deltaX = previousRect.left - nextRect.left; |
| 388 | | const deltaY = previousRect.top - nextRect.top; |
| 389 | | if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) continue; |
| 378 | for (const [label, previousRect] of previousRects) { |
| 379 | if (!label.isConnected) continue; |
| 380 | const nextWidth = label.getBoundingClientRect().width; |
| 381 | if (Math.abs(previousRect.width - nextWidth) < 0.5) continue; |
| 390 | 382 | |
| 391 | | const animation = control.animate( |
| 383 | // Animate the space occupied by each label so flex layout keeps the |
| 384 | // trailing controls anchored. Translating the whole group after its |
| 385 | // width snaps sends expanded text beyond the strip's right edge. |
| 386 | const animation = label.animate( |
| 392 | 387 | [ |
| 393 | | { transform: `translate3d(${deltaX}px, ${deltaY}px, 0)` }, |
| 394 | | { transform: "translate3d(0, 0, 0)" }, |
| 388 | { width: `${previousRect.width}px`, maxWidth: `${previousRect.width}px` }, |
| 389 | { width: `${nextWidth}px`, maxWidth: `${nextWidth}px` }, |
| 395 | 390 | ], |
| 396 | 391 | { |
| 397 | 392 | duration: COMPOSER_CONTEXT_MOTION_DURATION_MS, |
| ... | ... | @@ -399,12 +394,12 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { |
| 399 | 394 | fill: "backwards", |
| 400 | 395 | }, |
| 401 | 396 | ); |
| 402 | | controlAnimationsRef.current.set(control, animation); |
| 397 | labelAnimationsRef.current.set(label, animation); |
| 403 | 398 | animation.addEventListener( |
| 404 | 399 | "finish", |
| 405 | 400 | () => { |
| 406 | | if (controlAnimationsRef.current.get(control) === animation) { |
| 407 | | controlAnimationsRef.current.delete(control); |
| 401 | if (labelAnimationsRef.current.get(label) === animation) { |
| 402 | labelAnimationsRef.current.delete(label); |
| 408 | 403 | } |
| 409 | 404 | }, |
| 410 | 405 | { once: true }, |
| ... | ... | @@ -414,7 +409,7 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean { |
| 414 | 409 | |
| 415 | 410 | useEffect( |
| 416 | 411 | () => () => { |
| 417 | | for (const animation of controlAnimationsRef.current.values()) { |
| 412 | for (const animation of labelAnimationsRef.current.values()) { |
| 418 | 413 | animation.cancel(); |
| 419 | 414 | } |
| 420 | 415 | }, |