48 lines
1.5 KiB
TypeScript
48 lines
1.5 KiB
TypeScript
|
|
import { useToast } from '@/hooks/useToast';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Stack of toast notifications anchored bottom-right.
|
|||
|
|
* Pure DESIGN.md surfaces: rounded-xl, soft-lift, ink slab for errors.
|
|||
|
|
*/
|
|||
|
|
export function ToastViewport(): JSX.Element {
|
|||
|
|
const { toasts, dismiss } = useToast();
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
aria-live="polite"
|
|||
|
|
aria-atomic="true"
|
|||
|
|
className="fixed bottom-xl right-xl z-50 flex flex-col gap-sm w-[320px] pointer-events-none"
|
|||
|
|
>
|
|||
|
|
{toasts.map((t) => {
|
|||
|
|
const isError = t.kind === 'error';
|
|||
|
|
const isSuccess = t.kind === 'success';
|
|||
|
|
const surface = isError
|
|||
|
|
? 'bg-ink text-ink-on'
|
|||
|
|
: isSuccess
|
|||
|
|
? 'bg-primary text-ink-on'
|
|||
|
|
: 'bg-canvas text-ink border border-hairline';
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
key={t.id}
|
|||
|
|
role="status"
|
|||
|
|
data-testid="toast"
|
|||
|
|
data-kind={t.kind}
|
|||
|
|
className={`pointer-events-auto rounded-xl px-md py-sm shadow-soft-lift text-[14px] leading-[1.4] ${surface}`}
|
|||
|
|
>
|
|||
|
|
<div className="flex items-start justify-between gap-sm">
|
|||
|
|
<span className="flex-1">{t.message}</span>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => dismiss(t.id)}
|
|||
|
|
aria-label="Dismiss notification"
|
|||
|
|
className="text-current opacity-70 hover:opacity-100"
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|