authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-09-17 21:42:22+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 15:42:22-03:00
log4749035bda13b4b6260499caedbc0d69a2f60e6f
tree2cd697819f084feb7bb33c886721500879cdf6fc
parent03950089ffa5cecf0ce731227f58551f6437495f
signature Signed by PGP key B5690EEEBB952194

fix(web): keep composer banners compact and readable (#12166)


5 files changed, 87 insertions(+), 76 deletions(-)

apps/web/src/components/ChatView.tsx+2
......@@ -2201,6 +2201,7 @@ export default function ChatView(props: ChatViewProps) {
22012201 return {
22022202 id: `project-clone:${projectId}`,
22032203 variant: "info",
2204 compact: true,
22042205 priority: "activity",
22052206 icon: <DownloadIcon />,
22062207 title: `Cloning ${name}`,
......@@ -2224,6 +2225,7 @@ export default function ChatView(props: ChatViewProps) {
22242225 return {
22252226 id: `project-clone:${projectId}`,
22262227 variant: cancelled ? "warning" : "error",
2228 compact: true,
22272229 icon: <DownloadIcon />,
22282230 title: cancelled ? `Cancelled cloning ${name}` : `Failed to clone ${name}`,
22292231 description: cancelled ? "Retry to bring in the repository." : activeProjectClone.error,
apps/web/src/components/chat/ComposerBanner.tsx+1-1
......@@ -257,7 +257,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) {
257257 className={cn(
258258 "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1",
259259 "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)]:-ms-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:justify-start",
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",
261261 "@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",
262262 className,
263263 )}
apps/web/src/components/chat/ComposerBannerStack.tsx+20-15
......@@ -13,6 +13,7 @@ export interface ComposerBannerStackItem {
1313 readonly id: string;
1414 readonly variant: ComposerBannerVariant;
1515 readonly priority?: "urgent" | "activity" | "notice";
16 readonly compact?: boolean;
1617 readonly icon: ReactNode;
1718 readonly title: ReactNode;
1819 readonly description?: ReactNode;
......@@ -270,22 +271,20 @@ function ComposerBannerStackAlert({
270271 variant={item.variant}
271272 density="comfortable"
272273 >
273 <ComposerBanner.Row layout="wrap-actions-narrow">
274 <ComposerBanner.Row layout={item.compact ? "wrap-actions-narrow" : "wrap-actions"}>
274275 <ComposerBanner.Icon className="h-(--composer-banner-icon-column) self-start">
275276 {item.icon}
276277 </ComposerBanner.Icon>
277278 <ComposerBanner.Content className="whitespace-nowrap">
278 <span
279 className={cn(
280 "min-w-0 font-medium leading-7 sm:leading-6",
281 typeof item.title === "string" && "truncate",
282 )}
283 >
284 {item.title}
285 </span>
279 <span className="min-w-0 truncate font-medium leading-7 sm:leading-6">{item.title}</span>
286280 {item.description ? (
287 <>
288 <span className="min-w-0 shrink-[9999] truncate text-muted-foreground @max-[400px]:sr-only">
281 <span className={item.compact ? "contents" : "flex min-w-8 flex-1 items-center gap-1"}>
282 <span
283 className={cn(
284 "min-w-0 truncate text-muted-foreground",
285 item.compact && "shrink-[9999] @max-[400px]:sr-only",
286 )}
287 >
289288 {item.description}
290289 </span>
291290 <Popover>
......@@ -296,21 +295,27 @@ function ComposerBannerStackAlert({
296295 size="icon-xs"
297296 variant="ghost"
298297 aria-label="Show notice details"
299 className="hidden flex-none text-muted-foreground hover:text-foreground @max-[400px]:inline-flex"
298 className={cn(
299 "flex-none text-muted-foreground hover:text-foreground",
300 item.compact && "hidden @max-[400px]:inline-flex",
301 )}
300302 />
301303 }
302304 >
303305 <InfoIcon className="size-3.5" />
304306 </PopoverTrigger>
305307 <PopoverPopup
308 aria-label="Notice details"
306309 tooltipStyle
307310 side="top"
308 className="max-w-72 whitespace-normal text-pretty"
311 className="max-w-80 whitespace-normal text-pretty wrap-anywhere"
309312 >
310 {item.description}
313 <ComposerBanner.Scroll className="max-h-[min(var(--available-height),24rem,40dvh)]">
314 {item.description}
315 </ComposerBanner.Scroll>
311316 </PopoverPopup>
312317 </Popover>
313 </>
318 </span>
314319 ) : null}
315320 </ComposerBanner.Content>
316321 {item.actions || item.onDismiss ? (
apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx+63-59
......@@ -228,66 +228,70 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(
228228 </ComposerBanner.Actions>
229229 </CollapsibleTrigger>
230230 <CollapsiblePanel>
231 <ComposerBanner.Body className="pe-1 pb-1">
232 <p className="text-sm text-foreground/85">{activeQuestion.question}</p>
233 {activeQuestion.multiSelect ? (
234 <p className="mt-1 text-secondary-label text-xs">Select one or more options.</p>
235 ) : null}
236 <div className="mt-2 space-y-0.5">
237 {activeQuestion.options.map((option, index) => {
238 const optionValue = option.value ?? option.label;
239 const isOptimisticallySelected =
240 optimisticSingleSelect?.questionId === activeQuestion.id &&
241 optimisticSingleSelect.optionValue === optionValue;
242 const isSelected =
243 isOptimisticallySelected ||
244 (!customAnswerActive && progress.selectedOptionValues.includes(optionValue));
245 const shortcutKey = index < 9 ? index + 1 : null;
246 const className = cn(
247 "group flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left outline-none transition-colors duration-150 focus-visible:ring-1 focus-visible:ring-primary/25",
248 isSelected
249 ? "bg-muted/55 text-foreground"
250 : "bg-transparent text-foreground/85 hover:bg-muted/30",
251 isResponding && "opacity-50 cursor-not-allowed",
252 !isResponding && "cursor-pointer",
253 );
254 const content = (
255 <>
256 <div className="min-w-0 flex-1 flex flex-col gap-0.5">
257 <span className="text-sm font-medium">{option.label}</span>
258 {option.description && option.description !== option.label ? (
259 <span className="text-secondary-label text-[11px]">{option.description}</span>
231 <ComposerBanner.Scroll>
232 <ComposerBanner.Body className="pe-1 pb-1 wrap-anywhere">
233 <p className="text-sm text-foreground/85">{activeQuestion.question}</p>
234 {activeQuestion.multiSelect ? (
235 <p className="mt-1 text-secondary-label text-xs">Select one or more options.</p>
236 ) : null}
237 <div className="mt-2 space-y-0.5">
238 {activeQuestion.options.map((option, index) => {
239 const optionValue = option.value ?? option.label;
240 const isOptimisticallySelected =
241 optimisticSingleSelect?.questionId === activeQuestion.id &&
242 optimisticSingleSelect.optionValue === optionValue;
243 const isSelected =
244 isOptimisticallySelected ||
245 (!customAnswerActive && progress.selectedOptionValues.includes(optionValue));
246 const shortcutKey = index < 9 ? index + 1 : null;
247 const className = cn(
248 "group flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left outline-none transition-colors duration-150 focus-visible:ring-1 focus-visible:ring-primary/25",
249 isSelected
250 ? "bg-muted/55 text-foreground"
251 : "bg-transparent text-foreground/85 hover:bg-muted/30",
252 isResponding && "opacity-50 cursor-not-allowed",
253 !isResponding && "cursor-pointer",
254 );
255 const content = (
256 <>
257 <div className="min-w-0 flex-1 flex flex-col gap-0.5">
258 <span className="text-sm font-medium">{option.label}</span>
259 {option.description && option.description !== option.label ? (
260 <span className="text-secondary-label text-[11px]">
261 {option.description}
262 </span>
263 ) : null}
264 </div>
265 {isSelected ? (
266 <CheckIcon className="size-3.5 shrink-0 text-primary" />
267 ) : shortcutKey !== null ? (
268 <kbd
269 className={cn(
270 "flex size-5 shrink-0 items-center justify-center text-[10px] font-medium text-muted-foreground tabular-nums",
271 )}
272 >
273 {shortcutKey}
274 </kbd>
260275 ) : null}
261 </div>
262 {isSelected ? (
263 <CheckIcon className="size-3.5 shrink-0 text-primary" />
264 ) : shortcutKey !== null ? (
265 <kbd
266 className={cn(
267 "flex size-5 shrink-0 items-center justify-center text-[10px] font-medium text-muted-foreground tabular-nums",
268 )}
269 >
270 {shortcutKey}
271 </kbd>
272 ) : null}
273 </>
274 );
275 return (
276 <button
277 key={`${activeQuestion.id}:${optionValue}`}
278 type="button"
279 disabled={isResponding}
280 onClick={() => {
281 handleOptionSelection(activeQuestion.id, optionValue);
282 }}
283 className={className}
284 >
285 {content}
286 </button>
287 );
288 })}
289 </div>
290 </ComposerBanner.Body>
276 </>
277 );
278 return (
279 <button
280 key={`${activeQuestion.id}:${optionValue}`}
281 type="button"
282 disabled={isResponding}
283 onClick={() => {
284 handleOptionSelection(activeQuestion.id, optionValue);
285 }}
286 className={className}
287 >
288 {content}
289 </button>
290 );
291 })}
292 </div>
293 </ComposerBanner.Body>
294 </ComposerBanner.Scroll>
291295 </CollapsiblePanel>
292296 </Collapsible>
293297 );
apps/web/src/components/chat/ComposerTasksBadge.tsx+1-1
......@@ -92,7 +92,7 @@ function TaskSummary({
9292 >
9393 {progress.completedSteps}/{progress.totalSteps}
9494 </ComposerBanner.Count>
95 <TaskSegments className="hidden w-20 sm:flex" steps={steps} />
95 <TaskSegments className="hidden w-20 @min-[560px]:flex" steps={steps} />
9696 <ComposerBanner.ToggleIcon expanded={expanded} />
9797 </ComposerBanner.Actions>
9898 </>