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 {...@@ -38,6 +38,7 @@ import {
38 gitStatusQueryOptions,38 gitStatusQueryOptions,
39 invalidateGitQueries,39 invalidateGitQueries,
40} from "~/lib/gitReactQuery";40} from "~/lib/gitReactQuery";
41import { useStore } from "~/store";
41import { preferredTerminalEditor, resolvePathLinkTarget } from "~/terminal-links";42import { preferredTerminalEditor, resolvePathLinkTarget } from "~/terminal-links";
4243
43interface GitActionsControlProps {44interface GitActionsControlProps {
...@@ -136,6 +137,12 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {...@@ -136,6 +137,12 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {
136}137}
137138
138export default function GitActionsControl({ api, gitCwd }: GitActionsControlProps) {139export 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 );
139 const queryClient = useQueryClient();146 const queryClient = useQueryClient();
140 const [activeDialogAction, setActiveDialogAction] = useState<GitDialogAction | null>(null);147 const [activeDialogAction, setActiveDialogAction] = useState<GitDialogAction | null>(null);
141 const [dialogCommitMessage, setDialogCommitMessage] = useState("");148 const [dialogCommitMessage, setDialogCommitMessage] = useState("");
...@@ -190,25 +197,34 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -190,25 +197,34 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
190197
191 const openExistingPr = useCallback(async () => {198 const openExistingPr = useCallback(async () => {
192 if (!api) {199 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 });
194 return;205 return;
195 }206 }
196 const prUrl = gitStatus?.openPr?.url ?? null;207 const prUrl = gitStatus?.openPr?.url ?? null;
197 if (!prUrl) {208 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 });
199 return;214 return;
200 }215 }
201 const promise = api.shell.openExternal(prUrl);216 const promise = api.shell.openExternal(prUrl);
202 toastManager.promise(promise, {217 toastManager.promise(promise, {
203 loading: { title: "Opening PR..." },218 loading: { title: "Opening PR...", data: threadToastData },
204 success: { title: "PR opened" },219 success: { title: "PR opened", data: threadToastData },
205 error: (err) => ({220 error: (err) => ({
206 title: "Unable to open PR link",221 title: "Unable to open PR link",
207 description: err instanceof Error ? err.message : "An error occurred.",222 description: err instanceof Error ? err.message : "An error occurred.",
223 data: threadToastData,
208 }),224 }),
209 });225 });
210 void promise.catch(() => undefined);226 void promise.catch(() => undefined);
211 }, [api, gitStatus?.openPr?.url]);227 }, [api, gitStatus?.openPr?.url, threadToastData]);
212228
213 const runGitActionWithToast = useCallback(229 const runGitActionWithToast = useCallback(
214 async ({230 async ({
...@@ -238,6 +254,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -238,6 +254,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
238 type: "loading",254 type: "loading",
239 title: progressStages[0] ?? "Running git action...",255 title: progressStages[0] ?? "Running git action...",
240 timeout: 0,256 timeout: 0,
257 data: threadToastData,
241 });258 });
242259
243 let stageIndex = 0;260 let stageIndex = 0;
...@@ -247,6 +264,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -247,6 +264,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
247 title: progressStages[stageIndex] ?? "Running git action...",264 title: progressStages[stageIndex] ?? "Running git action...",
248 type: "loading",265 type: "loading",
249 timeout: 0,266 timeout: 0,
267 data: threadToastData,
250 });268 });
251 }, 1100);269 }, 1100);
252270
...@@ -279,6 +297,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -279,6 +297,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
279 title: resultToast.title,297 title: resultToast.title,
280 description: resultToast.description,298 description: resultToast.description,
281 timeout: 0,299 timeout: 0,
300 data: threadToastData,
282 ...(shouldOfferPushCta301 ...(shouldOfferPushCta
283 ? {302 ? {
284 actionProps: {303 actionProps: {
...@@ -320,6 +339,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -320,6 +339,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
320 type: "error",339 type: "error",
321 title: "Action failed",340 title: "Action failed",
322 description: err instanceof Error ? err.message : "An error occurred.",341 description: err instanceof Error ? err.message : "An error occurred.",
342 data: threadToastData,
323 });343 });
324 }344 }
325 },345 },
...@@ -332,6 +352,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -332,6 +352,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
332 maybeConfirmPushToDefaultBranch,352 maybeConfirmPushToDefaultBranch,
333 runImmediateGitActionMutation,353 runImmediateGitActionMutation,
334 setActiveDialogAction,354 setActiveDialogAction,
355 threadToastData,
335 ],356 ],
336 );357 );
337358
...@@ -343,17 +364,19 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -343,17 +364,19 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
343 if (quickAction.kind === "run_pull") {364 if (quickAction.kind === "run_pull") {
344 const promise = pullMutation.mutateAsync();365 const promise = pullMutation.mutateAsync();
345 toastManager.promise(promise, {366 toastManager.promise(promise, {
346 loading: { title: "Pulling..." },367 loading: { title: "Pulling...", data: threadToastData },
347 success: (result) => ({368 success: (result) => ({
348 title: result.status === "pulled" ? "Pulled" : "Already up to date",369 title: result.status === "pulled" ? "Pulled" : "Already up to date",
349 description:370 description:
350 result.status === "pulled"371 result.status === "pulled"
351 ? `Updated ${result.branch} from ${result.upstreamBranch ?? "upstream"}`372 ? `Updated ${result.branch} from ${result.upstreamBranch ?? "upstream"}`
352 : `${result.branch} is already synchronized.`,373 : `${result.branch} is already synchronized.`,
374 data: threadToastData,
353 }),375 }),
354 error: (err) => ({376 error: (err) => ({
355 title: "Pull failed",377 title: "Pull failed",
356 description: err instanceof Error ? err.message : "An error occurred.",378 description: err instanceof Error ? err.message : "An error occurred.",
379 data: threadToastData,
357 }),380 }),
358 });381 });
359 void promise.catch(() => undefined);382 void promise.catch(() => undefined);
...@@ -364,13 +387,14 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -364,13 +387,14 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
364 type: "info",387 type: "info",
365 title: quickAction.label,388 title: quickAction.label,
366 description: quickAction.hint,389 description: quickAction.hint,
390 data: threadToastData,
367 });391 });
368 return;392 return;
369 }393 }
370 if (quickAction.action) {394 if (quickAction.action) {
371 void runGitActionWithToast({ action: quickAction.action });395 void runGitActionWithToast({ action: quickAction.action });
372 }396 }
373 }, [openExistingPr, pullMutation, quickAction, runGitActionWithToast]);397 }, [openExistingPr, pullMutation, quickAction, runGitActionWithToast, threadToastData]);
374398
375 const openDialogForMenuItem = useCallback(399 const openDialogForMenuItem = useCallback(
376 (item: GitActionMenuItem) => {400 (item: GitActionMenuItem) => {
...@@ -414,6 +438,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -414,6 +438,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
414 toastManager.add({438 toastManager.add({
415 type: "error",439 type: "error",
416 title: "Editor opening is unavailable.",440 title: "Editor opening is unavailable.",
441 data: threadToastData,
417 });442 });
418 return;443 return;
419 }444 }
...@@ -423,10 +448,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -423,10 +448,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
423 type: "error",448 type: "error",
424 title: "Unable to open file",449 title: "Unable to open file",
425 description: error instanceof Error ? error.message : "An error occurred.",450 description: error instanceof Error ? error.message : "An error occurred.",
451 data: threadToastData,
426 });452 });
427 });453 });
428 },454 },
429 [api, gitCwd],455 [api, gitCwd, threadToastData],
430 );456 );
431457
432 if (!gitCwd) return null;458 if (!gitCwd) return null;
apps/web/src/components/ui/toast.tsx+174-148
...@@ -11,9 +11,15 @@ import {...@@ -11,9 +11,15 @@ import {
1111
12import { cn } from "~/lib/utils";12import { cn } from "~/lib/utils";
13import { buttonVariants } from "~/components/ui/button";13import { buttonVariants } from "~/components/ui/button";
14import { useStore } from "~/store";
1415
15const toastManager = Toast.createToastManager();16type ThreadToastData = {
16const anchoredToastManager = Toast.createToastManager();17 threadId?: string | null;
18 tooltipStyle?: boolean;
19};
20
21const toastManager = Toast.createToastManager<ThreadToastData>();
22const anchoredToastManager = Toast.createToastManager<ThreadToastData>();
1723
18const TOAST_ICONS = {24const TOAST_ICONS = {
19 error: CircleAlertIcon,25 error: CircleAlertIcon,
...@@ -35,6 +41,15 @@ interface ToastProviderProps extends Toast.Provider.Props {...@@ -35,6 +41,15 @@ interface ToastProviderProps extends Toast.Provider.Props {
35 position?: ToastPosition;41 position?: ToastPosition;
36}42}
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
38function ToastProvider({ children, position = "top-right", ...props }: ToastProviderProps) {53function ToastProvider({ children, position = "top-right", ...props }: ToastProviderProps) {
39 return (54 return (
40 <Toast.Provider toastManager={toastManager} {...props}>55 <Toast.Provider toastManager={toastManager} {...props}>
...@@ -45,7 +60,9 @@ function ToastProvider({ children, position = "top-right", ...props }: ToastProv...@@ -45,7 +60,9 @@ function ToastProvider({ children, position = "top-right", ...props }: ToastProv
45}60}
4661
47function Toasts({ position = "top-right" }: { position: ToastPosition }) {62function 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;
49 const isTop = position.startsWith("top");66 const isTop = position.startsWith("top");
5067
51 return (68 return (
...@@ -64,91 +81,96 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {...@@ -64,91 +81,96 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) {
64 data-position={position}81 data-position={position}
65 data-slot="toast-viewport"82 data-slot="toast-viewport"
66 >83 >
67 {toasts.map((toast) => {84 {toasts
68 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;85 .filter((toast) => shouldRenderForActiveThread(toast.data, activeThreadId))
6986 .map((toast) => {
70 return (87 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
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 )}
13488
135 <div className="flex flex-col gap-0.5">89 return (
136 <Toast.Title className="font-medium" data-slot="toast-title" />90 <Toast.Root
137 <Toast.Description91 className={cn(
138 className="text-muted-foreground"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%)]",
139 data-slot="toast-description"93 // Base positioning using data-position
140 />94 "data-[position*=right]:right-0 data-[position*=right]:left-auto",
141 </div>95 "data-[position*=left]:right-auto data-[position*=left]:left-0",
142 </div>96 "data-[position*=center]:right-0 data-[position*=center]:left-0",
143 {toast.actionProps && (97 "data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top",
144 <Toast.Action className={buttonVariants({ size: "xs" })} data-slot="toast-action">98 "data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom",
145 {toast.actionProps.children}99 // Gap fill for hover
146 </Toast.Action>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)))]",
147 )}131 )}
148 </Toast.Content>132 data-position={position}
149 </Toast.Root>133 key={toast.id}
150 );134 swipeDirection={
151 })}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 })}
152 </Toast.Viewport>174 </Toast.Viewport>
153 </Toast.Portal>175 </Toast.Portal>
154 );176 );
...@@ -164,76 +186,80 @@ function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) {...@@ -164,76 +186,80 @@ function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) {
164}186}
165187
166function AnchoredToasts() {188function AnchoredToasts() {
167 const { toasts } = Toast.useToastManager();189 const { state } = useStore();
190 const { toasts } = Toast.useToastManager<ThreadToastData>();
191 const activeThreadId = state.activeThreadId;
168192
169 return (193 return (
170 <Toast.Portal data-slot="toast-portal-anchored">194 <Toast.Portal data-slot="toast-portal-anchored">
171 <Toast.Viewport className="outline-none" data-slot="toast-viewport-anchored">195 <Toast.Viewport className="outline-none" data-slot="toast-viewport-anchored">
172 {toasts.map((toast) => {196 {toasts
173 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;197 .filter((toast) => shouldRenderForActiveThread(toast.data, activeThreadId))
174 const tooltipStyle = (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;198 .map((toast) => {
175 const positionerProps = toast.positionerProps;199 const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
176200 const tooltipStyle = toast.data?.tooltipStyle ?? false;
177 if (!positionerProps?.anchor) {201 const positionerProps = toast.positionerProps;
178 return null;202
179 }203 if (!positionerProps?.anchor) {
180204 return null;
181 return (205 }
182 <Toast.Positioner206
183 className="z-50 max-w-[min(--spacing(64),var(--available-width))]"207 return (
184 data-slot="toast-positioner"208 <Toast.Positioner
185 key={toast.id}209 className="z-50 max-w-[min(--spacing(64),var(--available-width))]"
186 sideOffset={positionerProps.sideOffset ?? 4}210 data-slot="toast-positioner"
187 toast={toast}211 key={toast.id}
188 >212 sideOffset={positionerProps.sideOffset ?? 4}
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"
197 toast={toast}213 toast={toast}
198 >214 >
199 {tooltipStyle ? (215 <Toast.Root
200 <Toast.Content className="pointer-events-auto px-2 py-1">216 className={cn(
201 <Toast.Title data-slot="toast-title" />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%)]",
202 </Toast.Content>218 tooltipStyle
203 ) : (219 ? "rounded-md shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]"
204 <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">220 : "rounded-lg shadow-lg/5 before:rounded-[calc(var(--radius-lg)-1px)]",
205 <div className="flex gap-2">221 )}
206 {Icon && (222 data-slot="toast-popup"
207 <div223 toast={toast}
208 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"224 >
209 data-slot="toast-icon"225 {tooltipStyle ? (
210 >226 <Toast.Content className="pointer-events-auto px-2 py-1">
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" />227 <Toast.Title data-slot="toast-title" />
212 </div>228 </Toast.Content>
213 )}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">241 <div className="flex flex-col gap-0.5">
216 <Toast.Title className="font-medium" data-slot="toast-title" />242 <Toast.Title className="font-medium" data-slot="toast-title" />
217 <Toast.Description243 <Toast.Description
218 className="text-muted-foreground"244 className="text-muted-foreground"
219 data-slot="toast-description"245 data-slot="toast-description"
220 />246 />
247 </div>
221 </div>248 </div>
222 </div>249 {toast.actionProps && (
223 {toast.actionProps && (250 <Toast.Action
224 <Toast.Action251 className={buttonVariants({ size: "xs" })}
225 className={buttonVariants({ size: "xs" })}252 data-slot="toast-action"
226 data-slot="toast-action"253 >
227 >254 {toast.actionProps.children}
228 {toast.actionProps.children}255 </Toast.Action>
229 </Toast.Action>256 )}
230 )}257 </Toast.Content>
231 </Toast.Content>258 )}
232 )}259 </Toast.Root>
233 </Toast.Root>260 </Toast.Positioner>
234 </Toast.Positioner>261 );
235 );262 })}
236 })}
237 </Toast.Viewport>263 </Toast.Viewport>
238 </Toast.Portal>264 </Toast.Portal>
239 );265 );