authorgravatar for 269035359+t3-code[bot]@users.noreply.github.comt3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> 2026-08-04 13:34:22+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-04 13:34:22+02:00
logcec1bb9de38a80d7c06faa6841360002ca136080
tree863347ab168f00075236e3541e07a351b9cbd7de
parent25ec0b9d13deaaa6d30c42f66ddfee633e9a29be
signature Signed by PGP key B5690EEEBB952194

fix(web): align multiline error alert controls (#5304)


3 files changed, 54 insertions(+), 5 deletions(-)

apps/web/src/components/chat/ThreadErrorBanner.test.tsx created+23
......@@ -0,0 +1,23 @@
1import { renderToStaticMarkup } from "react-dom/server";
2import { describe, expect, it } from "vite-plus/test";
3
4import { ThreadErrorBanner } from "./ThreadErrorBanner";
5
6describe("ThreadErrorBanner", () => {
7 it("aligns the warning and dismiss icons with the first line of a multi-line error", () => {
8 const markup = renderToStaticMarkup(
9 <ThreadErrorBanner
10 error={"The first error line\ncontinues on a second line"}
11 onDismiss={() => {}}
12 />,
13 );
14
15 expect(markup).toContain('role="alert"');
16 expect(markup).toContain('aria-label="Dismiss error"');
17 expect(markup).not.toContain("controlAlignment");
18 expect(markup).toContain("flex gap-2 items-start");
19 expect(markup).toContain("min-h-7 pt-1 sm:min-h-6 sm:pt-0.5");
20 expect(markup).toContain("h-lh w-4");
21 expect(markup).toContain("h-lh self-start");
22 });
23});
apps/web/src/components/chat/ThreadErrorBanner.tsx+1-1
......@@ -14,7 +14,7 @@ export const ThreadErrorBanner = memo(function ThreadErrorBanner({
1414 if (!error) return null;
1515 return (
1616 <div className="mx-auto w-fit max-w-[min(48rem,calc(100%-2rem))] pt-3">
17 <Alert variant="error">
17 <Alert variant="error" controlAlignment="first-line">
1818 <CircleAlertIcon />
1919 <AlertDescription>
2020 <Tooltip>
apps/web/src/components/ui/alert.tsx+30-4
......@@ -42,9 +42,13 @@ function alertChildSlot(child: React.ReactElement): string | undefined {
4242function Alert({
4343 className,
4444 variant,
45 controlAlignment = "center",
4546 children,
4647 ...props
47}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
48}: React.ComponentProps<"div"> &
49 VariantProps<typeof alertVariants> & {
50 controlAlignment?: "center" | "first-line";
51 }) {
4852 const icon: React.ReactNode[] = [];
4953 const content: React.ReactNode[] = [];
5054 const action: React.ReactNode[] = [];
......@@ -71,9 +75,24 @@ function Alert({
7175 role="alert"
7276 {...props}
7377 >
74 <div className="flex items-center gap-2">
78 <div
79 className={cn(
80 "flex gap-2",
81 controlAlignment === "first-line" ? "items-start" : "items-center",
82 controlAlignment === "first-line" &&
83 action.length > 0 &&
84 "min-h-7 pt-1 sm:min-h-6 sm:pt-0.5",
85 )}
86 >
7587 {icon.length > 0 && (
76 <div className="flex size-4 shrink-0 items-center justify-center [&>svg]:size-full">
88 <div
89 className={cn(
90 "flex shrink-0 items-center justify-center",
91 controlAlignment === "first-line"
92 ? "h-lh w-4 [&>svg]:size-4"
93 : "size-4 [&>svg]:size-full",
94 )}
95 >
7796 {icon}
7897 </div>
7998 )}
......@@ -81,7 +100,14 @@ function Alert({
81100 <div className="flex min-w-0 flex-1 flex-col gap-0.5">{content}</div>
82101 )}
83102 {action.length > 0 && (
84 <div className="flex shrink-0 items-center self-center">{action}</div>
103 <div
104 className={cn(
105 "flex shrink-0 items-center",
106 controlAlignment === "first-line" ? "h-lh self-start" : "self-center",
107 )}
108 >
109 {action}
110 </div>
85111 )}
86112 </div>
87113 </div>