-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathNotificationBanners.tsx
More file actions
104 lines (97 loc) · 3.45 KB
/
Copy pathNotificationBanners.tsx
File metadata and controls
104 lines (97 loc) · 3.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* `displayType: 'banner'` — a persistent, page-width strip.
*
* Unlike the overlay surfaces, a banner is IN FLOW: it takes vertical space and
* pushes the content down, which is what makes it read as a standing condition
* ("you are viewing a draft", "the connection dropped") rather than as an event
* that just happened. Placement is therefore the host's: mount this at the top
* of the content area, inside the `NotificationProvider`.
*/
import * as React from 'react';
import { X } from 'lucide-react';
import { useNotifications, useNotificationsByPresentation } from '@object-ui/react';
import { cn } from '../lib/utils';
import { Button } from '../ui/button';
import { notificationIcon, notificationSeverityStyle } from './severity';
export interface NotificationBannersProps {
/** Extra classes for the banner stack container. */
className?: string;
/**
* Cap on simultaneously rendered banners (newest first). Banners are
* persistent, so an uncapped stack can swallow the viewport. Default 3.
*/
max?: number;
}
/**
* Renders every active `banner` notification as a stacked strip.
*
* @example
* ```tsx
* <main>
* <NotificationBanners />
* <Outlet />
* </main>
* ```
*/
export function NotificationBanners({ className, max = 3 }: NotificationBannersProps) {
const banners = useNotificationsByPresentation('banner');
const { dismiss } = useNotifications();
if (banners.length === 0) return null;
return (
<div className={cn('flex w-full flex-col', className)} data-notification-surface="banner">
{banners.slice(0, max).map((notification) => {
const { tone } = notificationSeverityStyle(notification.severity);
const Icon = notificationIcon(notification);
return (
<div
key={notification.id}
role="status"
data-severity={notification.severity}
className={cn(
'flex w-full items-center gap-3 border-b px-4 py-2.5 text-sm',
tone,
)}
>
<Icon className="h-4 w-4 shrink-0" aria-hidden="true" />
<div className="flex min-w-0 flex-1 flex-wrap items-baseline gap-x-2 gap-y-0.5">
<span className="font-medium">{notification.title}</span>
{notification.message ? (
<span className="min-w-0 opacity-90">{notification.message}</span>
) : null}
</div>
{notification.actions?.map((action) => (
<Button
key={action.label}
size="sm"
variant={action.variant ?? 'outline'}
className="h-7 shrink-0"
onClick={action.onClick}
>
{action.label}
</Button>
))}
{notification.dismissible === false ? null : (
<Button
size="icon"
variant="ghost"
className="h-7 w-7 shrink-0"
aria-label={`Dismiss ${notification.title}`}
onClick={() => dismiss(notification.id)}
>
<X className="h-4 w-4" aria-hidden="true" />
</Button>
)}
</div>
);
})}
</div>
);
}
NotificationBanners.displayName = 'NotificationBanners';