authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-10-13 10:34:46-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-13 12:34:46-05:00
log6a68bee34f5ecfd478b6e2bd0af2ce39c0c32583
tree28345b4ef30acd42e802a15d5f3bd7a7e21b47c6
parent05e4f888a0d8591c790d036140080092cf024d30
signature Signed by PGP key B5690EEEBB952194

fix(web): merge button fits its label and animates width changes (#18478)


2 files changed, 53 insertions(+), 14 deletions(-)

apps/web/src/components/AnimatedWidth.tsx created+39
...@@ -0,0 +1,39 @@
1import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
2
3import { observeResize } from "~/lib/observeResize";
4import { cn } from "~/lib/utils";
5
6export function AnimatedWidth({
7 children,
8 className,
9}: {
10 readonly children: ReactNode;
11 readonly className?: string;
12}) {
13 const contentRef = useRef<HTMLSpanElement>(null);
14 const [width, setWidth] = useState<number | null>(null);
15
16 useLayoutEffect(() => {
17 const element = contentRef.current;
18 if (!element) return;
19 return observeResize(element, () => {
20 const nextWidth = Math.ceil(element.getBoundingClientRect().width);
21 if (nextWidth > 0) setWidth(nextWidth);
22 });
23 }, []);
24
25 return (
26 <span
27 data-slot="animated-width"
28 className={cn(
29 "inline-block overflow-hidden transition-[width] duration-150 ease-out motion-reduce:transition-none",
30 className,
31 )}
32 style={width === null ? undefined : { width }}
33 >
34 <span ref={contentRef} className="block w-max whitespace-nowrap">
35 {children}
36 </span>
37 </span>
38 );
39}
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+14-14
...@@ -2,6 +2,7 @@ import { parseChangeRequestUrl } from "@t3tools/shared/changeRequestUrl";...@@ -2,6 +2,7 @@ import { parseChangeRequestUrl } from "@t3tools/shared/changeRequestUrl";
2import { useAtomValue } from "@effect/atom-react";2import { useAtomValue } from "@effect/atom-react";
3import { usePullRequestStack } from "~/state/usePullRequestStack";3import { usePullRequestStack } from "~/state/usePullRequestStack";
4import { RefreshIcon } from "~/components/ui/refresh-icon";4import { RefreshIcon } from "~/components/ui/refresh-icon";
5import { AnimatedWidth } from "~/components/AnimatedWidth";
5import {6import {
6 scopedThreadKey,7 scopedThreadKey,
7 scopeProjectRef,8 scopeProjectRef,
...@@ -2122,21 +2123,20 @@ export function PullRequestDetailPanel({...@@ -2122,21 +2123,20 @@ export function PullRequestDetailPanel({
2122 ) : (2123 ) : (
2123 <PullRequestGlyph.merged aria-hidden className="size-3.5" />2124 <PullRequestGlyph.merged aria-hidden className="size-3.5" />
2124 )}2125 )}
2125 <span className="inline-grid @max-[30rem]/pr-header:hidden">2126 <AnimatedWidth className="@max-[30rem]/pr-header:hidden">
2126 <span aria-hidden className="invisible col-start-1 row-start-1">2127 <span className="grid">
2127 {selectedMergeMethodLabel}2128 <span aria-hidden className="invisible col-start-1 row-start-1">
2128 </span>2129 {selectedMergeMethodLabel}
2129 <span aria-hidden className="invisible col-start-1 row-start-1">2130 </span>
2130 Confirm merge?2131 <span className="col-start-1 row-start-1">
2132 {pendingAction === "merge"
2133 ? "Merging..."
2134 : inlineConfirm.isArmed("merge")
2135 ? "Confirm merge?"
2136 : selectedMergeMethodLabel}
2137 </span>
2131 </span>2138 </span>
2132 <span className="col-start-1 row-start-1">2139 </AnimatedWidth>
2133 {pendingAction === "merge"
2134 ? "Merging..."
2135 : inlineConfirm.isArmed("merge")
2136 ? "Confirm merge?"
2137 : selectedMergeMethodLabel}
2138 </span>
2139 </span>
2140 </Button>2140 </Button>
2141 </span>2141 </span>
2142 }2142 }