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"
}