Files
helix-engage/src/components/application/notifications/toaster.tsx
saridsa2 526ad18159 feat: call desk redesign — 2-panel layout, collapsible sidebar, inline AI, ringtone
- Collapsible sidebar with Jotai atom (icon-only mode, persisted to localStorage)
- 2-panel call desk: worklist (60%) + context panel (40%) with AI + Lead 360 tabs
- Inline AI call prep card — known lead summary or unknown caller script
- Active call card with compact Answer/Decline buttons
- Worklist panel with human-readable labels, priority badges, click-to-select
- Context panel auto-switches to Lead 360 when lead selected or call incoming
- Browser ringtone via Web Audio API on incoming calls
- Sonner + Untitled UI IconNotification for toast system
- apiClient pattern: centralized post/get/graphql with auto-toast on errors
- Remove duplicate avatar from top bar, hide floating widget on call desk
- Fix Link routing in collapsed sidebar (was using <a> causing full page reload)
- Fix GraphQL field names: adStatus→status, platformUrl needs subfield selection
- Silent mode for DataProvider queries to prevent toast spam

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 18:33:36 +05:30

73 lines
2.5 KiB
TypeScript

import type { ToasterProps } from "sonner";
import { Toaster as SonnerToaster, useSonner } from "sonner";
import { cx } from "@/utils/cx";
export const DEFAULT_TOAST_POSITION = "bottom-right";
export const ToastsOverlay = () => {
const { toasts } = useSonner();
const styles = {
"top-right": {
className: "top-0 right-0",
background: "linear-gradient(215deg, rgba(0, 0, 0, 0.10) 0%, rgba(0, 0, 0, 0.00) 50%)",
},
"top-left": {
className: "top-0 left-0",
background: "linear-gradient(139deg, rgba(0, 0, 0, 0.10) 0%, rgba(0, 0, 0, 0.00) 40.64%)",
},
"bottom-right": {
className: "bottom-0 right-0",
background: "linear-gradient(148deg, rgba(0, 0, 0, 0.00) 58.58%, rgba(0, 0, 0, 0.10) 97.86%)",
},
"bottom-left": {
className: "bottom-0 left-0",
background: "linear-gradient(214deg, rgba(0, 0, 0, 0.00) 54.54%, rgba(0, 0, 0, 0.10) 95.71%)",
},
};
// Deduplicated list of positions
const positions = toasts.reduce<NonNullable<ToasterProps["position"]>[]>((acc, t) => {
acc.push(t.position || DEFAULT_TOAST_POSITION);
return acc;
}, []);
return (
<>
{Object.entries(styles).map(([position, style]) => (
<div
key={position}
className={cx(
"pointer-events-none fixed z-40 hidden h-72.5 w-130 transition duration-500 xs:block",
style.className,
positions.includes(position as keyof typeof styles) ? "visible opacity-100" : "invisible opacity-0",
)}
style={{
background: style.background,
}}
/>
))}
<div
className={cx(
"pointer-events-none fixed right-0 bottom-0 left-0 z-40 h-67.5 w-full bg-linear-to-t from-black/10 to-transparent transition duration-500 xs:hidden",
positions.length > 0 ? "visible opacity-100" : "invisible opacity-0",
)}
/>
</>
);
};
export const Toaster = () => (
<>
<SonnerToaster
position={DEFAULT_TOAST_POSITION}
style={
{
"--width": "400px",
} as React.CSSProperties
}
/>
<ToastsOverlay />
</>
);