authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 01:08:34-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 01:08:34-08:00
log1f17e9c429c4dad56762c60137436923b61cc499
tree7ef0d1915872e4253490c8aa3529b72ee08bf7e1
parent6708fe996ac144e606abd8645530ea0d7023d42d

scope toasts per thread


2 files changed, 208 insertions(+), 156 deletions(-)

apps/web/src/components/GitActionsControl.tsx+34-8
......@@ -38,6 +38,7 @@ import {
3838 gitStatusQueryOptions,
3939 invalidateGitQueries,
4040} from "~/lib/gitReactQuery";
41import { useStore } from "~/store";
4142import { preferredTerminalEditor, resolvePathLinkTarget } from "~/terminal-links";
4243
4344interface GitActionsControlProps {
......@@ -136,6 +137,12 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {
136137}
137138
138139export default function GitActionsControl({ api, gitCwd }: GitActionsControlProps) {
140 const { state } = useStore();
141 const activeThreadId = state.activeThreadId;
142 const threadToastData = useMemo(
143 () => (activeThreadId ? { threadId: activeThreadId } : undefined),
144 [activeThreadId],
145 );
139146 const queryClient = useQueryClient();
140147 const [activeDialogAction, setActiveDialogAction] = useState<GitDialogAction | null>(null);
141148 const [dialogCommitMessage, setDialogCommitMessage] = useState("");
......@@ -190,25 +197,34 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
190197
191198 const openExistingPr = useCallback(async () => {
192199 if (!api) {
193 toastManager.add({ type: "error", title: "Link opening is unavailable." });
200 toastManager.add({
201 type: "error",
202 title: "Link opening is unavailable.",
203 data: threadToastData,
204 });
194205 return;
195206 }
196207 const prUrl = gitStatus?.openPr?.url ?? null;
197208 if (!prUrl) {
198 toastManager.add({ type: "error", title: "No open PR found." });
209 toastManager.add({
210 type: "error",
211 title: "No open PR found.",
212 data: threadToastData,
213 });
199214 return;
200215 }
201216 const promise = api.shell.openExternal(prUrl);
202217 toastManager.promise(promise, {
203 loading: { title: "Opening PR..." },
204 success: { title: "PR opened" },
218 loading: { title: "Opening PR...", data: threadToastData },
219 success: { title: "PR opened", data: threadToastData },
205220 error: (err) => ({
206221 title: "Unable to open PR link",
207222 description: err instanceof Error ? err.message : "An error occurred.",
223 data: threadToastData,
208224 }),
209225 });
210226 void promise.catch(() => undefined);
211 }, [api, gitStatus?.openPr?.url]);
227 }, [api, gitStatus?.openPr?.url, threadToastData]);
212228
213229 const runGitActionWithToast = useCallback(
214230 async ({
......@@ -238,6 +254,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
238254 type: "loading",
239255 title: progressStages[0] ?? "Running git action...",
240256 timeout: 0,
257 data: threadToastData,
241258 });
242259
243260 let stageIndex = 0;
......@@ -247,6 +264,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
247264 title: progressStages[stageIndex] ?? "Running git action...",
248265 type: "loading",
249266 timeout: 0,
267 data: threadToastData,
250268 });
251269 }, 1100);
252270
......@@ -279,6 +297,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
279297 title: resultToast.title,
280298 description: resultToast.description,
281299 timeout: 0,
300 data: threadToastData,
282301 ...(shouldOfferPushCta
283302 ? {
284303 actionProps: {
......@@ -320,6 +339,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
320339 type: "error",
321340 title: "Action failed",
322341 description: err instanceof Error ? err.message : "An error occurred.",
342 data: threadToastData,
323343 });
324344 }
325345 },
......@@ -332,6 +352,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
332352 maybeConfirmPushToDefaultBranch,
333353 runImmediateGitActionMutation,
334354 setActiveDialogAction,
355 threadToastData,
335356 ],
336357 );
337358
......@@ -343,17 +364,19 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
343364 if (quickAction.kind === "run_pull") {
344365 const promise = pullMutation.mutateAsync();
345366 toastManager.promise(promise, {
346 loading: { title: "Pulling..." },
367 loading: { title: "Pulling...", data: threadToastData },
347368 success: (result) => ({
348369 title: result.status === "pulled" ? "Pulled" : "Already up to date",
349370 description:
350371 result.status === "pulled"
351372 ? `Updated ${result.branch} from ${result.upstreamBranch ?? "upstream"}`
352373 : `${result.branch} is already synchronized.`,
374 data: threadToastData,
353375 }),
354376 error: (err) => ({
355377 title: "Pull failed",
356378 description: err instanceof Error ? err.message : "An error occurred.",
379 data: threadToastData,
357380 }),
358381 });
359382 void promise.catch(() => undefined);
......@@ -364,13 +387,14 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
364387 type: "info",
365388 title: quickAction.label,
366389 description: quickAction.hint,
390 data: threadToastData,
367391 });
368392 return;
369393 }
370394 if (quickAction.action) {
371395 void runGitActionWithToast({ action: quickAction.action });
372396 }
373 }, [openExistingPr, pullMutation, quickAction, runGitActionWithToast]);
397 }, [openExistingPr, pullMutation, quickAction, runGitActionWithToast, threadToastData]);
374398
375399 const openDialogForMenuItem = useCallback(
376400 (item: GitActionMenuItem) => {
......@@ -414,6 +438,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
414438 toastManager.add({
415439 type: "error",
416440 title: "Editor opening is unavailable.",
441 data: threadToastData,
417442 });
418443 return;
419444 }
......@@ -423,10 +448,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
423448 type: "error",
424449 title: "Unable to open file",
425450 description: error instanceof Error ? error.message : "An error occurred.",
451 data: threadToastData,
426452 });
427453 });
428454 },
429 [api, gitCwd],
455 [api, gitCwd, threadToastData],
430456 );
431457
432458 if (!gitCwd) return null;
apps/web/src/components/ui/toast.tsx+174-148
......@@ -11,9 +11,15 @@ import {
1111
1212import { cn } from "~/lib/utils";
1313import { buttonVariants } from "~/components/ui/button";
14import { useStore } from "~/store";
1415
15const toastManager = Toast.createToastManager();
16const anchoredToastManager = Toast.createToastManager();
16type ThreadToastData = {
17 threadId?: string | null;
18 tooltipStyle?: boolean;
19};
20
21const toastManager = Toast.createToastManager<ThreadToastData>();
22const anchoredToastManager = Toast.createToastManager<ThreadToastData>();
1723
1824const TOAST_ICONS = {
1925 error: CircleAlertIcon,
......@@ -35,6 +41,15 @@ interface ToastProviderProps extends Toast.Provider.Props {
3541 position?: ToastPosition;
3642}
3743
44function shouldRenderForActiveThread(
45 data: ThreadToastData | undefined,
46 activeThreadId: string | null,
47): boolean {
48 const toastThreadId = data?.threadId;
49 if (!toastThreadId) return true;
50 return toastThreadId === activeThreadId;
51}
52
3853function ToastProvider({ children, position = "top-right", ...props }: ToastProviderProps) {
3954 return (
4055 <Toast.Provider toastManager={toastManager} {...props}>
......@@ -45,7 +60,9 @@ function ToastProvider({ children, position = "top-right", ...props }: ToastProv
4560}
4661
4762function Toasts({ position = "top-right" }: { position: ToastPosition }) {
48 const { toasts } = Toast.useToastManager();
63 const { state } = useStore();
64 const { toasts } = Toast.useToastManager<ThreadToastData>();
65 const activeThreadId = state.activeThreadId;
4966 const isTop = position.startsWith("top");
5067
5168 return (
......@@ -64,91 +81,96 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
6481 data-position={position}
6582 data-slot="toast-viewport"
6683 >
67 {toasts.map((toast) => {
68 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
69
70 return (
71 <Toast.Root
72 className={cn(
73 "absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full select-none rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
74 // Base positioning using data-position
75 "data-[position*=right]:right-0 data-[position*=right]:left-auto",
76 "data-[position*=left]:right-auto data-[position*=left]:left-0",
77 "data-[position*=center]:right-0 data-[position*=center]:left-0",
78 "data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top",
79 "data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom",
80 // Gap fill for hover
81 "after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full",
82 "data-[position*=top]:after:top-full",
83 "data-[position*=bottom]:after:bottom-full",
84 // Define some variables
85 "[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]",
86 // Define offset-y variable
87 "data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",
88 "data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]",
89 // Default state transform
90 "data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
91 "data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
92 // Limited state
93 "data-limited:opacity-0",
94 // Expanded state
95 "data-expanded:h-(--toast-height)",
96 "data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]",
97 // Starting and ending animations
98 "data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]",
99 "data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]",
100 "data-ending-style:opacity-0",
101 // Ending animations (direction-aware)
102 "data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]",
103 "data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
104 "data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
105 "data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
106 "data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
107 // Ending animations (expanded)
108 "data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
109 "data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
110 "data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
111 "data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
112 )}
113 data-position={position}
114 key={toast.id}
115 swipeDirection={
116 position.includes("center")
117 ? [isTop ? "up" : "down"]
118 : position.includes("left")
119 ? ["left", isTop ? "up" : "down"]
120 : ["right", isTop ? "up" : "down"]
121 }
122 toast={toast}
123 >
124 <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm transition-opacity duration-250 data-behind:not-data-expanded:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100">
125 <div className="flex gap-2">
126 {Icon && (
127 <div
128 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
129 data-slot="toast-icon"
130 >
131 <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
132 </div>
133 )}
84 {toasts
85 .filter((toast) => shouldRenderForActiveThread(toast.data, activeThreadId))
86 .map((toast) => {
87 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
13488
135 <div className="flex flex-col gap-0.5">
136 <Toast.Title className="font-medium" data-slot="toast-title" />
137 <Toast.Description
138 className="text-muted-foreground"
139 data-slot="toast-description"
140 />
141 </div>
142 </div>
143 {toast.actionProps && (
144 <Toast.Action className={buttonVariants({ size: "xs" })} data-slot="toast-action">
145 {toast.actionProps.children}
146 </Toast.Action>
89 return (
90 <Toast.Root
91 className={cn(
92 "absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full select-none rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
93 // Base positioning using data-position
94 "data-[position*=right]:right-0 data-[position*=right]:left-auto",
95 "data-[position*=left]:right-auto data-[position*=left]:left-0",
96 "data-[position*=center]:right-0 data-[position*=center]:left-0",
97 "data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top",
98 "data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom",
99 // Gap fill for hover
100 "after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full",
101 "data-[position*=top]:after:top-full",
102 "data-[position*=bottom]:after:bottom-full",
103 // Define some variables
104 "[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]",
105 // Define offset-y variable
106 "data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",
107 "data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]",
108 // Default state transform
109 "data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
110 "data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
111 // Limited state
112 "data-limited:opacity-0",
113 // Expanded state
114 "data-expanded:h-(--toast-height)",
115 "data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]",
116 // Starting and ending animations
117 "data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]",
118 "data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]",
119 "data-ending-style:opacity-0",
120 // Ending animations (direction-aware)
121 "data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]",
122 "data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
123 "data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
124 "data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
125 "data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
126 // Ending animations (expanded)
127 "data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
128 "data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
129 "data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
130 "data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
147131 )}
148 </Toast.Content>
149 </Toast.Root>
150 );
151 })}
132 data-position={position}
133 key={toast.id}
134 swipeDirection={
135 position.includes("center")
136 ? [isTop ? "up" : "down"]
137 : position.includes("left")
138 ? ["left", isTop ? "up" : "down"]
139 : ["right", isTop ? "up" : "down"]
140 }
141 toast={toast}
142 >
143 <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm transition-opacity duration-250 data-behind:not-data-expanded:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100">
144 <div className="flex gap-2">
145 {Icon && (
146 <div
147 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
148 data-slot="toast-icon"
149 >
150 <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
151 </div>
152 )}
153
154 <div className="flex flex-col gap-0.5">
155 <Toast.Title className="font-medium" data-slot="toast-title" />
156 <Toast.Description
157 className="text-muted-foreground"
158 data-slot="toast-description"
159 />
160 </div>
161 </div>
162 {toast.actionProps && (
163 <Toast.Action
164 className={buttonVariants({ size: "xs" })}
165 data-slot="toast-action"
166 >
167 {toast.actionProps.children}
168 </Toast.Action>
169 )}
170 </Toast.Content>
171 </Toast.Root>
172 );
173 })}
152174 </Toast.Viewport>
153175 </Toast.Portal>
154176 );
......@@ -164,76 +186,80 @@ function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) {
164186}
165187
166188function AnchoredToasts() {
167 const { toasts } = Toast.useToastManager();
189 const { state } = useStore();
190 const { toasts } = Toast.useToastManager<ThreadToastData>();
191 const activeThreadId = state.activeThreadId;
168192
169193 return (
170194 <Toast.Portal data-slot="toast-portal-anchored">
171195 <Toast.Viewport className="outline-none" data-slot="toast-viewport-anchored">
172 {toasts.map((toast) => {
173 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
174 const tooltipStyle = (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
175 const positionerProps = toast.positionerProps;
176
177 if (!positionerProps?.anchor) {
178 return null;
179 }
180
181 return (
182 <Toast.Positioner
183 className="z-50 max-w-[min(--spacing(64),var(--available-width))]"
184 data-slot="toast-positioner"
185 key={toast.id}
186 sideOffset={positionerProps.sideOffset ?? 4}
187 toast={toast}
188 >
189 <Toast.Root
190 className={cn(
191 "relative text-balance border bg-popover not-dark:bg-clip-padding text-popover-foreground text-xs transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
192 tooltipStyle
193 ? "rounded-md shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]"
194 : "rounded-lg shadow-lg/5 before:rounded-[calc(var(--radius-lg)-1px)]",
195 )}
196 data-slot="toast-popup"
196 {toasts
197 .filter((toast) => shouldRenderForActiveThread(toast.data, activeThreadId))
198 .map((toast) => {
199 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
200 const tooltipStyle = toast.data?.tooltipStyle ?? false;
201 const positionerProps = toast.positionerProps;
202
203 if (!positionerProps?.anchor) {
204 return null;
205 }
206
207 return (
208 <Toast.Positioner
209 className="z-50 max-w-[min(--spacing(64),var(--available-width))]"
210 data-slot="toast-positioner"
211 key={toast.id}
212 sideOffset={positionerProps.sideOffset ?? 4}
197213 toast={toast}
198214 >
199 {tooltipStyle ? (
200 <Toast.Content className="pointer-events-auto px-2 py-1">
201 <Toast.Title data-slot="toast-title" />
202 </Toast.Content>
203 ) : (
204 <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
205 <div className="flex gap-2">
206 {Icon && (
207 <div
208 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
209 data-slot="toast-icon"
210 >
211 <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
212 </div>
213 )}
215 <Toast.Root
216 className={cn(
217 "relative text-balance border bg-popover not-dark:bg-clip-padding text-popover-foreground text-xs transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
218 tooltipStyle
219 ? "rounded-md shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]"
220 : "rounded-lg shadow-lg/5 before:rounded-[calc(var(--radius-lg)-1px)]",
221 )}
222 data-slot="toast-popup"
223 toast={toast}
224 >
225 {tooltipStyle ? (
226 <Toast.Content className="pointer-events-auto px-2 py-1">
227 <Toast.Title data-slot="toast-title" />
228 </Toast.Content>
229 ) : (
230 <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
231 <div className="flex gap-2">
232 {Icon && (
233 <div
234 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
235 data-slot="toast-icon"
236 >
237 <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
238 </div>
239 )}
214240
215 <div className="flex flex-col gap-0.5">
216 <Toast.Title className="font-medium" data-slot="toast-title" />
217 <Toast.Description
218 className="text-muted-foreground"
219 data-slot="toast-description"
220 />
241 <div className="flex flex-col gap-0.5">
242 <Toast.Title className="font-medium" data-slot="toast-title" />
243 <Toast.Description
244 className="text-muted-foreground"
245 data-slot="toast-description"
246 />
247 </div>
221248 </div>
222 </div>
223 {toast.actionProps && (
224 <Toast.Action
225 className={buttonVariants({ size: "xs" })}
226 data-slot="toast-action"
227 >
228 {toast.actionProps.children}
229 </Toast.Action>
230 )}
231 </Toast.Content>
232 )}
233 </Toast.Root>
234 </Toast.Positioner>
235 );
236 })}
249 {toast.actionProps && (
250 <Toast.Action
251 className={buttonVariants({ size: "xs" })}
252 data-slot="toast-action"
253 >
254 {toast.actionProps.children}
255 </Toast.Action>
256 )}
257 </Toast.Content>
258 )}
259 </Toast.Root>
260 </Toast.Positioner>
261 );
262 })}
237263 </Toast.Viewport>
238264 </Toast.Portal>
239265 );