[]previous and next page

Toast

shadcn's toast is Toast plus a useToast() store and a Toaster that renders the store.

For agentsUpdated

shadcn's toast is Toast plus a useToast() store and a Toaster that renders the store. createToaster is that store (ids, de-duplication by key, a limit with a waiting list, promise toasts, exit animations), so the port keeps shadcn's names and replaces the hand-written reducer.

src/ui/toast.tsxtsx
import { createToaster, Toast as ToastPrimitive, type ToastOptions } from "@shadow-js/primitives";
import { For, Show } from "@shadow-js/web";
import { variants } from "../lib/utils";
import { XIcon } from "./icons";

export const toastVariants = variants(
  "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[state=closed]:animate-out data-[state=open]:animate-in",
  {
    variants: {
      variant: {
        default: "border bg-background text-foreground",
        destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
      },
    },
    defaultVariants: { variant: "default" },
  },
);

export interface ToastData {
  readonly variant?: "default" | "destructive" | undefined;
}

/** One queue for the application, as with shadcn's `useToast` store. */
export const toaster = createToaster<ToastData>({ limit: 3, duration: 5000 });

export function toast(options: ToastOptions<ToastData>): string {
  return toaster.add(options);
}

export function Toaster() {
  return (
    <ToastPrimitive.Provider label="Notifications">
      <For each={toaster.toasts()} key={(item) => item.id}>
        {(item) => (
          <ToastPrimitive
            class={toastVariants({ variant: item().data?.variant })}
            open={item().open}
            duration={item().duration}
            onOpenChange={(open: boolean) => {
              if (!open) toaster.dismiss(item().id);
            }}
            onExitComplete={() => toaster.remove(item().id)}
          >
            <div class="grid gap-1">
              <ToastPrimitive.Title class="text-sm font-semibold">{item().title}</ToastPrimitive.Title>
              <Show when={item().description !== undefined}>
                <ToastPrimitive.Description class="text-sm opacity-90">{item().description}</ToastPrimitive.Description>
              </Show>
            </div>
            <ToastPrimitive.Close
              aria-label="Close"
              class="absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100"
            >
              <XIcon class="h-4 w-4" />
            </ToastPrimitive.Close>
          </ToastPrimitive>
        )}
      </For>
      <ToastPrimitive.Viewport class="fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]" />
    </ToastPrimitive.Provider>
  );
}