dodil/erp-ui

public/r/toast.json

{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "title": "Toaster",
  "description": "Confirm a step that worked — once, announced, dismissible. Refusals are never toasts.",
  "dependencies": [
    "@radix-ui/themes",
    "radix-ui"
  ],
  "files": [
    {
      "path": "src/components/toast/Toaster.tsx",
      "content": "/**\n * Toasts — \"Deal moved to proposal\", \"Quote sent\". The confirmation of a step that worked, said once and\n * gone. A refusal is NOT a toast: it stays where the person acted (FormDialog, ConfirmDialog, ErrorState),\n * because a reason that disappears after four seconds is a reason nobody read.\n *\n * Built on the Radix Toast primitive (announced to screen readers, swipe / Escape to dismiss), styled with\n * Radix Themes tokens.\n */\nimport { Card, Flex, IconButton, Text } from \"@radix-ui/themes\";\nimport { Toast } from \"radix-ui\";\nimport { createContext, useCallback, useContext, useState, type ReactNode } from \"react\";\n\nexport interface ToastMessage {\n  title: string;\n  description?: string;\n  tone?: \"success\" | \"neutral\" | \"warning\";\n}\n\nconst Ctx = createContext<(m: ToastMessage) => void>(() => {});\n\n/** `const toast = useToast(); toast({ title: \"Quote sent\" })`. */\nexport function useToast() {\n  return useContext(Ctx);\n}\n\nconst COLOR = { success: \"green\", neutral: \"gray\", warning: \"amber\" } as const;\n\nexport function Toaster({ children, duration = 4000 }: { children: ReactNode; duration?: number }) {\n  const [items, setItems] = useState<(ToastMessage & { id: number })[]>([]);\n  const push = useCallback((m: ToastMessage) => setItems((s) => [...s, { ...m, id: Date.now() + Math.random() }]), []);\n  return (\n    <Ctx.Provider value={push}>\n      <Toast.Provider duration={duration} swipeDirection=\"right\" label=\"Notification\">\n        {children}\n        {items.map((t) => (\n          <Toast.Root key={t.id} asChild\n                      onOpenChange={(open) => { if (!open) setItems((s) => s.filter((x) => x.id !== t.id)); }}>\n            <Card style={{ borderLeft: `3px solid var(--${COLOR[t.tone ?? \"success\"]}-9)` }}>\n              <Flex gap=\"3\" align=\"start\" justify=\"between\">\n                <Flex direction=\"column\" gap=\"1\">\n                  <Toast.Title asChild><Text weight=\"bold\" size=\"2\">{t.title}</Text></Toast.Title>\n                  {t.description && <Toast.Description asChild><Text size=\"2\" color=\"gray\">{t.description}</Text></Toast.Description>}\n                </Flex>\n                <Toast.Close asChild>\n                  <IconButton size=\"1\" variant=\"ghost\" color=\"gray\" aria-label=\"Dismiss\">×</IconButton>\n                </Toast.Close>\n              </Flex>\n            </Card>\n          </Toast.Root>\n        ))}\n        <Toast.Viewport style={{\n          position: \"fixed\", bottom: \"var(--space-4)\", right: \"var(--space-4)\", zIndex: 2147483647,\n          display: \"flex\", flexDirection: \"column\", gap: \"var(--space-2)\", width: \"min(380px, calc(100vw - 32px))\",\n          margin: 0, padding: 0, listStyle: \"none\", outline: \"none\",\n        }} />\n      </Toast.Provider>\n    </Ctx.Provider>\n  );\n}\n",
      "type": "registry:component",
      "target": "erp-ui/components/toast/Toaster.tsx"
    }
  ],
  "type": "registry:component"
}