mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
style: improve the ui of the toast for notifications
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { ActionIcon, Box, Group, Stack, Text, ThemeIcon } from "@mantine/core";
|
||||
import { Bell, ChevronRight, X } from "lucide-react";
|
||||
|
||||
import { timeAgo } from "./timeAgo";
|
||||
import type { NotificationItemData, NotificationVisual } from "./types";
|
||||
|
||||
export interface NotificationToastProps {
|
||||
item: NotificationItemData;
|
||||
/** Resolved by the hosting app's registry from `item.type`/`item.data`. */
|
||||
visual?: NotificationVisual;
|
||||
/** react-hot-toast enter/exit flag; drives the slide/fade animation. */
|
||||
visible?: boolean;
|
||||
/** Whole-card click (usually: mark read + navigate). Shows a chevron affordance. */
|
||||
onClick?: () => void;
|
||||
onDismiss?: () => void;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rich, ERP-grade notification toast: a colored-accent card with an icon, a
|
||||
* bold title, a two-line body, a relative timestamp, and a dismiss button.
|
||||
* Presentational — the host resolves the visual and click action and fires it
|
||||
* via `toast.custom`.
|
||||
*/
|
||||
export function NotificationToast({
|
||||
item,
|
||||
visual,
|
||||
visible = true,
|
||||
onClick,
|
||||
onDismiss,
|
||||
width = 380,
|
||||
}: NotificationToastProps) {
|
||||
const color = visual?.color ?? "blue";
|
||||
const icon = visual?.icon ?? <Bell size={18} strokeWidth={2} />;
|
||||
const clickable = Boolean(onClick);
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
maxWidth: "calc(100vw - 32px)",
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
borderRadius: "var(--mantine-radius-md)",
|
||||
background: "var(--mantine-color-body)",
|
||||
border: "1px solid var(--mantine-color-default-border)",
|
||||
boxShadow: "var(--mantine-shadow-lg)",
|
||||
pointerEvents: "auto",
|
||||
transform: visible ? "translateX(0)" : "translateX(16px)",
|
||||
opacity: visible ? 1 : 0,
|
||||
transition: "transform 180ms ease, opacity 180ms ease",
|
||||
}}
|
||||
>
|
||||
{/* colored accent rail */}
|
||||
<Box
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 4,
|
||||
flexShrink: 0,
|
||||
background: `var(--mantine-color-${color}-6)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
role={clickable ? "button" : undefined}
|
||||
tabIndex={clickable ? 0 : undefined}
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => {
|
||||
if (clickable && (e.key === "Enter" || e.key === " ")) {
|
||||
e.preventDefault();
|
||||
onClick?.();
|
||||
}
|
||||
}}
|
||||
p="sm"
|
||||
style={{ flex: 1, minWidth: 0, cursor: clickable ? "pointer" : "default" }}
|
||||
>
|
||||
<Group align="flex-start" wrap="nowrap" gap="sm">
|
||||
<ThemeIcon
|
||||
variant="light"
|
||||
color={color}
|
||||
radius="xl"
|
||||
size={40}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text
|
||||
fw={600}
|
||||
size="sm"
|
||||
lineClamp={1}
|
||||
pr={onDismiss ? 20 : 0}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text c="dimmed" size="xs" lineClamp={2} style={{ lineHeight: 1.4 }}>
|
||||
{item.body}
|
||||
</Text>
|
||||
<Group justify="space-between" wrap="nowrap" gap={6} mt={4}>
|
||||
<Text c="dimmed" size="xs" style={{ whiteSpace: "nowrap" }}>
|
||||
{timeAgo(item.createdAt)}
|
||||
</Text>
|
||||
{clickable && (
|
||||
<Group gap={2} wrap="nowrap">
|
||||
<Text size="xs" fw={600} c={`${color}.6`}>
|
||||
View details
|
||||
</Text>
|
||||
<ChevronRight
|
||||
size={13}
|
||||
color={`var(--mantine-color-${color}-6)`}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{onDismiss && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
radius="xl"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss();
|
||||
}}
|
||||
style={{ position: "absolute", top: 6, right: 6 }}
|
||||
>
|
||||
<X size={14} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationToast;
|
||||
@@ -7,6 +7,9 @@ export type { NotificationItemProps } from "./NotificationItem";
|
||||
export { NotificationDrawer } from "./NotificationDrawer";
|
||||
export type { NotificationDrawerProps } from "./NotificationDrawer";
|
||||
|
||||
export { NotificationToast } from "./NotificationToast";
|
||||
export type { NotificationToastProps } from "./NotificationToast";
|
||||
|
||||
export type {
|
||||
NotificationItemData,
|
||||
NotificationVisual,
|
||||
|
||||
@@ -32,11 +32,13 @@ export {
|
||||
NotificationBell,
|
||||
NotificationItem,
|
||||
NotificationDrawer,
|
||||
NotificationToast,
|
||||
} from "./components/NotificationBell";
|
||||
export type {
|
||||
NotificationBellProps,
|
||||
NotificationItemProps,
|
||||
NotificationDrawerProps,
|
||||
NotificationToastProps,
|
||||
NotificationItemData,
|
||||
NotificationVisual,
|
||||
ResolveNotificationVisual,
|
||||
|
||||
Reference in New Issue
Block a user