Initial commit
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
|
||||
import { closeDialog, openDialog } from '@/state/dialogs';
|
||||
|
||||
import { getActiveDialog } from '@/state/dialogsSelectors';
|
||||
|
||||
import { ClosePositionDialog } from '@/views/dialogs/ClosePositionDialog';
|
||||
import { DepositDialog } from '@/views/dialogs/DepositDialog';
|
||||
import { DisconnectDialog } from '@/views/dialogs/DisconnectDialog';
|
||||
import { ExchangeOfflineDialog } from '@/views/dialogs/ExchangeOfflineDialog';
|
||||
import { HelpDialog } from '@/views/dialogs/HelpDialog';
|
||||
import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog';
|
||||
import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog';
|
||||
import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
|
||||
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
|
||||
import { TradeDialog } from '@/views/dialogs/TradeDialog';
|
||||
import { TransferDialog } from '@/views/dialogs/TransferDialog';
|
||||
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
|
||||
|
||||
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
|
||||
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
|
||||
|
||||
export const DialogManager = () => {
|
||||
const dispatch = useDispatch();
|
||||
const activeDialog = useSelector(getActiveDialog);
|
||||
|
||||
if (!activeDialog) return null;
|
||||
const { dialogProps, type } = activeDialog;
|
||||
|
||||
const modalProps = {
|
||||
...dialogProps,
|
||||
setIsOpen: (isOpen: boolean) => {
|
||||
dispatch(
|
||||
isOpen
|
||||
? openDialog({ type: activeDialog.type, dialogProps: activeDialog.dialogProps })
|
||||
: closeDialog()
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
[DialogTypes.ClosePosition]: <ClosePositionDialog {...modalProps} />,
|
||||
[DialogTypes.Deposit]: <DepositDialog {...modalProps} />,
|
||||
[DialogTypes.DisconnectWallet]: <DisconnectDialog {...modalProps} />,
|
||||
[DialogTypes.ExchangeOffline]: <ExchangeOfflineDialog {...modalProps} />,
|
||||
[DialogTypes.FillDetails]: <FillDetailsDialog {...modalProps} />,
|
||||
[DialogTypes.Help]: <HelpDialog {...modalProps} />,
|
||||
[DialogTypes.MnemonicExport]: <MnemonicExportDialog {...modalProps} />,
|
||||
[DialogTypes.MobileSignIn]: <MobileSignInDialog {...modalProps} />,
|
||||
[DialogTypes.Onboarding]: <OnboardingDialog {...modalProps} />,
|
||||
[DialogTypes.OrderDetails]: <OrderDetailsDialog {...modalProps} />,
|
||||
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
|
||||
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
|
||||
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
|
||||
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
|
||||
}[type];
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import styled, { type AnyStyledComponent, css } from 'styled-components';
|
||||
|
||||
import { AbacusApiStatus } from '@/constants/abacus';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
|
||||
import { useApiState, useStringGetter } from '@/hooks';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { Details } from '@/components/Details';
|
||||
import { Output, OutputType } from '@/components/Output';
|
||||
import { WithTooltip } from '@/components/WithTooltip';
|
||||
|
||||
enum FooterItems {
|
||||
ChainHeight,
|
||||
IndexerHeight,
|
||||
}
|
||||
|
||||
enum ExchangeStatus {
|
||||
Operational = 'Operational',
|
||||
Degraded = 'Degraded',
|
||||
}
|
||||
|
||||
export const FooterDesktop = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
const { height, indexerHeight, status, statusErrorMessage } = useApiState();
|
||||
|
||||
const { exchangeStatus, label } =
|
||||
!status || status === AbacusApiStatus.NORMAL
|
||||
? {
|
||||
exchangeStatus: ExchangeStatus.Operational,
|
||||
label: stringGetter({ key: STRING_KEYS.OPERATIONAL }),
|
||||
}
|
||||
: {
|
||||
exchangeStatus: ExchangeStatus.Degraded,
|
||||
label: stringGetter({ key: STRING_KEYS.DEGRADED }),
|
||||
};
|
||||
|
||||
return (
|
||||
<Styled.Footer>
|
||||
<WithTooltip
|
||||
slotTooltip={
|
||||
statusErrorMessage && (
|
||||
<dl>
|
||||
<dd>{statusErrorMessage}</dd>
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Styled.Row>
|
||||
<Styled.StatusDot exchangeStatus={exchangeStatus} />
|
||||
<span>{label}</span>
|
||||
</Styled.Row>
|
||||
</WithTooltip>
|
||||
{import.meta.env.MODE !== 'production' && (
|
||||
<Styled.Details
|
||||
withSeparators
|
||||
items={[
|
||||
{
|
||||
key: FooterItems.ChainHeight,
|
||||
label: 'Block Height',
|
||||
value: <Output useGrouping type={OutputType.Number} value={height} />,
|
||||
},
|
||||
height !== indexerHeight && {
|
||||
key: FooterItems.IndexerHeight,
|
||||
label: 'Indexer Block Height',
|
||||
value: (
|
||||
<Styled.WarningOutput useGrouping type={OutputType.Number} value={indexerHeight} />
|
||||
),
|
||||
},
|
||||
].filter(Boolean)}
|
||||
layout="row"
|
||||
/>
|
||||
)}
|
||||
</Styled.Footer>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Footer = styled.footer`
|
||||
${layoutMixins.stickyFooter}
|
||||
${layoutMixins.spacedRow}
|
||||
grid-area: Footer;
|
||||
|
||||
font-size: 0.66em;
|
||||
`;
|
||||
|
||||
Styled.Row = styled.div`
|
||||
${layoutMixins.row}
|
||||
padding: 0 1rem;
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
Styled.StatusDot = styled.div<{ exchangeStatus: ExchangeStatus }>`
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: 50%;
|
||||
|
||||
background-color: ${({ exchangeStatus }) =>
|
||||
({
|
||||
[ExchangeStatus.Degraded]: css`var(--color-warning)`,
|
||||
[ExchangeStatus.Operational]: css`var(--color-positive)`,
|
||||
}[exchangeStatus])};
|
||||
`;
|
||||
|
||||
Styled.WarningOutput = styled(Output)`
|
||||
color: var(--color-warning);
|
||||
`;
|
||||
|
||||
Styled.Details = styled(Details)`
|
||||
${layoutMixins.scrollArea}
|
||||
|
||||
padding: 0 0.5em;
|
||||
`;
|
||||
@@ -0,0 +1,170 @@
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ButtonAction, ButtonSize } from '@/constants/buttons';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { DEFAULT_MARKETID } from '@/constants/markets';
|
||||
import { AppRoute } from '@/constants/routes';
|
||||
|
||||
import { useShouldShowFooter, useStringGetter } from '@/hooks';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { NavigationMenu } from '@/components/NavigationMenu';
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { BellIcon, MarketsIcon, PlayIcon, PortfolioIcon, ProfileIcon, TradeIcon } from '@/icons';
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
|
||||
import { calculateCanAccountTrade } from '@/state/accountCalculators';
|
||||
import { openDialog } from '@/state/dialogs';
|
||||
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
|
||||
|
||||
export const FooterMobile = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const canAccountTrade = useSelector(calculateCanAccountTrade);
|
||||
|
||||
const marketId = useSelector(getCurrentMarketId);
|
||||
|
||||
if (!useShouldShowFooter()) return null;
|
||||
|
||||
return (
|
||||
<Styled.MobileNav>
|
||||
<Styled.NavigationMenu
|
||||
items={[
|
||||
{
|
||||
group: 'navigation',
|
||||
items: [
|
||||
canAccountTrade
|
||||
? {
|
||||
value: 'trade',
|
||||
label: stringGetter({ key: STRING_KEYS.TRADE }),
|
||||
slotBefore: (
|
||||
<Styled.IconButton
|
||||
iconComponent={TradeIcon}
|
||||
size={ButtonSize.Large}
|
||||
action={ButtonAction.Primary}
|
||||
/>
|
||||
),
|
||||
href: `${AppRoute.Trade}/${marketId ?? DEFAULT_MARKETID}`,
|
||||
}
|
||||
: {
|
||||
value: 'onboarding',
|
||||
label: stringGetter({ key: STRING_KEYS.ONBOARDING }),
|
||||
slotBefore: (
|
||||
<Styled.IconButton
|
||||
iconComponent={PlayIcon}
|
||||
size={ButtonSize.Large}
|
||||
action={ButtonAction.Primary}
|
||||
/>
|
||||
),
|
||||
onClick: () => dispatch(openDialog({ type: DialogTypes.Onboarding })),
|
||||
},
|
||||
{
|
||||
value: 'portfolio',
|
||||
label: stringGetter({ key: STRING_KEYS.PORTFOLIO }),
|
||||
slotBefore: <Styled.Icon iconComponent={PortfolioIcon} />,
|
||||
href: AppRoute.Portfolio,
|
||||
},
|
||||
{
|
||||
value: 'markets',
|
||||
label: stringGetter({ key: STRING_KEYS.MARKETS }),
|
||||
slotBefore: <Styled.Icon iconComponent={MarketsIcon} />,
|
||||
href: AppRoute.Markets,
|
||||
},
|
||||
{
|
||||
value: 'alerts',
|
||||
label: stringGetter({ key: STRING_KEYS.ALERTS }),
|
||||
slotBefore: <Styled.Icon iconComponent={BellIcon} />,
|
||||
href: AppRoute.Alerts,
|
||||
},
|
||||
{
|
||||
value: 'profile',
|
||||
label: stringGetter({ key: STRING_KEYS.PROFILE }),
|
||||
slotBefore: <Styled.Icon iconComponent={ProfileIcon} />,
|
||||
href: AppRoute.Profile,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
orientation="horizontal"
|
||||
itemOrientation="vertical"
|
||||
/>
|
||||
</Styled.MobileNav>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.MobileNav = styled.footer`
|
||||
grid-area: Footer;
|
||||
|
||||
${layoutMixins.stickyFooter}
|
||||
`;
|
||||
|
||||
Styled.NavigationMenu = styled(NavigationMenu)`
|
||||
--navigationMenu-height: var(--page-currentFooterHeight);
|
||||
--navigationMenu-item-height: var(--page-currentFooterHeight);
|
||||
--navigationMenu-item-radius: 0;
|
||||
|
||||
--navigationMenu-item-checked-backgroundColor: transparent;
|
||||
--navigationMenu-item-highlighted-backgroundColor: transparent;
|
||||
--navigationMenu-item-highlighted-textColor: var(--color-text-2);
|
||||
--navigationMenu-item-radius: 0px;
|
||||
--navigationMenu-item-padding: 0px;
|
||||
|
||||
font: var(--font-tiny-book);
|
||||
|
||||
> section {
|
||||
flex: 1;
|
||||
|
||||
ul[data-orientation='horizontal'] {
|
||||
gap: 0;
|
||||
|
||||
> li {
|
||||
flex: 1;
|
||||
|
||||
&[data-item='onboarding'],
|
||||
&[data-item='trade'] {
|
||||
span {
|
||||
content-visibility: hidden;
|
||||
position: absolute;
|
||||
|
||||
@supports not (content-visibility: hidden) {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
+ *,
|
||||
+ * + * {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> li:not(:last-child):after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
width: var(--border-width);
|
||||
top: 12.5%;
|
||||
bottom: 12.5%;
|
||||
right: 0;
|
||||
|
||||
background: linear-gradient(to bottom, transparent, var(--border-color), transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.IconButton = styled(IconButton)`
|
||||
margin-top: -0.25rem;
|
||||
`;
|
||||
|
||||
Styled.Icon = styled(Icon)`
|
||||
font-size: 1.5rem;
|
||||
`;
|
||||
@@ -0,0 +1,216 @@
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { ButtonShape } from '@/constants/buttons';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { AppRoute } from '@/constants/routes';
|
||||
import { useStringGetter } from '@/hooks';
|
||||
import { LogoShortIcon } from '@/icons';
|
||||
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { BellIcon } from '@/icons';
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
import { NavigationMenu } from '@/components/NavigationMenu';
|
||||
import { VerticalSeparator } from '@/components/Separator';
|
||||
|
||||
import { AccountMenu } from '@/views/menus/AccountMenu';
|
||||
import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu';
|
||||
import { NotificationsMenu } from '@/views/menus/NotificationsMenu';
|
||||
import { LanguageSelector } from '@/views/menus/LanguageSelector';
|
||||
|
||||
import { openDialog } from '@/state/dialogs';
|
||||
|
||||
import { headerMixins } from '@/styles/headerMixins';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
export const HeaderDesktop = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
group: 'navigation',
|
||||
items: [
|
||||
{
|
||||
value: 'MARKET',
|
||||
label: stringGetter({ key: STRING_KEYS.MARKET }),
|
||||
href: AppRoute.Markets,
|
||||
},
|
||||
{
|
||||
value: 'TRADE',
|
||||
label: stringGetter({ key: STRING_KEYS.TRADE }),
|
||||
href: AppRoute.Trade,
|
||||
},
|
||||
{
|
||||
value: 'PORTFOLIO',
|
||||
label: stringGetter({ key: STRING_KEYS.PORTFOLIO }),
|
||||
href: AppRoute.Portfolio,
|
||||
},
|
||||
{
|
||||
value: 'MORE',
|
||||
label: stringGetter({ key: STRING_KEYS.MORE }),
|
||||
subitems: [
|
||||
{
|
||||
value: 'DOCUMENTATION',
|
||||
slotBefore: <Icon iconName={IconName.Terminal} />,
|
||||
label: stringGetter({ key: STRING_KEYS.DOCUMENTATION }),
|
||||
href: 'https://v4-teacher.vercel.app/',
|
||||
},
|
||||
{
|
||||
value: 'MINTSCAN',
|
||||
slotBefore: <Icon iconName={IconName.Mintscan} />,
|
||||
label: stringGetter({ key: STRING_KEYS.MINTSCAN }),
|
||||
href: 'https://testnet.mintscan.io/dydx-testnet',
|
||||
},
|
||||
{
|
||||
value: 'COMMUNITY',
|
||||
slotBefore: <Icon iconName={IconName.Discord} />,
|
||||
label: stringGetter({ key: STRING_KEYS.COMMUNITY }),
|
||||
href: 'https://discord.gg/dydx',
|
||||
},
|
||||
{
|
||||
value: 'TERMS_OF_USE',
|
||||
slotBefore: <Icon iconName={IconName.File} />,
|
||||
label: stringGetter({ key: STRING_KEYS.TERMS_OF_USE }),
|
||||
href: 'https://dydx.exchange/terms',
|
||||
},
|
||||
{
|
||||
value: 'PRIVACY_POLICY',
|
||||
slotBefore: <Icon iconName={IconName.Privacy} />,
|
||||
label: stringGetter({ key: STRING_KEYS.PRIVACY_POLICY }),
|
||||
href: 'https://dydx.exchange/privacy',
|
||||
},
|
||||
{
|
||||
value: 'HELP',
|
||||
slotBefore: <Icon iconName={IconName.HelpCircle} />,
|
||||
label: stringGetter({ key: STRING_KEYS.HELP }),
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
dispatch(openDialog({ type: DialogTypes.Help }));
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Styled.Header>
|
||||
<Styled.LogoLink to="/">
|
||||
<LogoShortIcon />
|
||||
</Styled.LogoLink>
|
||||
|
||||
<VerticalSeparator />
|
||||
|
||||
<Styled.NavBefore>
|
||||
<NetworkSelectMenu sideOffset={16} />
|
||||
<VerticalSeparator />
|
||||
<LanguageSelector sideOffset={16} />
|
||||
</Styled.NavBefore>
|
||||
|
||||
<VerticalSeparator />
|
||||
|
||||
<Styled.NavigationMenu items={navItems} orientation="horizontal" />
|
||||
|
||||
<div role="separator" />
|
||||
|
||||
<Styled.NavAfter>
|
||||
<Styled.IconButton
|
||||
shape={ButtonShape.Rectangle}
|
||||
iconName={IconName.HelpCircle}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))}
|
||||
/>
|
||||
|
||||
<VerticalSeparator />
|
||||
|
||||
<NotificationsMenu
|
||||
slotTrigger={<Styled.IconButton shape={ButtonShape.Rectangle} iconComponent={BellIcon} />}
|
||||
/>
|
||||
|
||||
<VerticalSeparator />
|
||||
|
||||
<AccountMenu />
|
||||
</Styled.NavAfter>
|
||||
</Styled.Header>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Header = styled.header`
|
||||
--header-horizontal-padding-mobile: 0.5rem;
|
||||
--logo-width: 3.5rem;
|
||||
|
||||
${layoutMixins.container}
|
||||
${layoutMixins.stickyHeader}
|
||||
${layoutMixins.scrollSnapItem}
|
||||
backdrop-filter: none;
|
||||
|
||||
grid-area: Header;
|
||||
|
||||
display: grid;
|
||||
align-items: stretch;
|
||||
grid-auto-flow: column;
|
||||
grid-template:
|
||||
'Logo . NavBefore . Nav . NavAfter' 100%
|
||||
/ var(--logo-width) var(--border-width) calc(
|
||||
var(--sidebar-width) - var(--logo-width) - var(--border-width)
|
||||
)
|
||||
var(--border-width) 1fr var(--border-width) auto;
|
||||
|
||||
font-size: 0.9375em;
|
||||
|
||||
:before {
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.NavigationMenu = styled(NavigationMenu)`
|
||||
& {
|
||||
--navigationMenu-height: var(--stickyArea-topHeight);
|
||||
}
|
||||
|
||||
${layoutMixins.scrollArea}
|
||||
padding: 0 0.5rem;
|
||||
scroll-padding: 0 0.5rem;
|
||||
`;
|
||||
|
||||
Styled.NavBefore = styled.div`
|
||||
${layoutMixins.flexEqualColumns}
|
||||
|
||||
> * {
|
||||
align-self: center;
|
||||
margin: 0 0.4rem;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.LogoLink = styled(Link)`
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
|
||||
> svg {
|
||||
margin: auto;
|
||||
width: auto;
|
||||
height: 45%;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.NavAfter = styled.div`
|
||||
${layoutMixins.row}
|
||||
justify-self: end;
|
||||
padding-right: 0.75rem;
|
||||
|
||||
gap: 0.5rem;
|
||||
|
||||
a {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.IconButton = styled(IconButton)`
|
||||
${headerMixins.button}
|
||||
--button-border: none;
|
||||
`;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { NotificationStatus } from '@/constants/notifications';
|
||||
import { ButtonSize } from '@/constants/buttons';
|
||||
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { useBreakpoints } from '@/hooks/useBreakpoints';
|
||||
|
||||
import { ToastArea } from '@/components/ToastArea';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { Button } from '@/components/Button';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
type StyleProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const NotificationsToastArea = ({ className }: StyleProps) => {
|
||||
const { notifications, getKey, getDisplayData, markUnseen, markSeen, isMenuOpen, onNotificationAction } = useNotifications();
|
||||
|
||||
const { isMobile } = useBreakpoints();
|
||||
|
||||
return (
|
||||
<$ToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
|
||||
{Object.values(notifications)
|
||||
// Sort by time of first trigger
|
||||
.sort(
|
||||
(n1, n2) =>
|
||||
n1.timestamps[NotificationStatus.Triggered]! -
|
||||
n2.timestamps[NotificationStatus.Triggered]!
|
||||
)
|
||||
.map((notification) => ({
|
||||
notification,
|
||||
key: getKey(notification),
|
||||
displayData: getDisplayData(notification),
|
||||
}))
|
||||
.filter(({ displayData }) => displayData)
|
||||
.map(({ notification, key, displayData }) => (
|
||||
<Toast
|
||||
key={key}
|
||||
isOpen={notification.status < NotificationStatus.Unseen}
|
||||
slotIcon={displayData.icon}
|
||||
slotTitle={displayData.title}
|
||||
slotDescription={displayData.description}
|
||||
slotCustomContent={displayData.customContent}
|
||||
slotAction={
|
||||
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
|
||||
{displayData.actionDescription}
|
||||
</Button>
|
||||
}
|
||||
actionDescription={displayData.actionDescription}
|
||||
actionAltText={displayData.actionAltText}
|
||||
duration={isMenuOpen ? Infinity : displayData.toastDuration}
|
||||
sensitivity={displayData.toastSensitivity}
|
||||
setIsOpen={(isOpen, isClosedFromTimeout) => {
|
||||
if (!isOpen)
|
||||
// Toast timer expired without user interaction
|
||||
if (isClosedFromTimeout) markUnseen(notification);
|
||||
// Toast interacted with or dismissed
|
||||
else markSeen(notification);
|
||||
}}
|
||||
lastUpdated={notification.timestamps[notification.status]}
|
||||
/>
|
||||
))}
|
||||
</$ToastArea>
|
||||
);
|
||||
};
|
||||
|
||||
const $ToastArea = styled(ToastArea)`
|
||||
position: absolute;
|
||||
width: min(16rem, 100%);
|
||||
inset: 0 0 0 auto;
|
||||
|
||||
padding: 0.75rem 0.75rem 0.75rem 0;
|
||||
|
||||
mask-image: linear-gradient(to left, transparent, white 0.5rem);
|
||||
`;
|
||||
Reference in New Issue
Block a user