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