Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 28 additions & 33 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
>
<span
data-composer-label-motion
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"
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"
>
{autoEnvironmentLabel ??
(showEnvironmentIndicator ? (activeEnvironment?.label ?? "Run on") : workspaceLabel)}
Expand Down Expand Up @@ -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<Map<HTMLElement, DOMRect> | null>(null);
const controlAnimationsRef = useRef(new Map<HTMLElement, Animation>());
const pendingLabelRectsRef = useRef<Map<HTMLElement, DOMRect> | null>(null);
const labelAnimationsRef = useRef(new Map<HTMLElement, Animation>());
// A render-synced mirror instead of useEffectEvent: the compiler memoizes
// the event callback, which left observers reading the first render's null
// element forever.
Expand Down Expand Up @@ -344,67 +344,62 @@ function useLabelsOverflow(element: HTMLDivElement | null): boolean {
for (const inner of label.querySelectorAll<HTMLElement>("*")) {
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,
neededWidth: needed,
availableWidth: available,
});
if (nextOverflows !== compact) {
pendingControlRectsRef.current = new Map(
Array.from(current.querySelectorAll<HTMLElement>(COMPOSER_CONTEXT_CONTROL_SELECTOR)).map(
(control) => [control, control.getBoundingClientRect()],
pendingLabelRectsRef.current = new Map(
Array.from(current.querySelectorAll<HTMLElement>(COMPOSER_CONTEXT_LABEL_SELECTOR)).map(
(label) => [label, label.getBoundingClientRect()],
),
);
}
setOverflows(nextOverflows);
}, []);

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,
easing: COMPOSER_CONTEXT_MOTION_EASING,
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 },
Expand All @@ -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();
}
},
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@ export function BranchToolbarBranchSelector({
>
<span
data-composer-label-motion
className="block w-full min-w-0 max-w-12 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"
className="block w-full min-w-0 max-w-12 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"
>
#{branchPr.number}
</span>
Expand Down Expand Up @@ -784,7 +784,7 @@ export function BranchToolbarBranchSelector({
>
<span
data-composer-label-motion
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"
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"
>
{triggerLabel}
</span>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/BranchToolbarEnvModeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe
>
<span
data-composer-label-motion
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"
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"
>
{resolveLockedWorkspaceLabel(activeWorktreePath)}
</span>
Expand Down Expand Up @@ -108,7 +108,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe
>
<span
data-composer-label-motion
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"
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"
>
<SelectValue />
</span>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/BranchToolbarEnvironmentSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir
>
<span
data-composer-label-motion
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"
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"
>
{activeEnvironment?.label ?? "Run on"}
</span>
Expand Down Expand Up @@ -111,7 +111,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir
>
<span
data-composer-label-motion
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"
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"
>
<SelectValue />
</span>
Expand Down
Loading