zebflow 0.7.9

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { cx } from "zeb";
import { useState, useEffect } from "zeb";

interface ToastItem {
  id: number;
  title?: string;
  description?: string;
  variant?: "default" | "success" | "error" | "warning" | "info";
}

interface ToasterProps {
  position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
  className?: string;
}

const VARIANT_CLASSES = {
  default: "bg-white border-gray-200 text-gray-900",
  success: "bg-green-50 border-green-200 text-green-900",
  error:   "bg-red-50 border-red-200 text-red-900",
  warning: "bg-yellow-50 border-yellow-200 text-yellow-900",
  info:    "bg-blue-50 border-blue-200 text-blue-900",
};

const POSITION_CLASSES = {
  "top-left":      "top-4 left-4",
  "top-center":    "top-4 left-1/2 -translate-x-1/2",
  "top-right":     "top-4 right-4",
  "bottom-left":   "bottom-4 left-4",
  "bottom-center": "bottom-4 left-1/2 -translate-x-1/2",
  "bottom-right":  "bottom-4 right-4",
};

// Global toast queue
let _toastQueue: ToastItem[] = [];
let _nextId = 1;
let _listeners: Array<(toasts: ToastItem[]) => void> = [];

function notify() {
  _listeners.forEach(fn => fn([..._toastQueue]));
}

export const toast = {
  show(msg: string | { title?: string; description?: string }, variant: ToastItem["variant"] = "default") {
    const item: ToastItem = {
      id: _nextId++,
      variant,
      ...(typeof msg === "string" ? { title: msg } : msg),
    };
    _toastQueue = [..._toastQueue, item];
    notify();
    setTimeout(() => {
      _toastQueue = _toastQueue.filter(t => t.id !== item.id);
      notify();
    }, 4000);
  },
  success: (msg: string | { title?: string; description?: string }) => toast.show(msg, "success"),
  error:   (msg: string | { title?: string; description?: string }) => toast.show(msg, "error"),
  warning: (msg: string | { title?: string; description?: string }) => toast.show(msg, "warning"),
  info:    (msg: string | { title?: string; description?: string }) => toast.show(msg, "info"),
};

export function Toaster({ position = "bottom-right", className }: ToasterProps) {
  const [toasts, setToasts] = useState<ToastItem[]>([]);

  useEffect(() => {
    _listeners.push(setToasts);
    return () => {
      _listeners = _listeners.filter(fn => fn !== setToasts);
    };
  }, []);

  if (toasts.length === 0) return null;

  return (
    <div
      aria-live="polite"
      aria-atomic="true"
      className={cx(
        "fixed z-[100] flex flex-col gap-2 pointer-events-none",
        POSITION_CLASSES[position] ?? POSITION_CLASSES["bottom-right"],
        className
      )}
    >
      {toasts.map((t) => (
        <div
          key={t.id}
          role="status"
          className={cx(
            "pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-lg border p-4 shadow-lg",
            VARIANT_CLASSES[t.variant ?? "default"]
          )}
        >
          <div className="flex-1 space-y-1">
            {t.title && <p className="text-sm font-semibold">{t.title}</p>}
            {t.description && <p className="text-sm opacity-90">{t.description}</p>}
          </div>
          <button
            type="button"
            onClick={() => { _toastQueue = _toastQueue.filter(x => x.id !== t.id); notify(); }}
            className="shrink-0 opacity-50 hover:opacity-100"
            aria-label="Dismiss"
          >
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
              <path d="M18 6 6 18" /><path d="m6 6 12 12" />
            </svg>
          </button>
        </div>
      ))}
    </div>
  );
}

export default Toaster;