▚Toastcustom
Toast provider with queue, tones, and auto-dismiss
$ termino toast — click buttons below
queue 0 auto-dismiss after ttl
##API
Context-based: wrap the app in `<ToastProvider>`, then call `useToasts().push(...)` from anywhere. Auto-dismiss via timers; a per-toast timeline renders the TTL bar.
properties
| prop | type | default | description |
|---|---|---|---|
| ToastProvider | children: ReactNode | - | Root provider, renders the toast stack overlay |
| push | { title, message?, tone, duration? } | duration 4000 | Returns toast id |
| dismiss | (id: number) => void | - | Remove a toast |
| ToastItem | interface | - | { id, title, message?, tone, duration } |
##Keymap
push()Append a toast (info | success | warning | error)
dismiss(id)Remove a toast early
##Notes
- ▸Timers are cleaned up on unmount — no leaks.
- ▸Stack is absolutely positioned top-right, above your app content.
##Source
Real implementation — this is the code that runs in your terminal.
1import { createElement as h, createContext, useContext, useCallback, useEffect, useState } from "react";2import { useTimeline } from "@opentui/react";34export interface ToastItem {5 id: number;6 title: string;7 message?: string;8 tone: "info" | "success" | "warning" | "error";9 duration: number;10}1112interface ToastContextValue {13 push: (toast: Omit<ToastItem, "id" | "duration"> & { duration?: number }) => number;14 dismiss: (id: number) => void;15}1617const ToastContext = createContext<ToastContextValue>({18 push: () => 0,19 dismiss: () => {},20});2122export function useToasts() {23 return useContext(ToastContext);24}2526const TONES = {27 info: { fg: "#7dcfff", bg: "#1f3a3a", glyph: "ℹ" },28 success: { fg: "#9ece6a", bg: "#1f3a2e", glyph: "✓" },29 warning: { fg: "#e0af68", bg: "#3a2e1f", glyph: "!" },30 error: { fg: "#f7768e", bg: "#3a1f28", glyph: "✗" },31} as const;3233let nextId = 1;3435const timers = new Map<number, ReturnType<typeof setTimeout>>();3637export function ToastProvider({ children }: { children: React.ReactNode }) {38 const [toasts, setToasts] = useState<ToastItem[]>([]);3940 const dismiss = useCallback((id: number) => {41 const timer = timers.get(id);42 if (timer) clearTimeout(timer);43 timers.delete(id);44 setToasts((prev) => prev.filter((t) => t.id !== id));45 }, []);4647 const push = useCallback(48 (toast: Omit<ToastItem, "id" | "duration"> & { duration?: number }) => {49 const id = nextId++;50 const duration = toast.duration ?? 4000;51 setToasts((prev) => [...prev, { ...toast, id, duration }]);52 timers.set(53 id,54 setTimeout(() => dismiss(id), duration),55 );56 return id;57 },58 [dismiss],59 );6061 useEffect(() => {62 return () => {63 for (const timer of timers.values()) clearTimeout(timer);64 timers.clear();65 };66 }, []);6768 return h(69 ToastContext.Provider,70 { value: { push, dismiss } },71 children,72 h(ToastViewport, { toasts }),73 );74}7576function ToastViewport({ toasts }: { toasts: ToastItem[] }) {77 return h(78 "box",79 { position: "absolute", right: 0, top: 0, flexDirection: "column", gap: 1 },80 toasts.map((toast) => h(ToastCard, { key: toast.id, toast })),81 );82}8384function ToastCard({ toast }: { toast: ToastItem }) {85 const tone = TONES[toast.tone];86 const [pct, setPct] = useState(100);8788 const timeline = useTimeline({89 duration: toast.duration,90 autoplay: true,91 });9293 useEffect(() => {94 timeline.add(95 { pct: 100 },96 {97 pct: 0,98 duration: toast.duration,99 ease: "linear",100 onUpdate: (a) => setPct(a.targets[0].pct as number),101 },102 0,103 );104 }, [timeline, toast.duration]);105106 return h(107 "box",108 {109 style: {110 width: 40,111 border: true,112 borderColor: tone.fg,113 backgroundColor: "#16161e",114 paddingLeft: 1,115 paddingRight: 1,116 flexDirection: "column",117 },118 },119 h(120 "box",121 { flexDirection: "row", justifyContent: "space-between", width: 38 },122 h(123 "text",124 { fg: tone.fg },125 `${tone.glyph} ${toast.title}`,126 ),127 h("text", { fg: "#565f89" }, "esc"),128 ),129 toast.message ? h("text", { fg: "#a9b1d6" }, toast.message) : null,130 h("box", { style: { height: 1, marginTop: 1, backgroundColor: "#2f3449" } },131 h("box", {132 style: {133 width: Math.round((pct / 100) * 38),134 height: 1,135 backgroundColor: tone.fg,136 },137 }),138 ),139 );140}141