authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-09 22:32:19-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 22:32:19-03:00
log3e94afa7c26734f525a7d8a9cdb47896e8618cf8
tree3f601a34a21423508a131a4171727b67c38eff32
parentcc62349c37fa486000b89c7ecc975c1bf66ec8a4
signature Signed by PGP key B5690EEEBB952194

fix(web): composer banner actions stay inline whenever they fit (#17640)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

3 files changed, 9 insertions(+), 22 deletions(-)

apps/web/src/components/ChatView.tsx-2
...@@ -2542,7 +2542,6 @@ export default function ChatView(props: ChatViewProps) {...@@ -2542,7 +2542,6 @@ export default function ChatView(props: ChatViewProps) {
2542 return {2542 return {
2543 id: `project-clone:${projectId}`,2543 id: `project-clone:${projectId}`,
2544 variant: "info",2544 variant: "info",
2545 compact: true,
2546 priority: "activity",2545 priority: "activity",
2547 icon: <DownloadIcon />,2546 icon: <DownloadIcon />,
2548 title: `Cloning ${name}`,2547 title: `Cloning ${name}`,
...@@ -2566,7 +2565,6 @@ export default function ChatView(props: ChatViewProps) {...@@ -2566,7 +2565,6 @@ export default function ChatView(props: ChatViewProps) {
2566 return {2565 return {
2567 id: `project-clone:${projectId}`,2566 id: `project-clone:${projectId}`,
2568 variant: cancelled ? "warning" : "error",2567 variant: cancelled ? "warning" : "error",
2569 compact: true,
2570 icon: <DownloadIcon />,2568 icon: <DownloadIcon />,
2571 title: cancelled ? `Cancelled cloning ${name}` : `Failed to clone ${name}`,2569 title: cancelled ? `Cancelled cloning ${name}` : `Failed to clone ${name}`,
2572 description: cancelled ? "Retry to bring in the repository." : activeProjectClone.error,2570 description: cancelled ? "Retry to bring in the repository." : activeProjectClone.error,
apps/web/src/components/chat/ComposerBanner.tsx+4-7
...@@ -183,17 +183,15 @@ function Row({...@@ -183,17 +183,15 @@ function Row({
183 layout = "inline",183 layout = "inline",
184 ...props184 ...props
185}: useRender.ComponentProps<"div"> & {185}: useRender.ComponentProps<"div"> & {
186 layout?: "inline" | "wrap-actions" | "wrap-actions-narrow" | "approval";186 layout?: "inline" | "wrap-actions" | "approval";
187}) {187}) {
188 const rowProps = {188 const rowProps = {
189 className: cn(189 className: cn(
190 "group/banner-row grid min-h-(--composer-banner-icon-column) w-full min-w-0 grid-cols-[var(--composer-banner-icon-column)_minmax(0,1fr)_auto] items-center gap-x-1 text-start",190 "group/banner-row grid min-h-(--composer-banner-icon-column) w-full min-w-0 grid-cols-[var(--composer-banner-icon-column)_minmax(0,1fr)_auto] items-center gap-x-1 text-start",
191 "not-has-[>[data-slot=composer-banner-actions]]:grid-cols-[var(--composer-banner-icon-column)_minmax(0,1fr)]",191 "not-has-[>[data-slot=composer-banner-actions]]:grid-cols-[var(--composer-banner-icon-column)_minmax(0,1fr)]",
192 "[&:is(button)]:cursor-pointer [&:is(button)]:rounded-md [&:is(button)]:focus-visible:outline-2 [&:is(button)]:focus-visible:-outline-offset-2 [&:is(button)]:focus-visible:outline-ring",192 "[&:is(button)]:cursor-pointer [&:is(button)]:rounded-md [&:is(button)]:focus-visible:outline-2 [&:is(button)]:focus-visible:-outline-offset-2 [&:is(button)]:focus-visible:outline-ring",
193 layout === "wrap-actions" &&193 // Actions share the title row while the title fits and move below it otherwise.
194 "@max-[400px]:*:data-[slot=composer-banner-content]:min-h-(--composer-banner-icon-column)",194 layout === "wrap-actions" && "flex flex-wrap *:data-[slot=composer-banner-content]:grow",
195 layout === "wrap-actions-narrow" &&
196 "@max-[320px]:*:data-[slot=composer-banner-content]:min-h-(--composer-banner-icon-column)",
197 layout === "approval" && "items-start gap-x-2 gap-y-3",195 layout === "approval" && "items-start gap-x-2 gap-y-3",
198 className,196 className,
199 ),197 ),
...@@ -257,8 +255,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) {...@@ -257,8 +255,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) {
257 className={cn(255 className={cn(
258 "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1",256 "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1",
259 "group-data-[composer-banner-layout=approval]/banner-row:self-center group-data-[composer-banner-layout=approval]/banner-row:gap-1.5 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-start-2 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:row-start-2",257 "group-data-[composer-banner-layout=approval]/banner-row:self-center group-data-[composer-banner-layout=approval]/banner-row:gap-1.5 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-start-2 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:row-start-2",
260 "@max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:justify-end",258 "group-data-[composer-banner-layout=wrap-actions]/banner-row:ms-auto",
261 "@max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:justify-start",
262 className,259 className,
263 )}260 )}
264 {...props}261 {...props}
apps/web/src/components/chat/ComposerBannerStack.tsx+5-13
...@@ -13,7 +13,6 @@ export interface ComposerBannerStackItem {...@@ -13,7 +13,6 @@ export interface ComposerBannerStackItem {
13 readonly id: string;13 readonly id: string;
14 readonly variant: ComposerBannerVariant;14 readonly variant: ComposerBannerVariant;
15 readonly priority?: "urgent" | "activity" | "notice";15 readonly priority?: "urgent" | "activity" | "notice";
16 readonly compact?: boolean;
17 readonly icon: ReactNode;16 readonly icon: ReactNode;
18 readonly title: ReactNode;17 readonly title: ReactNode;
19 readonly description?: ReactNode;18 readonly description?: ReactNode;
...@@ -250,7 +249,7 @@ export function ComposerBannerStack({ className, items, attachedAbove }: Compose...@@ -250,7 +249,7 @@ export function ComposerBannerStack({ className, items, attachedAbove }: Compose
250}249}
251250
252/** Keep full descriptions reachable only when their inline copy is clipped. */251/** Keep full descriptions reachable only when their inline copy is clipped. */
253function NoticeDescription({ children, compact }: { children: ReactNode; compact?: boolean }) {252function NoticeDescription({ children }: { children: ReactNode }) {
254 const descriptionRef = useRef<HTMLSpanElement>(null);253 const descriptionRef = useRef<HTMLSpanElement>(null);
255 const detailsRef = useRef<HTMLButtonElement>(null);254 const detailsRef = useRef<HTMLButtonElement>(null);
256 const [showDetails, setShowDetails] = useState(false);255 const [showDetails, setShowDetails] = useState(false);
...@@ -283,13 +282,10 @@ function NoticeDescription({ children, compact }: { children: ReactNode; compact...@@ -283,13 +282,10 @@ function NoticeDescription({ children, compact }: { children: ReactNode; compact
283 }, []);282 }, []);
284283
285 return (284 return (
286 <span className={compact ? "contents" : "flex min-w-8 flex-1 items-center gap-1"}>285 <span className="contents">
287 <span286 <span
288 ref={descriptionRef}287 ref={descriptionRef}
289 className={cn(288 className="w-0 min-w-0 grow truncate text-muted-foreground @max-[400px]:sr-only"
290 "min-w-0 truncate text-muted-foreground",
291 compact && "shrink-[9999] @max-[400px]:sr-only",
292 )}
293 >289 >
294 {children}290 {children}
295 </span>291 </span>
...@@ -354,17 +350,13 @@ function ComposerBannerStackAlert({...@@ -354,17 +350,13 @@ function ComposerBannerStackAlert({
354 variant={item.variant}350 variant={item.variant}
355 density="comfortable"351 density="comfortable"
356 >352 >
357 <ComposerBanner.Row layout={item.compact ? "wrap-actions-narrow" : "wrap-actions"}>353 <ComposerBanner.Row layout="wrap-actions">
358 <ComposerBanner.Icon className="h-(--composer-banner-icon-column) self-start">354 <ComposerBanner.Icon className="h-(--composer-banner-icon-column) self-start">
359 {item.icon}355 {item.icon}
360 </ComposerBanner.Icon>356 </ComposerBanner.Icon>
361 <ComposerBanner.Content className="whitespace-nowrap">357 <ComposerBanner.Content className="whitespace-nowrap">
362 <span className="min-w-0 truncate font-medium leading-7 sm:leading-6">{item.title}</span>358 <span className="min-w-0 truncate font-medium leading-7 sm:leading-6">{item.title}</span>
363 {item.description ? (359 {item.description ? <NoticeDescription>{item.description}</NoticeDescription> : null}
364 <NoticeDescription compact={item.compact ?? false}>
365 {item.description}
366 </NoticeDescription>
367 ) : null}
368 </ComposerBanner.Content>360 </ComposerBanner.Content>
369 {item.actions || item.onDismiss ? (361 {item.actions || item.onDismiss ? (
370 <ComposerBanner.Actions>362 <ComposerBanner.Actions>