diff --git a/apps/site/components/withBanner.tsx b/apps/site/components/withBanner.tsx index 1023051e1d0d5..b8a186d01b23a 100644 --- a/apps/site/components/withBanner.tsx +++ b/apps/site/components/withBanner.tsx @@ -1,6 +1,9 @@ +'use client'; + import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; import Banner from '@node-core/ui-components/Common/Banner'; import { useTranslations } from 'next-intl'; +import { useEffect, useState } from 'react'; import Link from '#site/components/Link'; import { siteConfig } from '#site/next.json.mjs'; @@ -8,17 +11,35 @@ import { dateIsBetween } from '#site/util/date'; import type { FC } from 'react'; +const STORAGE_KEY = 'banner-dismissal'; + const WithBanner: FC<{ section: string }> = ({ section }) => { const banner = siteConfig.websiteBanners[section]; const t = useTranslations(); - if (banner && dateIsBetween(banner.startDate, banner.endDate)) { + const [open, setOpen] = useState(false); + + useEffect(() => { + if (banner) { + // eslint-disable-next-line @eslint-react/set-state-in-effect + setOpen(localStorage.getItem(STORAGE_KEY) !== banner.text); + } + }, [banner]); + + if (banner && open && dateIsBetween(banner.startDate, banner.endDate)) { const bannerType = banner.type || 'default'; + const onClose = () => { + localStorage.setItem(STORAGE_KEY, banner.text); + setOpen(false); + }; + return ( {banner.link ? ( {banner.text} diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 4fbbeb0c19b46..6c8466e119bf2 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -191,7 +191,8 @@ "banner": { "default": "Announcement", "warning": "Warning notification", - "error": "Error notification" + "error": "Error notification", + "close": "Close banner" }, "search": { "searchPlaceholder": "Start typing...", diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json index b05e7e5119a64..76a3b62338c4a 100644 --- a/packages/ui-components/package.json +++ b/packages/ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@node-core/ui-components", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "exports": { "./*": { diff --git a/packages/ui-components/src/Common/Banner/index.module.css b/packages/ui-components/src/Common/Banner/index.module.css index d55d5359cf8c8..d9bf2cc94b981 100644 --- a/packages/ui-components/src/Common/Banner/index.module.css +++ b/packages/ui-components/src/Common/Banner/index.module.css @@ -1,12 +1,11 @@ @reference "../../styles/index.css"; .banner { - @apply flex + @apply animate-fade-and-slide-down + relative + grid w-full - flex-row - items-center - justify-center - gap-2 + grid-rows-1 px-8 py-3 text-sm; @@ -29,6 +28,15 @@ } } +.bannerContent { + @apply flex + min-h-0 + flex-row + items-center + justify-center + gap-2; +} + .default { @apply bg-brand-600; } @@ -40,3 +48,15 @@ .warning { @apply bg-warning-600; } + +.closeButton { + @apply absolute + right-4 + text-lg; + + span { + @apply text-white/60 + transition-colors + hover:text-white; + } +} diff --git a/packages/ui-components/src/Common/Banner/index.stories.tsx b/packages/ui-components/src/Common/Banner/index.stories.tsx index 60fc4c4d2e336..1ca61a6312971 100644 --- a/packages/ui-components/src/Common/Banner/index.stories.tsx +++ b/packages/ui-components/src/Common/Banner/index.stories.tsx @@ -26,4 +26,12 @@ export const Warning: Story = { }, }; +export const Dismissible: Story = { + args: { + children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'default', + onClose: () => {}, + }, +}; + export default { component: Banner } as Meta; diff --git a/packages/ui-components/src/Common/Banner/index.tsx b/packages/ui-components/src/Common/Banner/index.tsx index b28ceade24816..b3cba18be84ec 100644 --- a/packages/ui-components/src/Common/Banner/index.tsx +++ b/packages/ui-components/src/Common/Banner/index.tsx @@ -1,21 +1,44 @@ -import type { FC, HTMLAttributes, PropsWithChildren } from 'react'; +import classNames from 'classnames'; + +import type { + FC, + HTMLAttributes, + MouseEventHandler, + PropsWithChildren, +} from 'react'; import styles from './index.module.css'; export type BannerProps = { type?: 'default' | 'warning' | 'error'; + closeButtonAriaLabel?: string; + onClose?: MouseEventHandler; } & HTMLAttributes; const Banner: FC> = ({ type = 'default', + closeButtonAriaLabel = 'Close banner', + onClose, children, ...props }) => (
- {children} +
+ {children} + {onClose && ( + + )} +
); diff --git a/packages/ui-components/src/styles/animations.css b/packages/ui-components/src/styles/animations.css index 8be5edac4e807..c16c21240ad67 100644 --- a/packages/ui-components/src/styles/animations.css +++ b/packages/ui-components/src/styles/animations.css @@ -7,6 +7,8 @@ --animate-dot-move-delay-400: dot-move 1.2s infinite ease-in-out 400ms; --animate-slide-to-left: slide-to-left 300ms ease-in-out; --animate-slide-to-right: slide-to-right 300ms ease-in-out; + --animate-fade-and-slide-down: fade-and-slide-down 300ms ease-out 400ms + backwards; @keyframes surf { 0% { @@ -86,4 +88,20 @@ transform: translateX(0); } } + + @keyframes fade-and-slide-down { + from { + grid-template-rows: 0fr; + opacity: 0; + padding-block: 0; + transform: translateY(-100%); + } + + to { + grid-template-rows: 1fr; + opacity: 1; + padding-block: calc(var(--spacing, 0.25rem) * 3); + transform: translateY(0); + } + } }